11using System ;
22using System . Collections . Generic ;
3+ using System . Diagnostics ;
34using System . Diagnostics . Contracts ;
45using System . Threading ;
56using System . Threading . Channels ;
@@ -20,7 +21,12 @@ public class BatchingChannelReader<T> : BufferingChannelReader<T, List<T>>
2021 /// Constructs a BatchingChannelReader.
2122 /// Use the .Batch extension instead of constructing this directly.
2223 /// </summary>
23- public BatchingChannelReader ( ChannelReader < T > source , int batchSize , bool singleReader , bool syncCont = false ) : base ( source , singleReader , syncCont )
24+ public BatchingChannelReader (
25+ ChannelReader < T > source ,
26+ int batchSize ,
27+ bool singleReader ,
28+ bool syncCont = false )
29+ : base ( source , singleReader , syncCont )
2430 {
2531 if ( batchSize < 1 ) throw new ArgumentOutOfRangeException ( nameof ( batchSize ) , batchSize , "Must be at least 1." ) ;
2632 Contract . EndContractBlock ( ) ;
@@ -32,31 +38,59 @@ public BatchingChannelReader(ChannelReader<T> source, int batchSize, bool single
3238 /// If no full batch is waiting, will force buffering any batch that has at least one item.
3339 /// Returns true if anything was added to the buffer.
3440 /// </summary>
35- public bool ForceBatch ( )
41+ public bool ForceBatch ( ) => TryPipeItems ( true ) ;
42+
43+ long _timeout = - 1 ;
44+ Timer ? _timer ;
45+
46+ /// <summary>
47+ /// Specifies a timeout by which a batch will be emmited there is at least one item but has been waiting
48+ /// for longer than the timeout value.
49+ /// </summary>
50+ /// <param name="millisecondsTimeout">
51+ /// The timeout value where after a batch is forced.<br/>
52+ /// A value of zero or less cancels/clears any timeout.
53+ /// </param>
54+ /// <returns>The current reader.</returns>
55+ public BatchingChannelReader < T > WithTimeout ( long millisecondsTimeout )
3656 {
37- if ( Buffer is null || Buffer . Reader . Completion . IsCompleted ) return false ;
38- if ( TryPipeItems ( ) ) return true ;
39- if ( _batch is null ) return false ;
57+ _timeout = millisecondsTimeout <= 0 ? Timeout . Infinite : millisecondsTimeout ;
4058
41- lock ( Buffer )
59+ if ( Buffer is null || Buffer . Reader . Completion . IsCompleted )
60+ return this ;
61+
62+ if ( _timeout == Timeout . Infinite )
4263 {
43- if ( Buffer . Reader . Completion . IsCompleted ) return false ;
44- if ( TryPipeItems ( ) ) return true ;
45- List < T > ? c = _batch ;
46- if ( c is null || Buffer . Reader . Completion . IsCompleted )
47- return false ;
48- c . TrimExcess ( ) ;
49- _batch = null ;
50- return Buffer . Writer . TryWrite ( c ) ; // Should always be true at this point.
64+ Interlocked . Exchange ( ref _timer , null ) ? . Dispose ( ) ;
65+ return this ;
5166 }
67+
68+ LazyInitializer . EnsureInitialized ( ref _timer ,
69+ ( ) => new Timer ( obj => ForceBatch ( ) ) ) ;
70+
71+ return this ;
5272 }
5373
74+ /// <param name="timeout">
75+ /// The timeout value where after a batch is forced.<br/>
76+ /// A value of zero or less cancels/clears any timeout.<br/>
77+ /// Note: Values are converted to milliseconds.
78+ /// </param>
79+ /// <inheritdoc cref="WithTimeout(long)"/>
80+ public BatchingChannelReader < T > WithTimeout ( TimeSpan timeout )
81+ => WithTimeout ( TimeSpan . FromMilliseconds ( timeout . TotalMilliseconds ) ) ;
82+
5483 /// <inheritdoc />
55- protected override bool TryPipeItems ( )
84+ protected override void OnBeforeFinalFlush ( )
85+ => Interlocked . Exchange ( ref _timer , null ) ? . Dispose ( ) ;
86+
87+ /// <inheritdoc />
88+ protected override bool TryPipeItems ( bool flush )
5689 {
5790 if ( Buffer is null || Buffer . Reader . Completion . IsCompleted )
5891 return false ;
5992
93+ var batched = false ;
6094 lock ( Buffer )
6195 {
6296 if ( Buffer . Reader . Completion . IsCompleted ) return false ;
@@ -65,37 +99,50 @@ protected override bool TryPipeItems()
6599 ChannelReader < T > ? source = Source ;
66100 if ( source is null || source . Completion . IsCompleted )
67101 {
68- // All finished, release the last batch to the buffer.
102+ // All finished, if necessary, release the last batch to the buffer.
69103 if ( c is null ) return false ;
70-
71- c . TrimExcess ( ) ;
72- _batch = null ;
73-
74- Buffer . Writer . TryWrite ( c ) ;
75- return true ;
104+ goto flushBatch ;
76105 }
77106
78107 while ( source . TryRead ( out T ? item ) )
79108 {
80109 if ( c is null ) _batch = c = new List < T > ( _batchSize ) { item } ;
81110 else c . Add ( item ) ;
82111
83- if ( c . Count == _batchSize )
84- {
85- _batch = null ;
86- Buffer . Writer . TryWrite ( c ) ;
87- return true ;
88- }
112+ Debug . Assert ( c . Count <= _batchSize ) ;
113+ if ( c . Count != _batchSize ) continue ; // should never be greater.
114+
115+ _batch = null ; // _batch should always have at least 1 item in it.
116+ batched = Buffer . Writer . TryWrite ( c ) ;
117+ Debug . Assert ( batched ) ;
118+ c = null ;
89119 }
90120
91- return false ;
121+ if ( ! flush || c is null )
122+ goto finalizeTimer ;
123+
124+ flushBatch :
125+
126+ c . TrimExcess ( ) ;
127+ _batch = null ;
128+
129+ batched = Buffer . Writer . TryWrite ( c ) ;
130+ Debug . Assert ( batched ) ;
131+
132+ finalizeTimer :
133+
134+ var ok = _timer ? . Change ( _batch is null ? Timeout . Infinite : _timeout , 0 ) ;
135+ Debug . Assert ( ok ?? true ) ;
136+
137+ return batched ;
92138 }
93139 }
94140
95141 /// <inheritdoc />
96- protected override async ValueTask < bool > WaitToReadAsyncCore ( ValueTask < bool > bufferWait , CancellationToken cancellationToken )
142+ protected override async ValueTask < bool > WaitToReadAsyncCore (
143+ ValueTask < bool > bufferWait ,
144+ CancellationToken cancellationToken )
97145 {
98-
99146 ChannelReader < T > ? source = Source ;
100147 if ( source is null ) return await bufferWait . ConfigureAwait ( false ) ;
101148
@@ -108,7 +155,7 @@ protected override async ValueTask<bool> WaitToReadAsyncCore(ValueTask<bool> buf
108155 if ( b . IsCompleted ) return await b . ConfigureAwait ( false ) ;
109156
110157 ValueTask < bool > s = source . WaitToReadAsync ( token ) ;
111- if ( s . IsCompleted && ! b . IsCompleted ) TryPipeItems ( ) ;
158+ if ( s . IsCompleted && ! b . IsCompleted ) TryPipeItems ( false ) ;
112159
113160 if ( b . IsCompleted )
114161 {
@@ -123,7 +170,7 @@ protected override async ValueTask<bool> WaitToReadAsyncCore(ValueTask<bool> buf
123170 return await b . ConfigureAwait ( false ) ;
124171 }
125172
126- TryPipeItems ( ) ;
173+ TryPipeItems ( false ) ;
127174 goto start ;
128175 }
129176}
0 commit comments