Merged
Conversation
15ca6d2 to
cbe3cc3
Compare
pjfanning
reviewed
Apr 20, 2026
pjfanning
reviewed
Apr 20, 2026
He-Pin
added a commit
that referenced
this pull request
Apr 25, 2026
Motivation:
Pekko Stream's TLS implementation historically relied on a legacy actor-based
approach (TLSActor/FanoutProcessor). This introduces a new GraphStage-based
implementation (TlsGraphStage) that directly manages SSLEngine state, ByteBuffer
choreography, and clean up/teardown semantics without actor overhead.
The new path is internal-only in this PR—no routing changes yet. It is purely
for validating correctness and compatibility before defaulting to it in PR2.
Modification:
1. TlsGraphStage (485 lines):
- Direct SSLEngine state machine (wrap/unwrap sequencing per HandshakeStatus)
- Warmup timer mechanism: defers first pump to next scheduler tick, giving
upstream failures a window to propagate before TLS handshake bytes emit
- Synchronous delegated-task draining (NEED_TASK)
- Careful ByteBuffer.copyToBuffer/drop/putBack choreography
- Session corking (user data buffered until handshake FINISHED)
- Renegotiation support (re-cork until new session FINISHED)
- Close mode handling: EagerClose vs IgnoreComplete semantics
- Loop guards for NEED_WRAP with zero output (JDK edge case mitigation)
- Forced asyncBoundary via Attributes: ensures independent scheduling island
(SSLEngine must not fuse with other stages per performance testing)
2. TlsGraphStageIsolatedSpec (7 tests):
- Startup failure propagation: cipherIn/plainIn early abort
- Empty/non-empty ByteString alternation
- Large payload fragmentation (64KiB+1)
- Completion semantics: EagerClose/IgnoreComplete both sides
- TLS 1.2 renegotiation (NegotiateNewSession command)
Result:
✅ 7/7 isolated tests pass
✅ Compilation: stream/test:compile, stream-tests/test:compile
✅ Binary compatibility: stream/mimaReportBinaryIssues (new internal class)
✅ Format: scalafmtAll, javafmtAll (JDK 21)
✅ Headers: ASF Apache 2.0 license, correct Pekko import pattern
References:
Upstream Issue: #2860
Related PR: #2878 (routing layer, deferred to PR2)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cbe3cc3 to
b07587b
Compare
He-Pin
added a commit
that referenced
this pull request
Apr 25, 2026
Motivation:
Pekko Stream's TLS implementation historically relied on a legacy actor-based
approach (TLSActor/FanoutProcessor). This introduces a new GraphStage-based
implementation (TlsGraphStage) that directly manages SSLEngine state, ByteBuffer
choreography, and clean up/teardown semantics without actor overhead.
The new path is internal-only in this PR—no routing changes yet. It is purely
for validating correctness and compatibility before defaulting to it in PR2.
Modification:
1. TlsGraphStage (485 lines):
- Direct SSLEngine state machine (wrap/unwrap sequencing per HandshakeStatus)
- Warmup timer mechanism: defers first pump to next scheduler tick, giving
upstream failures a window to propagate before TLS handshake bytes emit
- Synchronous delegated-task draining (NEED_TASK)
- Careful ByteBuffer.copyToBuffer/drop/putBack choreography
- Session corking (user data buffered until handshake FINISHED)
- Renegotiation support (re-cork until new session FINISHED)
- Close mode handling: EagerClose vs IgnoreComplete semantics
- Loop guards for NEED_WRAP with zero output (JDK edge case mitigation)
- Forced asyncBoundary via Attributes: ensures independent scheduling island
(SSLEngine must not fuse with other stages per performance testing)
2. TlsGraphStageIsolatedSpec (7 tests):
- Startup failure propagation: cipherIn/plainIn early abort
- Empty/non-empty ByteString alternation
- Large payload fragmentation (64KiB+1)
- Completion semantics: EagerClose/IgnoreComplete both sides
- TLS 1.2 renegotiation (NegotiateNewSession command)
Result:
✅ 7/7 isolated tests pass
✅ Compilation: stream/test:compile, stream-tests/test:compile
✅ Binary compatibility: stream/mimaReportBinaryIssues (new internal class)
✅ Format: scalafmtAll, javafmtAll (JDK 21)
✅ Headers: ASF Apache 2.0 license, correct Pekko import pattern
References:
Upstream Issue: #2860
Related PR: #2878 (routing layer, deferred to PR2)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
4166c15 to
02f3cb3
Compare
He-Pin
added a commit
that referenced
this pull request
Apr 25, 2026
Motivation: The stream TLS path still depends on the legacy actor/FanoutProcessor infrastructure. A GraphStage engine is needed for the stream internals while preserving the existing Pekko TLSActor SSLEngine state machine semantics without changing the legacy actor implementation. Modification: - Add TlsGraphStage as a GraphStage adapter for the existing Pekko TLS pump phases. - Reuse the Pekko TCP direct BufferPool for TLS transport buffers and allocate application buffers from SSLEngine session sizes. - Add a pekko.stream.materializer.tls.engine selector with legacy-actor as the default and graph-stage as the opt-in engine. - Run the shared TLS regression matrix against both legacy and GraphStage paths and add focused GraphStage edge-case coverage. - Add TLS JMH benchmarks for cold handshake and warm round-trip scenarios. Result: The GraphStage path is opt-in, the legacy TLSActor remains untouched, and TLS close, truncation, renegotiation, failure-alert, and TLS 1.3 behavior are covered by regression tests. Tests: - stream / scalafmtCheck - stream-tests / scalafmtCheck - bench-jmh / scalafmtCheck - stream / Test / compile - stream-tests / Test / testOnly org.apache.pekko.stream.io.TlsSpec org.apache.pekko.stream.io.TlsGraphStageSpec org.apache.pekko.stream.io.TlsGraphStageEdgeCasesSpec org.apache.pekko.stream.io.TlsGraphStageIsolatedSpec - git diff --check - red-flag rg scan for prior suspicious Akka-port markers References: - #2878 - #2860
02f3cb3 to
14d49be
Compare
He-Pin
added a commit
that referenced
this pull request
Apr 25, 2026
Motivation: The stream TLS path still depends on the legacy actor/FanoutProcessor infrastructure. A GraphStage engine is needed for the stream internals while preserving the existing Pekko TLSActor SSLEngine state machine semantics without changing the legacy actor implementation. Modification: - Add TlsGraphStage as a GraphStage adapter for the existing Pekko TLS pump phases. - Reuse the Pekko TCP direct BufferPool for TLS transport buffers and allocate application buffers from SSLEngine session sizes. - Add a pekko.stream.materializer.tls.engine selector with legacy-actor as the default and graph-stage as the opt-in engine. - Run the shared TLS regression matrix against both legacy and GraphStage paths and add focused GraphStage edge-case coverage. - Add TLS JMH benchmarks for cold handshake and warm round-trip scenarios. Result: The GraphStage path is opt-in, the legacy TLSActor remains untouched, and TLS close, truncation, renegotiation, failure-alert, and TLS 1.3 behavior are covered by regression tests. Tests: - stream / scalafmtCheck - stream-tests / scalafmtCheck - bench-jmh / scalafmtCheck - stream / Test / compile - stream-tests / Test / testOnly org.apache.pekko.stream.io.TlsSpec org.apache.pekko.stream.io.TlsGraphStageSpec org.apache.pekko.stream.io.TlsGraphStageEdgeCasesSpec org.apache.pekko.stream.io.TlsGraphStageIsolatedSpec - git diff --check - red-flag rg scan for prior suspicious port markers References: - #2878 - #2860
14d49be to
36c4a22
Compare
He-Pin
added a commit
that referenced
this pull request
Apr 26, 2026
Motivation: The stream TLS path still depends on the legacy actor/FanoutProcessor infrastructure. A GraphStage engine is needed for the stream internals while preserving the existing Pekko TLSActor SSLEngine state machine semantics without changing the legacy actor implementation. Modification: - Add TlsGraphStage as a GraphStage adapter for the existing Pekko TLS pump phases. - Reuse the Pekko TCP direct BufferPool for TLS transport buffers and allocate application buffers from SSLEngine session sizes. - Add a pekko.stream.materializer.tls.engine selector with legacy-actor as the default and graph-stage as the opt-in engine. - Run the shared TLS regression matrix against both legacy and GraphStage paths and add focused GraphStage edge-case coverage. - Add TLS JMH benchmarks for cold handshake and warm round-trip scenarios. Result: The GraphStage path is opt-in, the legacy TLSActor remains untouched, and TLS close, truncation, renegotiation, failure-alert, and TLS 1.3 behavior are covered by regression tests. Tests: - stream / scalafmtCheck - stream-tests / scalafmtCheck - bench-jmh / scalafmtCheck - stream / Test / compile - stream-tests / Test / testOnly org.apache.pekko.stream.io.TlsSpec org.apache.pekko.stream.io.TlsGraphStageSpec org.apache.pekko.stream.io.TlsGraphStageEdgeCasesSpec org.apache.pekko.stream.io.TlsGraphStageIsolatedSpec - git diff --check - red-flag rg scan for prior suspicious port markers References: - #2878 - #2860
36c4a22 to
a6750a2
Compare
He-Pin
added a commit
that referenced
this pull request
Apr 26, 2026
Motivation: The stream TLS path still depends on the legacy actor/FanoutProcessor infrastructure. A GraphStage engine is needed for the stream internals while preserving the existing Pekko TLSActor SSLEngine state machine semantics without changing the legacy actor implementation. Modification: - Add TlsGraphStage as a GraphStage adapter for the existing Pekko TLS pump phases. - Reuse the Pekko TCP direct BufferPool for TLS transport buffers and allocate application buffers from SSLEngine session sizes. - Add a pekko.stream.materializer.tls.engine selector with legacy-actor as the default and graph-stage as the opt-in engine. - Run the shared TLS regression matrix against both legacy and GraphStage paths and add focused GraphStage edge-case coverage. - Add TLS JMH benchmarks for cold handshake and warm round-trip scenarios. Result: The GraphStage path is opt-in, the legacy TLSActor remains untouched, and TLS close, truncation, renegotiation, failure-alert, and TLS 1.3 behavior are covered by regression tests. Tests: - stream / scalafmtCheck - stream-tests / scalafmtCheck - bench-jmh / scalafmtCheck - stream / Test / compile - stream-tests / Test / testOnly org.apache.pekko.stream.io.TlsSpec org.apache.pekko.stream.io.TlsGraphStageSpec org.apache.pekko.stream.io.TlsGraphStageEdgeCasesSpec org.apache.pekko.stream.io.TlsGraphStageIsolatedSpec - git diff --check - red-flag rg scan for prior suspicious port markers References: - #2878 - #2860
a6750a2 to
665e287
Compare
Motivation: The stream TLS path still depends on the legacy actor/FanoutProcessor infrastructure. A GraphStage engine is needed for the stream internals while preserving the existing Pekko TLSActor SSLEngine state machine semantics without changing the legacy actor implementation. Modification: - Add TlsGraphStage as a GraphStage adapter for the existing Pekko TLS pump phases. - Reuse the Pekko TCP direct BufferPool for TLS transport buffers and allocate application buffers from SSLEngine session sizes. - Add a pekko.stream.materializer.tls.engine selector with legacy-actor as the default and graph-stage as the opt-in engine. - Run the shared TLS regression matrix against both legacy and GraphStage paths and add focused GraphStage edge-case coverage. - Add TLS JMH benchmarks for cold handshake and warm round-trip scenarios. Result: The GraphStage path is opt-in, the legacy TLSActor remains untouched, and TLS close, truncation, renegotiation, failure-alert, and TLS 1.3 behavior are covered by regression tests. Tests: - stream / scalafmtCheck - stream-tests / scalafmtCheck - bench-jmh / scalafmtCheck - stream / Test / compile - stream-tests / Test / testOnly org.apache.pekko.stream.io.TlsSpec org.apache.pekko.stream.io.TlsGraphStageSpec org.apache.pekko.stream.io.TlsGraphStageEdgeCasesSpec org.apache.pekko.stream.io.TlsGraphStageIsolatedSpec - git diff --check - red-flag rg scan for prior suspicious port markers References: - #2878 - #2860
665e287 to
bb11b44
Compare
Member
Author
|
I run it with GPT5.5 xhigh, and seems ok to me now. |
Member
Author
|
Huge thanks @mingyang91 for gpt 5.5 |
pjfanning
previously approved these changes
May 2, 2026
Member
pjfanning
left a comment
There was a problem hiding this comment.
lgtm - I guess if this is optional and non-default then it seems like rolling this out is less risky
Contributor
There was a problem hiding this comment.
Pull request overview
Adds an opt-in GraphStage-based TLS engine to Pekko Streams while retaining the existing actor-backed TLS path as the default, plus shared helper extraction and expanded test/benchmark coverage.
Changes:
- Introduce
TlsGraphStage(bidi TLS engine) and sharedTlsEngineHelpersutilities. - Add global TLS engine selection via
pekko.stream.materializer.tls.engine(legacy-actordefault,graph-stageopt-in). - Extend TLS coverage with GraphStage-focused specs and a JMH benchmark fixture; add internal
ByteString.copyToBuffer(..., offset)to reduce copying overhead.
Reviewed changes
Copilot reviewed 11 out of 13 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| stream/src/main/scala/org/apache/pekko/stream/scaladsl/TLS.scala | Select TLS engine (legacy vs graph-stage) and apply GraphStage attributes when enabled. |
| stream/src/main/scala/org/apache/pekko/stream/impl/io/TlsGraphStage.scala | New GraphStage TLS engine implementing the TLS pump/state-machine and batching/flush behavior. |
| stream/src/main/scala/org/apache/pekko/stream/impl/io/TlsEngineHelpers.scala | New shared ByteBuffer/SSLEngine helpers used by both implementations. |
| stream/src/main/scala/org/apache/pekko/stream/impl/io/TLSActor.scala | Refactor to reuse shared helpers; tighten close/unwrap-related conditions. |
| stream/src/main/scala/org/apache/pekko/stream/impl/Stages.scala | Add default stage name attribute for the new TLS GraphStage. |
| stream/src/main/resources/reference.conf | Document/configure pekko.stream.materializer.tls.engine (defaulting to legacy-actor). |
| stream-tests/src/test/scala/org/apache/pekko/stream/io/TlsSpec.scala | Split into shared abstract suite and run against both engines in the same JVM. |
| stream-tests/src/test/scala/org/apache/pekko/stream/io/TlsGraphStageIsolatedSpec.scala | New isolated GraphStage tests for failures, fragmentation, renegotiation, and completion behavior. |
| stream-tests/src/test/scala/org/apache/pekko/stream/io/TlsGraphStageEdgeCasesSpec.scala | New GraphStage edge-case tests for attributes, batching/flush behavior, backpressure, and isolation. |
| bench-jmh/src/main/scala/org/apache/pekko/stream/io/TlsBenchmark.scala | New JMH benchmark comparing legacy vs GraphStage TLS engines. |
| bench-jmh/src/main/resources/truststore | Benchmark TLS truststore resource. |
| bench-jmh/src/main/resources/keystore | Benchmark TLS keystore resource. |
| actor/src/main/scala/org/apache/pekko/util/ByteString.scala | Add internal offset-based copyToBuffer to avoid slicing/allocations in hot paths. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
broadly ok with the code as is but Copilot has some questions it raised
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an opt-in GraphStage TLS engine for Pekko Streams while keeping the existing legacy actor path as the default.
Motivation
The current Streams TLS route depends on the legacy actor/FanoutProcessor infrastructure. This PR adds a GraphStage route for the stream internals, but keeps the long-standing TLSActor route as the default compatibility baseline.
The new GraphStage implementation is a migration of Pekko's existing TLS pump state machine into GraphStage lifecycle and port handling. Shared low-level SSLEngine helpers are factored out so the legacy actor and GraphStage path keep the same record-buffer preparation and delegated-task behavior.
Implementation
TlsGraphStage, aBidiShape[SslTlsOutbound, ByteString, ByteString, SslTlsInbound]GraphStage.BufferPoolfor TLS transport packet buffers viaTcp(materializer.system).bufferPool.SSLSessionpacket/application buffer sizes instead of fixed magic constants.TLS.apply.pekko.stream.materializer.tls.enginewith valid values:legacy-actor(default)graph-stage(opt-in)Tests
TlsSpeccontinues to exercise the legacy actor path, including TLSv1.3.TlsGraphStageSpecruns the same shared TLS regression matrix against the GraphStage path in the same JVM.TlsGraphStageEdgeCasesSpeccovers production async/input-buffer attributes, async-boundary routing, fragmented cipher input, small-write batching, no-upstream-completion flushes for small and large plaintext writes, plainOut backpressure, per-materializationSSLEngineisolation, and empty-source completion.TlsGraphStageIsolatedSpeccovers early failures, empty/non-empty alternation, fragmented large payloads, eager close, immediate completion, and TLS 1.2 renegotiation.TlsBenchmarkremains as a JMH fixture for future comparison work; this PR does not switch the default engine based on benchmark numbers.Benchmarks
Local JMH run on macOS 26.3 with Java 21.0.5 HotSpot. Throughput is
ops/ms; higher is better.Commands:
bench-jmh/Jmh/run -wi 2 -i 3 -f1 -t1 -p implementation=legacy,graphstage -p payloadSize=64,256,1024,4096,65536 .*TlsBenchmark.warmRoundTrip.*bench-jmh/Jmh/run -wi 1 -i 2 -f1 -t1 -p implementation=legacy,graphstage -p payloadSize=256,4096 .*TlsBenchmark.coldHandshake.*Warm round-trip:
Cold handshake:
Validation
actor / scalafmtstream / scalafmtstream-tests / scalafmtbench-jmh / scalafmtactor / scalafmtCheckstream / scalafmtCheckstream-tests / scalafmtCheckbench-jmh / scalafmtCheckstream / Test / compilestream-tests / Test / testOnly org.apache.pekko.stream.io.TlsGraphStageEdgeCasesSpecTlsSpec,TlsGraphStageSpec,TlsGraphStageEdgeCasesSpec,TlsGraphStageIsolatedSpecTlsSpec,TlsGraphStageSpec,TlsGraphStageEdgeCasesSpec,TlsGraphStageIsolatedSpecTlsGraphStageSpec,TlsGraphStageEdgeCasesSpec,TlsGraphStageIsolatedSpecgit diff --checkRelated