Skip to content

feat(stream): add TLS GraphStage engine#2878

Merged
He-Pin merged 2 commits intomainfrom
issue-2860-tls-graphstage-path
May 8, 2026
Merged

feat(stream): add TLS GraphStage engine#2878
He-Pin merged 2 commits intomainfrom
issue-2860-tls-graphstage-path

Conversation

@He-Pin
Copy link
Copy Markdown
Member

@He-Pin He-Pin commented Apr 20, 2026

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

  • Add TlsGraphStage, a BidiShape[SslTlsOutbound, ByteString, ByteString, SslTlsInbound] GraphStage.
  • Preserve the existing TLS pump phases in GraphStage form: bidirectional transfer, outbound flushing, close-awaiting, outbound-closed, and inbound-closed.
  • Reuse Pekko's TCP direct BufferPool for TLS transport packet buffers via Tcp(materializer.system).bufferPool.
  • Size application buffers from the active SSLSession packet/application buffer sizes instead of fixed magic constants.
  • Keep the legacy actor path as the default; legacy actor changes are limited to shared helper use and TLS 1.3 close/unwrap correctness.
  • Add two-level batching for the GraphStage path:
    • small plaintext writes are batched before wrapping;
    • transport writes are consolidated with a size guard so large TLS records flush immediately.
  • Apply an async boundary and input buffer around the GraphStage engine from TLS.apply.
  • Add pekko.stream.materializer.tls.engine with valid values:
    • legacy-actor (default)
    • graph-stage (opt-in)

Tests

  • TlsSpec continues to exercise the legacy actor path, including TLSv1.3.
  • TlsGraphStageSpec runs the same shared TLS regression matrix against the GraphStage path in the same JVM.
  • TlsGraphStageEdgeCasesSpec covers 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-materialization SSLEngine isolation, and empty-source completion.
  • TlsGraphStageIsolatedSpec covers early failures, empty/non-empty alternation, fragmented large payloads, eager close, immediate completion, and TLS 1.2 renegotiation.
  • TlsBenchmark remains 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:

payload legacy graph-stage delta
64 B 248.011 616.426 +148.5%
256 B 195.364 466.311 +138.7%
1 KiB 230.728 314.497 +36.3%
4 KiB 130.534 158.900 +21.7%
64 KiB 17.021 16.929 -0.5%

Cold handshake:

payload legacy graph-stage delta
256 B 0.672 0.700 +4.2%
4 KiB 0.720 0.696 -3.3%

Validation

  • actor / scalafmt
  • stream / scalafmt
  • stream-tests / scalafmt
  • bench-jmh / scalafmt
  • actor / scalafmtCheck
  • stream / scalafmtCheck
  • stream-tests / scalafmtCheck
  • bench-jmh / scalafmtCheck
  • stream / Test / compile
  • JDK 21 latest targeted run after the directional tests: stream-tests / Test / testOnly org.apache.pekko.stream.io.TlsGraphStageEdgeCasesSpec
    • 13 tests succeeded, 0 failed
  • JDK 21 full TLS regression: TlsSpec, TlsGraphStageSpec, TlsGraphStageEdgeCasesSpec, TlsGraphStageIsolatedSpec
    • 239 tests succeeded, 0 failed
  • JDK 17 full TLS regression: TlsSpec, TlsGraphStageSpec, TlsGraphStageEdgeCasesSpec, TlsGraphStageIsolatedSpec
    • 239 tests succeeded, 0 failed
  • JDK 25 GraphStage regression: TlsGraphStageSpec, TlsGraphStageEdgeCasesSpec, TlsGraphStageIsolatedSpec
    • 128 tests succeeded, 0 failed
  • git diff --check
  • Red-flag scan for prior suspicious port markers: no matches in the new/changed TLS GraphStage, config, test, or benchmark files

Related

@He-Pin He-Pin added this to the 2.0.0-M2 milestone Apr 20, 2026
@He-Pin He-Pin added the t:stream Pekko Streams label Apr 20, 2026
@He-Pin He-Pin changed the title feat(stream): add clean-room GraphStage TLS implementation (TlsGraphStage) feat(stream): add GraphStage TLS implementation (TlsGraphStage) Apr 20, 2026
@He-Pin He-Pin force-pushed the issue-2860-tls-graphstage-path branch from 15ca6d2 to cbe3cc3 Compare April 20, 2026 11:29
Comment thread stream/src/main/resources/reference.conf Outdated
Comment thread stream/src/main/scala/org/apache/pekko/stream/impl/io/TlsGraphStage.scala Outdated
@He-Pin He-Pin modified the milestones: 2.0.0-M2, 2.0.0-M3 Apr 21, 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>
@He-Pin He-Pin force-pushed the issue-2860-tls-graphstage-path branch from cbe3cc3 to b07587b Compare April 25, 2026 08:14
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>
@He-Pin He-Pin force-pushed the issue-2860-tls-graphstage-path branch 3 times, most recently from 4166c15 to 02f3cb3 Compare April 25, 2026 17:42
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
@He-Pin He-Pin force-pushed the issue-2860-tls-graphstage-path branch from 02f3cb3 to 14d49be Compare April 25, 2026 17:59
@He-Pin He-Pin changed the title feat(stream): add GraphStage TLS implementation (TlsGraphStage) feat(stream): add TLS GraphStage engine Apr 25, 2026
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
@He-Pin He-Pin force-pushed the issue-2860-tls-graphstage-path branch from 14d49be to 36c4a22 Compare April 25, 2026 18:00
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
@He-Pin He-Pin force-pushed the issue-2860-tls-graphstage-path branch from 36c4a22 to a6750a2 Compare April 26, 2026 05:53
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
@He-Pin He-Pin force-pushed the issue-2860-tls-graphstage-path branch from a6750a2 to 665e287 Compare April 26, 2026 05:56
@He-Pin He-Pin marked this pull request as ready for review April 26, 2026 06:00
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
@He-Pin He-Pin force-pushed the issue-2860-tls-graphstage-path branch from 665e287 to bb11b44 Compare April 26, 2026 06:02
@He-Pin He-Pin modified the milestones: 2.0.0-M3, 2.0.0-M2 Apr 26, 2026
@He-Pin He-Pin requested a review from pjfanning April 26, 2026 06:05
@He-Pin
Copy link
Copy Markdown
Member Author

He-Pin commented Apr 26, 2026

I run it with GPT5.5 xhigh, and seems ok to me now.

@He-Pin
Copy link
Copy Markdown
Member Author

He-Pin commented Apr 26, 2026

Huge thanks @mingyang91 for gpt 5.5

pjfanning
pjfanning previously approved these changes May 2, 2026
Copy link
Copy Markdown
Member

@pjfanning pjfanning left a comment

Choose a reason for hiding this comment

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

lgtm - I guess if this is optional and non-default then it seems like rolling this out is less risky

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 shared TlsEngineHelpers utilities.
  • Add global TLS engine selection via pekko.stream.materializer.tls.engine (legacy-actor default, graph-stage opt-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.

Comment thread stream/src/main/scala/org/apache/pekko/stream/impl/io/TlsGraphStage.scala Outdated
Comment thread stream/src/main/scala/org/apache/pekko/stream/impl/io/TlsGraphStage.scala Outdated
Comment thread stream/src/main/scala/org/apache/pekko/stream/impl/io/TlsGraphStage.scala Outdated
Comment thread stream/src/main/scala/org/apache/pekko/stream/scaladsl/TLS.scala Outdated
Comment thread stream/src/main/scala/org/apache/pekko/stream/scaladsl/TLS.scala Outdated
@pjfanning pjfanning dismissed their stale review May 4, 2026 16:55

broadly ok with the code as is but Copilot has some questions it raised

@He-Pin He-Pin requested a review from pjfanning May 8, 2026 09:05
Copy link
Copy Markdown
Member

@pjfanning pjfanning left a comment

Choose a reason for hiding this comment

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

lgtm

@He-Pin He-Pin merged commit 5a4f236 into main May 8, 2026
9 checks passed
@He-Pin He-Pin deleted the issue-2860-tls-graphstage-path branch May 8, 2026 11:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

t:stream Pekko Streams

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants