Skip to content

Conversation

@Aaronontheweb
Copy link
Member

Summary

This PR backports #7905 from dev to v1.5.

This fixes issue #7895 by making both ISourceRef<T>.Source and ISinkRef<T>.Sink properties idempotent using Lazy<T>. Previously, these properties created new stage instances on every access, causing race conditions where multiple instances would compete for the same handshake, leading to intermittent subscription timeouts.

Changes

Files Modified:

  1. SourceRefImpl.cs - Made .Source property idempotent using Lazy<Source<T, NotUsed>>
  2. SinkRefImpl.cs - Made .Sink property idempotent using Lazy<Sink<T, NotUsed>>
  3. StreamRefsSpec.cs - Added 2 reproduction tests

Implementation:

// Before (created new instance every time)
public Source<T, NotUsed> Source =>
    Dsl.Source.FromGraph(new SourceRefStageImpl<T>(InitialPartnerRef))
        .MapMaterializedValue(_ => NotUsed.Instance);

// After (cached with Lazy<T>)
private readonly Lazy<Source<T, NotUsed>> _source;

public SourceRefImpl(IActorRef initialPartnerRef) : base(initialPartnerRef)
{
    _source = new Lazy<Source<T, NotUsed>>(() =>
        Dsl.Source.FromGraph(new SourceRefStageImpl<T>(InitialPartnerRef))
            .MapMaterializedValue(_ => NotUsed.Instance));
}

public Source<T, NotUsed> Source => _source.Value;

Tests Added

1. SourceRef_Source_property_should_be_idempotent_issue_7895

  • Verifies that multiple .Source property accesses return the same instance
  • Before fix: Failed 25/25 times (100% failure rate)
  • After fix: Passed 10/10 times (100% success rate)

2. SourceRef_multiple_materializations_cause_timeout_issue_7895

  • Demonstrates that the fix eliminates race conditions from multiple property accesses
  • Before fix: Failed consistently with timeouts
  • After fix: Passes consistently

Impact

What This Fixes:

  • ✅ Eliminates race conditions from accidental property accesses
    • Debugger hovering over variables
    • Logging frameworks inspecting properties
    • Serialization/framework reflection
    • IDE property viewers
  • ✅ Prevents subscription timeouts caused by multiple SourceRefStageImpl instances
  • ✅ Fixes intermittent ~30% failure rate in production workloads (see issue StreamRef: Multiple .Source property accesses cause intermittent subscription timeouts #7895)
  • ✅ Thread-safe using Lazy<T> default ExecutionAndPublication mode

What Still Fails (As Designed):

  • Intentional double materialization (user error)
    • Still fails gracefully at actor protocol level via ObserveAndValidateSender
    • Second materialization gets rejected with clear error message

Test Results

  • ✅ Reproduction tests: 2/2 passed
  • ✅ Serialization test: 1/1 passed
  • ✅ Stability: 10/10 consecutive runs passed

Technical Notes

Why Lazy<T>?

  • Simpler and cleaner than double-checked locking
  • Built-in thread safety with ExecutionAndPublication mode
  • No manual lock management required
  • Well-tested by .NET framework

Serialization Compatibility:

  • Lazy<T> fields are not serialized (transient)
  • StreamRefs use surrogate pattern - only InitialPartnerRef is serialized
  • On deserialization, new SourceRefImpl is created with fresh Lazy<T>
  • Verified by existing serialization tests

Fixes #7895
Backport of #7905

…ug (akkadotnet#7895)

Added two tests to demonstrate the bug where ISourceRef<T>.Source property
creates a new SourceRefStageImpl instance on every access instead of being
idempotent:

1. SourceRef_Source_property_should_be_idempotent_issue_7895
   - Verifies that multiple .Source property accesses should return the same
     instance (currently fails - demonstrates the bug exists)
   - Tested 25 times with 100% failure rate, proving consistent reproduction

2. SourceRef_multiple_materializations_cause_timeout_issue_7895
   - Demonstrates the race condition when multiple SourceRefStageImpl instances
     try to connect to the same SinkRef
   - Shows intermittent timeouts and failures due to handshake conflicts

These tests will pass once the Source property is made idempotent by caching
the created Source instance.

Issue: akkadotnet#7895
…kadotnet#7895)

Implemented Lazy<T> to make both ISourceRef<T>.Source and ISinkRef<T>.Sink
properties idempotent. Previously, these properties created new stage
instances on every access, causing race conditions where multiple instances
would compete for the same handshake, leading to intermittent subscription
timeouts.

Changes:
- SourceRefImpl<T>: Use Lazy<Source<T, NotUsed>> for thread-safe caching
- SinkRefImpl<T>: Use Lazy<Sink<T, NotUsed>> for thread-safe caching
- Lazy<T> uses default ExecutionAndPublication mode for thread safety

Impact:
- Eliminates race conditions from accidental property accesses (debugger,
  logging, serialization, framework inspection)
- Prevents subscription timeouts caused by multiple stage instances
- Fixes intermittent ~30% failure rate in production workloads
- Double materialization (user error) still fails gracefully at actor
  protocol level via ObserveAndValidateSender

Test Results:
- Before fix: Tests failed 25/25 times (100% failure rate)
- After fix: Tests passed 10/10 times (100% success rate)

Fixes akkadotnet#7895
@Aaronontheweb Aaronontheweb added this to the 1.5.54 milestone Oct 13, 2025
@Aaronontheweb Aaronontheweb enabled auto-merge (squash) October 13, 2025 21:58
@Aaronontheweb Aaronontheweb merged commit 53e1b3d into akkadotnet:v1.5 Oct 14, 2025
9 of 11 checks passed
@Aaronontheweb Aaronontheweb deleted the fix/streamref-source-idempotent-7895-backport-v1.5 branch October 14, 2025 00:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant