Skip to content

Fix memory consumption on empty/small messages - #13393

Merged
Dreamsorcerer merged 32 commits into
masterfrom
fix-ws-queue
Aug 24, 2026
Merged

Fix memory consumption on empty/small messages#13393
Dreamsorcerer merged 32 commits into
masterfrom
fix-ws-queue

Conversation

@Dreamsorcerer

Copy link
Copy Markdown
Member

No description provided.

@Dreamsorcerer Dreamsorcerer added backport-3.14 Trigger automatic backporting to the 3.14 release branch by Patchback robot backport-3.15 Trigger automatic backporting to the 3.15 release branch by Patchback robot labels Aug 11, 2026
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided There is a change note present in this PR label Aug 11, 2026
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.02%. Comparing base (5fa8584) to head (d5344ef).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #13393      +/-   ##
==========================================
+ Coverage   99.01%   99.02%   +0.01%     
==========================================
  Files         135      135              
  Lines       50048    50445     +397     
  Branches     2614     2645      +31     
==========================================
+ Hits        49553    49952     +399     
+ Misses        371      370       -1     
+ Partials      124      123       -1     
Flag Coverage Δ
Autobahn 22.04% <18.87%> (+0.01%) ⬆️
CI-GHA 98.91% <100.00%> (+0.01%) ⬆️
OS-Linux 98.69% <100.00%> (+0.02%) ⬆️
OS-Windows 97.10% <96.52%> (+0.10%) ⬆️
OS-macOS 98.02% <96.52%> (+0.09%) ⬆️
Py-3.10 98.11% <100.00%> (+0.02%) ⬆️
Py-3.11 98.35% <100.00%> (+0.01%) ⬆️
Py-3.12 98.43% <100.00%> (+0.01%) ⬆️
Py-3.13 98.42% <100.00%> (+0.01%) ⬆️
Py-3.14 98.45% <100.00%> (+0.01%) ⬆️
Py-3.14t 97.61% <96.52%> (+0.09%) ⬆️
Py-pypy-3.11 97.40% <100.00%> (+0.02%) ⬆️
VM-macos 98.02% <96.52%> (+0.09%) ⬆️
VM-ubuntu 98.69% <100.00%> (+0.02%) ⬆️
VM-windows 97.10% <96.52%> (+0.10%) ⬆️
cython-coverage 83.08% <81.94%> (+0.17%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@Dreamsorcerer

Copy link
Copy Markdown
Member Author

@aiolibsbot review

@codspeed-hq

codspeed-hq Bot commented Aug 11, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 94 untouched benchmarks
⏩ 83 skipped benchmarks1


Comparing fix-ws-queue (d5344ef) with master (5fa8584)

Open in CodSpeed

Footnotes

  1. 83 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@aiolibsbot

Copy link
Copy Markdown
Contributor

@Dreamsorcerer: @aiolibsbot review

Reviewed. No blockers. Verified locally, not just by reading.

Cythonized cleanly. Built mask + reader_c. 211 tests pass against compiled reader (parser, client ws functional, web ws functional). Pure Python: 92 pass. black, flake8, mypy clean.

Also probed three failure modes. Protocol error in stashed tail still surfaces correctly, after delivering queued messages. Partial-frame tail leaves no stall. Suspected quadratic re-slice on drain: measured 0.40s vs 0.34s on master for 43,690 frames per read. Not a concern.

Five nits. Two worth a look: new queue↔reader cycle needs breaking (pair survives del with GC disabled), and _feed_data clears _parser_paused before checking size, so one extra message inflates per read that slips through.

@aiolibsbot

aiolibsbot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

@Dreamsorcerer

Copy link
Copy Markdown
Member Author

@aiolibsbot review

@aiolibsbot

aiolibsbot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Reviews (26): Last reviewed commit: "Update reader_py.py" | Re-trigger Greptile

@bdraco

bdraco commented Aug 12, 2026

Copy link
Copy Markdown
Member

@aiolibsbot gh_request

Do a scope analysis on the everything that holds a WebSocketDataQueue to make sure there is no place where we do not hold a strong reference where data could be lost unexpectedly

@aiolibsbot

Copy link
Copy Markdown
Contributor

Reference-scope analysis: who holds the reader, and where data can go missing

Scope: every reference to WebSocketReader and WebSocketDataQueue on this branch, and
whether the parked-frames stash survives each way a connection can end. Tests offered in
#13408 (based on fix-ws-queue).

Who holds the WebSocketReader

Holder Strength Set in Released in
RequestHandler._payload_parser strong set_parser() from WebSocketResponse._post_start connection_lost(), after feed_eof(); data_received()close() on parser eof
ResponseHandler._payload_parser strong set_parser() from ClientSession._ws_connect connection_lost(), after feed_eof(); data_received() on parser eof
WebSocketResponse._parser (new) strong _post_start() never
ClientWebSocketResponse._parser (new) strong _ws_connect() never
WebSocketDataQueue._stalled_reader weak _feed_data() when over the mark top of _feed_data(), set_exception()

Both protocols drop their reference at connection loss, so from that moment the two new
_parser assignments are the only strong reference to a reader that may still hold a
stash. They are load-bearing, and they were the last hole: the reader is constructed in
exactly two places in-tree, both now owned.

Who holds the WebSocketDataQueue

WebSocketReader.queue, WebSocketResponse._reader / ClientWebSocketResponse._reader
(never cleared), and ResponseHandler._payload (cleared in close()/abort()/
connection_lost(), harmless because the response owns it). No weak references — the
queue cannot vanish under a live response. queue._protocol is strong, and
protocol._data_received_cb is a bound method of the response, so the response outlives
the protocol's own teardown.

Finding 1 — the ownership was untested

Reverting either _parser assignment loses 3903 of 8000 already-received frames, with
no exception and a normal CLOSED: the queue drains what it has, the weak reference is
dead, nothing drives the parser again. Nothing in the suite failed. #13408 adds one test
per side, each feeding a single oversized read, dropping the connection synchronously, and
asserting all 8000 arrive (assert 4097 == 8000 without the fix).

Finding 2 — set_exception() drops the stash, and that is a behaviour change

set_exception() clears _stalled_reader ("Nothing more will be parsed"), but
_read_from_buffer() raises only once the buffer is empty, so buffered messages are still
delivered first. Frames the parser stopped short of are not:

this branch:  queued before stall: 4097  → delivered: 4097, then ConnectionResetError
master:       queued: 8000               → delivered: 8000, then ConnectionResetError

(8000 empty TEXT frames in one read, then queue.set_exception(ConnectionResetError()).)

Correct for a parse error, where the reader is poisoned via self._exc anyway. Less
obviously correct for WebSocketResponse._cancel(), which is the transport-died hook and
now silently discards complete frames that master delivered. Worth a deliberate decision
either way — dropping the clear in set_exception() would keep the stash reachable, since
the terminal states that matter for the cycle are already covered by feed_eof() plus the
weak reference.

Side note found while tracing it: _cancel()'s comment says web_protocol calls it from
connection_lost or at shutdown, but the only in-tree callers of _cancel() are
BaseRequest._cancel (line 376/432 of web_protocol.py, a different method) and one
test. Either the comment is stale or a wiring regression is hiding there — either way, WS
handlers currently learn about connection loss via feed_eof(), not _cancel().

Finding 3 — the new rule is invisible to third parties

WebSocketReader is exported in aiohttp.http.__all__ and
aiohttp.http_websocket.__all__. Anyone constructing one and handing it to set_parser()
(proxies, ASGI bridges) must now keep their own strong reference, and the failure mode is
silent: a short stream, or a hang if EOF never arrives. Cheap to mention in the changelog
fragment.

Verified, not assumed

  • Post-EOF drain calls pause_reading()/resume_reading() with transport is None; both
    are guarded, and pause_reading()'s assert self._parser is not None is skipped only
    because _upgraded stays True after connection loss. Safe, but that is the coupling
    holding it up.
  • Driving the parser from _read_from_buffer() is not re-entrant: _release_waiter()
    only sets a future result.
  • Cython: cdef object __weakref__ cythonizes clean and weakref.ref() works on the
    compiled WebSocketReader; 98 parser tests pass against the built extension. (Only
    _websocket/* was compiled — the full build needs a Node.js build of vendored llhttp.)

Analysis by Kōan (Claude Opus 5) at @bdraco's request.

Comment thread tests/test_websocket_parser.py Fixed
Comment thread tests/test_websocket_parser.py Fixed
@Dreamsorcerer

Copy link
Copy Markdown
Member Author

@aiolibsbot review

@bdraco

bdraco commented Aug 13, 2026

Copy link
Copy Markdown
Member

bot was asleep, woke it up

@aiolibsbot

Copy link
Copy Markdown
Contributor

@bdraco: @aiolibsbot gh_request

Scope analysis done. Two holders of WebSocketDataQueue: WebSocketReader.queue, and _reader on both response classes — strong, never cleared. ResponseHandler._payload gets cleared at connection_lost(), harmless: response owns queue. No weakrefs to queue anywhere. Queue was never at risk; reader was.

Reader constructed in exactly two places in-tree, both now owned via _parser. Verified load-bearing: reverted each assignment separately, 3903 of 8000 frames lost silently, normal CLOSED, no test failure before #13408.

One gap remains. WebSocketReader exported publicly. Third-party code building one for set_parser() must keep own strong reference now. Silent truncation otherwise. Worth one changelog line.

Comment thread aiohttp/web_ws.py Dismissed
@bdraco

bdraco commented Aug 18, 2026

Copy link
Copy Markdown
Member

backported manally and tested on HA. no observable regression

@aiolibsbot

aiolibsbot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

@aiolibsbot

aiolibsbot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

@bdraco

bdraco commented Aug 18, 2026

Copy link
Copy Markdown
Member

pause_reading works on a normal asyncio Transport. If they don't implement pause_reading there are many other problems.Not addressing the re-drive finding because it won't happen without a custom transport.

However will address Drain-driven resume re-copies the whole tail on every single pop because its simple and makes sense

@aiolibsbot

Copy link
Copy Markdown
Contributor

@bdraco: pause_reading works on a normal asyncio Transport. If they don't implement pause_reading there a...

Important

Agreed on pause_reading, and the pop-driven re-drive finding no longer applies — 866234b46 fixed it with a low-water mark. Flagging a different path: _read_from_buffer() resumes the transport at _size < _limit while _stalled_reader remains set. Parser stalls at _limit + 1 msg, so two pops drop under _limit. Each resume admits one 256 KiB recv(). Parser then consumes 4 bytes of it. Remainder piles into _tail, re-copied whole on next read. Measured, pure Python, empty TEXT frames: 1000 pops → 500 reads admitted → _tail 131 MB → 74 s CPU. Reproduces with pause_reading fully honoured; aiohttp itself resumes here, not a custom transport. Gate on _stalled_reader is None: _tail stays 0.3 MB, 200 000 frames delivered, one resume at end, parser + ws functional suites green.

@aiolibsbot

Copy link
Copy Markdown
Contributor

@bdraco: backported manally and tested on HA. no observable regression

Useful signal, though HA traffic likely never stalls the parser — needs one read carrying more than ~4096 frames. Worth a targeted check: flood empty frames from a peer, drain slowly, watch parser._tail and RSS. That pattern grows _tail unbounded, per the inline finding.

@aiolibsbot

aiolibsbot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

@bdraco

bdraco commented Aug 18, 2026

Copy link
Copy Markdown
Member

@aiolibsbot gh_request

need to do a stranding audit, but I think there is always another resume. I think the only possible strand is if consumer doesn't call receive but that would be a bug anyways

@aiolibsbot

Copy link
Copy Markdown
Contributor

Stranding audit

Your instinct holds: there is always another resume, and the only strand in this PR's own stall/resume loop is a consumer that stops reading — which is backpressure working as designed, not a bug in the parser.

What I verified, and the one real strand I found next door.

The invariants that make the stall safe

  1. Stalled ⇒ the transport is already paused. The stall gate is queue._size > queue._limit at the frame boundary, and feed_data() pauses on exactly that predicate. Nothing can push the queue over the high-water mark without having paused first, so there is no window where a stalled reader keeps admitting socket reads into _tail.

  2. Stalled ⇒ the queue buffer is non-empty. _size is credited and debited symmetrically per message (payload + MSG_SIZE_OVERHEAD), so _size > _limit > 0 implies at least one buffered message. That is the load-bearing one: a stall can never be entered in a state where no pop is left to drive the resume.

  3. Every pop makes progress. Each _read_from_buffer() drops _size by at least MSG_SIZE_OVERHEAD, and refills only happen at _size <= _limit // 2. The low-water mark is therefore always reached within a bounded number of pops, and the re-stall after a resume cannot livelock — it strictly requires the queue to be over _limit again, which requires a refill, which requires having been under _limit // 2.

  4. The transport-resume gate is correct. After a resume, _stalled_reader is None implies the loop broke on short data, i.e. _tail is an incomplete frame — so more socket data is exactly what is needed, and resume_reading() is the right next step. Conversely a non-None _stalled_reader means the tail holds complete frames, and holding the transport paused is what keeps the bound in the queue instead of migrating it into _tail. The comment in _read_from_buffer gets this right.

Probed against the pure-Python reader with a real BaseProtocol (_limit = 128, 200 empty text frames in one feed):

probe result
stall entered 2 messages buffered, _size = 256, 396 bytes in _tail, transport paused
full drain 200/200 delivered, _tail empty, _stalled_reader cleared, transport resumed
EOF while stalled 200/200 delivered from the tail, then EofStream — no truncation
reader collected while stalled queued messages delivered, then the RuntimeError from the new guard

So: no strand. The weakref + low-water design converges.

The strand that does exist — and it is not in this PR

While walking the pause/resume graph I found a hard hang in the fragment-count backpressure from #13352 (master only, unreleased). It pauses the transport in the middle of a frame:

self._payload_fragments.append(data_cstr[f_start_pos:f_end_pos])
if (self._max_fragments and len(self._payload_fragments) > self._max_fragments
        and not self.queue._protocol._reading_paused):
    self.queue._protocol.pause_reading()
break

That pause can never be lifted. The frame only completes once more data arrives, and the only resume is _read_from_buffer() — which needs a message on the queue, and the frame in flight has not produced one. Invariant 2 above is precisely what this path violates.

Reproduced end-to-end against a real server over a real socket: one 4 KiB binary frame written 2 bytes at a time (2048 reads against a cap of 1024) and the server never receives it, forever. 1024 reads works. Costs a peer ~1024 small writes per connection, and the server WebSocket path has no read timeout unless a heartbeat is configured.

Fix in #13487 — collapse the fragments into one buffer instead of pausing, which keeps the memory bound the cap was added for. It also makes test_incomplete_frame_pauses_when_fragment_limit_exceeded obsolete: that test asserts the pause happens "long before the frame could complete", which is the bug stated as an expectation.

This is worth noting for this PR too: the new _stalled_reader is None gate adds a second condition to the only resume path in the file, so anything that pauses outside the queue's own accounting now has two reasons not to come back.

Two small things in this PR

1. An errored reader leaves _stalled_reader set for good. The resume calls the public feed_data(), which returns early when self._exc is not None — before _feed_data() clears the stall. So queue._stalled_reader stays set, and the _stalled_reader is None gate suppresses resume_reading() for the life of the connection. Measured: queue drains to _size = 0 with an empty buffer, transport still paused, zero resume_reading() calls.

Benign in practice (a parser exception is fatal and the transport gets closed), but it means the stash is retained and the pause is permanent for reasons unrelated to backpressure. Clearing queue._stalled_reader on the early-return path costs one line.

2. feed_eof() can swallow the collected-reader diagnostic. feed_eof() does self._exception = None, so if EOF lands after _read_from_buffer() raised the STALLED_READER_COLLECTED RuntimeError onto the queue, the consumer sees a plain EofStream — silent truncation rather than the contract violation you deliberately surfaced. The ws_logger.warning() still fires, which is presumably why it is there, so this may be exactly the tradeoff you intended.

Nit: data, self._tail = self._tail + data, b"" copies the whole tail on every resume even though data is always b"" on that path.


🤖 Audit run by Kōan

@bdraco

bdraco commented Aug 18, 2026

Copy link
Copy Markdown
Member

stranding/stall fix in #13488

@aiolibsbot

Copy link
Copy Markdown
Contributor

@bdraco: stranding/stall fix in #13488

Confirmed #13488 orthogonal to this PR. Mid-frame pause lives in _payload_fragments handling; this PR only stalls at frame boundaries, where a queued message always exists to drive resume.

One interaction worth noting: this PR adds _stalled_reader is None to the sole resume path in _read_from_buffer(). Any pause taken outside the queue's own accounting now has two conditions blocking its resume, not one.

Re-measured this HEAD with the gate: 200000 frames delivered, 5 reads, max tail 0.26 MB, 2.4 s. Prior blocker resolved.

@aiolibsbot

aiolibsbot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

@aiolibsbot

aiolibsbot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

PR Review — Fix memory consumption on empty/small messages

The two open suggestions from the last round are now one — a3d5a959 fixed both the empty-tail stall and the pxd widening. Merge-ready with a single non-blocking note carried forward plus one new diagnostics nit.

What's solid, verified on this head with the Cython extension built (cythonize + hand-compiled reader_c.so/mask.so):

  • The memory bound holds and costs almost nothing. Head-to-head probe, 200 000 empty masked TEXT frames through a transport that honours pause/resume: baseline peak_queued=10921, t=1.24s vs. PR peak_queued=4097, resumes=18, t=1.33s. 4097 is exactly _limit // MSG_SIZE_OVERHEAD — the accounting bound is the observed bound, and the ~7% throughput cost is the price of it. No quadratic blowup from the re-slicing, because the low-water batching amortises it.
  • Suggestion API changed to create_server #1 is genuinely fixed, not papered over. start_pos < data_len now gates the arm. Probe at _limit = 2048 with 17 empty frames: _size > _limit, transport paused, _stalled_reader is None, and two pops resume at 1024 < _size < 2048 — the documented _size < _limit threshold, not the low-water one. test_read_ending_on_frame_boundary_does_not_stall pins exactly that.
  • Suggestion The WSGI applications which are generators can now use "yield" #3 is fixed. cdef readonly on _size/_limit/_stalled_reader gives the tests read access without adding Python-level setters, and it compiles and passes under Cython 3.2.9.
  • No wedge under any state I could construct. The arm requires _size > _limit, which implies a non-empty buffer, which implies a pop is always available to drive the resume; and connection_lost on the client resets _reading_paused, while on the server force_close() nulls the transport so the late resume_reading() is a harmless no-op.
  • Tests: 317 passed / 12 skipped across test_websocket_parser.py, test_client_ws_functional.py, test_web_websocket_functional.py, test_web_websocket.py, test_client_ws.py with the C extension loaded, and 221 passed in pure-Python mode. black and flake8 clean. breaking and bugfix are both valid towncrier types with correct :user: attribution, and THREAT_MODEL.md is updated as AGENTS.md requires.
  • The breaking note's scope is proportionateWebSocketReader and set_parser() appear nowhere under docs/, so this is a private-API contract change, correctly documented rather than over-documented.

What's worth a look (neither blocks merge):

  • The drain-driven reader.feed_data(b"") still discards the (eof, tail) error flag — carried from last round. I re-confirmed it cannot wedge the transport, so the residue is a poisoned parser left installed at the protocol layer when a peer bursts a bad trailing frame and then goes silent.

  • The _parser = None release sites leave queue._stalled_reader armed, so a narrow ordering (heartbeat failure + already-run connection_lost + a pop crossing low-water) can log a caller-contract WARNING for a release aiohttp itself made.

  • Suggestion: drain-driven resume discards feed_data()'s (eof, tail) error flag (reader_py.py:153)

  • Suggestion: parser-release sites leave the queue's stall marker armed, so an in-tree release can be logged as third-party set_parser() misuse (web_ws.py:247 and four siblings)


✅ Resolved since last review (1)

Previously-flagged issues verified fixed
  • aiohttp/_websocket/reader_py.py:396 Stall marker is set even when the tail is empty, silently moving the resume threshold to _limit // 2 for all traffic


Checklist

  • Queue accounting is symmetric (no unsigned-int wrap)
  • Parser stalls only at frame boundaries, or mid-frame recoverably via _tail
  • Memory bound holds under a pause/resume-honouring transport (measured)
  • Empty-tail reads no longer arm the stall — resume threshold unchanged for ordinary backpressure
  • No wedge: transport is always eventually resumed
  • Weakref contract has a strong owner on every in-tree path
  • Parser released only where the response can no longer be drained
  • Stall marker cleared alongside the parser it points at
  • Cython pxd does not widen the public API beyond what is needed
  • Extension builds and behaves identically (317 tests with reader_c loaded)
  • New behaviour covered by tests (pure-Python + functional)
  • Changelog fragments use valid towncrier types and correct attribution
  • THREAT_MODEL.md updated for the parser/backpressure change
  • No docs/ update owed (WebSocketReader/set_parser are undocumented internals)
  • No scope creep beyond reader, its two owners, tests, changelog, threat model
ℹ️ Triage summary

1 pre-existing finding(s) on unchanged code suppressed (freeze).


Silent Failure Analysis

🟡 **1. MEDIUM** — misattributed error / silently discarded buffered data
aiohttp/client_ws.py:219-223

Risk: Dropping the only strong reference to a stalled parser here discards the frames parked in its tail and leaves the queue with no recorded cause, so the next drain logs WebSocketReader was garbage collected while stalled; callers of set_parser() must hold a strong reference and raises that RuntimeError — blaming application code for a release aiohttp performed, and hiding the real ServerTimeoutError; the stall is precisely what keeps the transport paused so the peer's PONG is never read, making this pairing likely rather than exotic.

self._set_closed()
# close() is never reached after this; release the parser here.
self._parser = None
self._close_code = WSCloseCode.ABNORMAL_CLOSURE
self._exception = exc
self._response.close()

Fix: Record the real cause on the queue before releasing the parser (set_exception(self._reader, exc)), or clear self._reader._stalled_reader when the library intentionally abandons the parser, so the contract error is reserved for actual caller violations.

🟡 **2. MEDIUM** — misattributed error / silently discarded buffered data
aiohttp/web_ws.py:243-247

Risk: Same as the client side: the heartbeat path releases the parser while a stash may be outstanding and, unlike _cancel(), never calls set_exception() on self._reader, so the frames stashed in the parser tail are lost and any later drain surfaces the caller-contract RuntimeError instead of the pong timeout that actually killed the connection.

self._set_closed()
# close() is never reached after this; release the parser here.
self._parser = None
self._set_code_close_transport(WSCloseCode.ABNORMAL_CLOSURE)
self._exception = exc

Fix: Mirror _cancel() and call set_exception(self._reader, exc) before clearing self._parser, so the recorded cause wins over the contract-violation fallback in _read_from_buffer().


Automated review by Kōan (Claude) HEAD=a3d5a95 17 min 24s

@bdraco bdraco left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seemingly endless edge cases the bot kept finding now addressed (and yes they were legit)

manually backported and retested on HA install. All good.

@Dreamsorcerer
Dreamsorcerer merged commit 5e54037 into master Aug 24, 2026
53 checks passed
@Dreamsorcerer
Dreamsorcerer deleted the fix-ws-queue branch August 24, 2026 23:43
@patchback

patchback Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Backport to 3.15: 💔 cherry-picking failed — conflicts found

❌ Failed to cleanly apply 5e54037 on top of patchback/backports/3.15/5e54037933fa8422be5fa8a2002d118ed2d4e5e0/pr-13393

Backporting merged PR #13393 into master

  1. Ensure you have a local repo clone of your fork. Unless you cloned it
    from the upstream, this would be your origin remote.
  2. Make sure you have an upstream repo added as a remote too. In these
    instructions you'll refer to it by the name upstream. If you don't
    have it, here's how you can add it:
    $ git remote add upstream https://github.com/aio-libs/aiohttp.git
  3. Ensure you have the latest copy of upstream and prepare a branch
    that will hold the backported code:
    $ git fetch upstream
    $ git checkout -b patchback/backports/3.15/5e54037933fa8422be5fa8a2002d118ed2d4e5e0/pr-13393 upstream/3.15
  4. Now, cherry-pick PR Fix memory consumption on empty/small messages #13393 contents into that branch:
    $ git cherry-pick -x 5e54037933fa8422be5fa8a2002d118ed2d4e5e0
    If it'll yell at you with something like fatal: Commit 5e54037933fa8422be5fa8a2002d118ed2d4e5e0 is a merge but no -m option was given., add -m 1 as follows instead:
    $ git cherry-pick -m1 -x 5e54037933fa8422be5fa8a2002d118ed2d4e5e0
  5. At this point, you'll probably encounter some merge conflicts. You must
    resolve them in to preserve the patch from PR Fix memory consumption on empty/small messages #13393 as close to the
    original as possible.
  6. Push this branch to your fork on GitHub:
    $ git push origin patchback/backports/3.15/5e54037933fa8422be5fa8a2002d118ed2d4e5e0/pr-13393
  7. Create a PR, ensure that the CI is green. If it's not — update it so that
    the tests and any other checks pass. This is it!
    Now relax and wait for the maintainers to process your pull request
    when they have some cycles to do reviews. Don't worry — they'll tell you if
    any improvements are necessary when the time comes!

🤖 @patchback
I'm built with octomachinery and
my source is open — https://github.com/sanitizers/patchback-github-app.

@patchback

patchback Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Backport to 3.14: 💔 cherry-picking failed — conflicts found

❌ Failed to cleanly apply 5e54037 on top of patchback/backports/3.14/5e54037933fa8422be5fa8a2002d118ed2d4e5e0/pr-13393

Backporting merged PR #13393 into master

  1. Ensure you have a local repo clone of your fork. Unless you cloned it
    from the upstream, this would be your origin remote.
  2. Make sure you have an upstream repo added as a remote too. In these
    instructions you'll refer to it by the name upstream. If you don't
    have it, here's how you can add it:
    $ git remote add upstream https://github.com/aio-libs/aiohttp.git
  3. Ensure you have the latest copy of upstream and prepare a branch
    that will hold the backported code:
    $ git fetch upstream
    $ git checkout -b patchback/backports/3.14/5e54037933fa8422be5fa8a2002d118ed2d4e5e0/pr-13393 upstream/3.14
  4. Now, cherry-pick PR Fix memory consumption on empty/small messages #13393 contents into that branch:
    $ git cherry-pick -x 5e54037933fa8422be5fa8a2002d118ed2d4e5e0
    If it'll yell at you with something like fatal: Commit 5e54037933fa8422be5fa8a2002d118ed2d4e5e0 is a merge but no -m option was given., add -m 1 as follows instead:
    $ git cherry-pick -m1 -x 5e54037933fa8422be5fa8a2002d118ed2d4e5e0
  5. At this point, you'll probably encounter some merge conflicts. You must
    resolve them in to preserve the patch from PR Fix memory consumption on empty/small messages #13393 as close to the
    original as possible.
  6. Push this branch to your fork on GitHub:
    $ git push origin patchback/backports/3.14/5e54037933fa8422be5fa8a2002d118ed2d4e5e0/pr-13393
  7. Create a PR, ensure that the CI is green. If it's not — update it so that
    the tests and any other checks pass. This is it!
    Now relax and wait for the maintainers to process your pull request
    when they have some cycles to do reviews. Don't worry — they'll tell you if
    any improvements are necessary when the time comes!

🤖 @patchback
I'm built with octomachinery and
my source is open — https://github.com/sanitizers/patchback-github-app.

Dreamsorcerer added a commit that referenced this pull request Aug 25, 2026
(cherry picked from commit 5e54037)

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-3.14 Trigger automatic backporting to the 3.14 release branch by Patchback robot backport-3.15 Trigger automatic backporting to the 3.15 release branch by Patchback robot bot:chronographer:provided There is a change note present in this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants