He-Pin opened a new issue, #3739: URL: https://github.com/apache/pekko/issues/3739
### Motivation Recent work hardened cancellation behaviour on the plaintext-output side of the TLS operators (#3727, #3731, #3738). While auditing cancellation symmetry across the TLS stage's four teardown paths, I found a gap in the opposite direction: when the transport (ciphertext) output is cancelled while inbound records have already been received but not yet decrypted, both TLS engines discard that data silently. The other three teardown paths all have an observable contract: - transport input truncated without `close_notify`: received data is delivered and `SessionTruncated` is emitted (documented, covered by `TlsSpec`); - transport input failure: the stage fails via `failTls`, visible to the application; - plaintext output cancellation: the graph-stage engine drains accepted plaintext (#3727, #3731, #3738), documented in the stream IO guide. Only "transport output cancelled while inbound data awaits plaintext demand" ends in a clean `onComplete` with the received data dropped and no signal at all. ### Scenario Observed on `main` (2.0.0-M4+ snapshot), both `pekko.stream.materializer.tls.engine = legacy-actor` and `graph-stage`. 1. A TLS client stage with an established session. The peer sends an application data record (e.g. a final response); the client's transport input receives the ciphertext. 2. The client's plaintext output has no outstanding demand at that moment (the consumer has not requested yet). 3. The transport output is then cancelled while the read side stays healthy (e.g. a kill switch on the send path, or an application that deliberately stops sending but still expects to read the response). ### Observed - The stage completes immediately; the plaintext output receives `onComplete` with no `SessionBytes`. - The already-received record is dropped undecrypted, and no `SessionTruncated` is emitted, so the application cannot tell that data received at the record layer was thrown away. - Identical behaviour with both engines. - If plaintext demand existed before the cancellation, the data is delivered normally, so the loss is specific to "cancellation while inbound data awaits demand". ### Internal notes - Graph stage: inbound decryption is gated on plaintext-output demand (`inboundReady` requires `userOutput.isDemandAvailable` in `TlsGraphStage`). A cancelled transport output makes `outboundCompleted` (and thus `bidirectionalCompleted`) true immediately, so the pump skips every phase and reaches `pumpFinished`, which cancels the transport input queue — the inbound close path never runs, hence no `closeInbound`, no `SessionTruncated`. - Legacy actor: same demand gating in the inbound transfer state (`demandAvailableFor(UserOut)`); a cancelled transport output jumps straight to the completed phase (`TLSActor.doOutbound`). ### Expected Either of the following, plus a sentence in the stream IO guide (the current text only covers the plaintext-output-cancellation/drain direction): 1. Deliver already-received inbound records to the plaintext output as demand arrives before completing — the read side is independent of the cancelled write side; or 2. At minimum, run the inbound close path so `SessionTruncated` is emitted before completion, making the loss observable. Option 2 is a small, low-risk change (signal only, no completion-timing change). Option 1 mirrors the outbound drain philosophy but needs a decision on how long the stage may wait for plaintext demand, similar to the trade-off already accepted and documented for the outbound drain. ### References Refs #3727, #3731, #3738 -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
