@@ -49,38 +49,67 @@ public void enumerating_while_registrations_land_neither_throws_nor_tears()
4949 {
5050 const int seeded = 500 ;
5151 const int added = 5000 ;
52+ const int passes = 50 ;
53+ var patience = TimeSpan . FromSeconds ( 30 ) ;
5254
5355 var registry = EmptyRegistry ( ) ;
5456 for ( var i = 0 ; i < seeded ; i ++ )
5557 {
5658 registry [ ( NpgsqlDbType ) i ] = AnyMapping ( ) ;
5759 }
5860
59- // Bounded on both sides: the writer stops after a fixed number of registrations and the
60- // reader stops with it, so the dictionary cannot grow without limit underneath repeated
61- // full enumerations.
61+ using var writingHasStarted = new ManualResetEventSlim ( false ) ;
62+ using var readerIsFinished = new ManualResetEventSlim ( false ) ;
63+
64+ // The writer holds the concurrent window open for the reader instead of racing it to the
65+ // finish: it registers the new keys, then keeps re-registering them -- which mutates the
66+ // map without moving the final count -- until the reader has taken its passes. The reader
67+ // in turn does not start until the first registration has landed, so every pass below is
68+ // taken against a registry actively being written to.
69+ //
70+ // The earlier `while (!writer.IsCompleted)` shape had no such handshake: when the pool
71+ // scheduled the writer promptly it finished all 5,000 registrations before the first
72+ // IsCompleted check, the loop body never ran, and the test asserted nothing at all --
73+ // green here, and intermittently red on the trailing `passes > 0` in CI.
6274 var writer = Task . Run ( ( ) =>
6375 {
6476 for ( var i = 0 ; i < added ; i ++ )
6577 {
6678 registry [ ( NpgsqlDbType ) ( 100_000 + i ) ] = AnyMapping ( ) ;
79+ writingHasStarted . Set ( ) ;
80+ }
81+
82+ while ( ! readerIsFinished . IsSet )
83+ {
84+ for ( var i = 0 ; i < added && ! readerIsFinished . IsSet ; i ++ )
85+ {
86+ registry [ ( NpgsqlDbType ) ( 100_000 + i ) ] = AnyMapping ( ) ;
87+ }
6788 }
6889 } ) ;
6990
70- var passes = 0 ;
71- while ( ! writer . IsCompleted )
91+ try
92+ {
93+ writingHasStarted . Wait ( patience ) . ShouldBeTrue ( ) ;
94+
95+ for ( var pass = 0 ; pass < passes ; pass ++ )
96+ {
97+ // Each read must see a coherent snapshot: never fewer than what was there before
98+ // the writer started, and never a null hole.
99+ var seen = registry . ToList ( ) ;
100+ seen . Count . ShouldBeGreaterThanOrEqualTo ( seeded ) ;
101+ seen . ShouldAllBe ( mapping => mapping != null ) ;
102+ }
103+ }
104+ finally
72105 {
73- // Each read must see a coherent snapshot: never fewer than what was there before the
74- // writer started, and never a null hole.
75- var seen = registry . ToList ( ) ;
76- seen . Count . ShouldBeGreaterThanOrEqualTo ( seeded ) ;
77- seen . ShouldAllBe ( mapping => mapping != null ) ;
78- passes ++ ;
106+ // In a finally so a failed assertion above releases the writer rather than hanging
107+ // the run in its churn loop.
108+ readerIsFinished . Set ( ) ;
79109 }
80110
81111 writer . GetAwaiter ( ) . GetResult ( ) ;
82112 registry . Count . ShouldBe ( seeded + added ) ;
83- passes . ShouldBeGreaterThan ( 0 ) ;
84113 }
85114
86115 [ Fact ]
0 commit comments