11import { nanoid } from "nanoid" ;
2+ import pLimit from "p-limit" ;
3+ import { logger } from "~/services/logger.server" ;
24
35export type DynamicFlushSchedulerConfig < T > = {
46 batchSize : number ;
57 flushInterval : number ;
68 callback : ( flushId : string , batch : T [ ] ) => Promise < void > ;
9+ // New configuration options
10+ minConcurrency ?: number ;
11+ maxConcurrency ?: number ;
12+ maxBatchSize ?: number ;
13+ memoryPressureThreshold ?: number ; // Number of items that triggers increased concurrency
714} ;
815
916export class DynamicFlushScheduler < T > {
10- private batchQueue : T [ ] [ ] ; // Adjust the type according to your data structure
11- private currentBatch : T [ ] ; // Adjust the type according to your data structure
17+ private batchQueue : T [ ] [ ] ;
18+ private currentBatch : T [ ] ;
1219 private readonly BATCH_SIZE : number ;
1320 private readonly FLUSH_INTERVAL : number ;
1421 private flushTimer : NodeJS . Timeout | null ;
1522 private readonly callback : ( flushId : string , batch : T [ ] ) => Promise < void > ;
23+
24+ // New properties for dynamic scaling
25+ private readonly minConcurrency : number ;
26+ private readonly maxConcurrency : number ;
27+ private readonly maxBatchSize : number ;
28+ private readonly memoryPressureThreshold : number ;
29+ private limiter : ReturnType < typeof pLimit > ;
30+ private currentBatchSize : number ;
31+ private totalQueuedItems : number = 0 ;
32+ private consecutiveFlushFailures : number = 0 ;
33+ private lastFlushTime : number = Date . now ( ) ;
34+ private metrics = {
35+ flushedBatches : 0 ,
36+ failedBatches : 0 ,
37+ totalItemsFlushed : 0 ,
38+ } ;
1639
1740 constructor ( config : DynamicFlushSchedulerConfig < T > ) {
1841 this . batchQueue = [ ] ;
1942 this . currentBatch = [ ] ;
2043 this . BATCH_SIZE = config . batchSize ;
44+ this . currentBatchSize = config . batchSize ;
2145 this . FLUSH_INTERVAL = config . flushInterval ;
2246 this . callback = config . callback ;
2347 this . flushTimer = null ;
48+
49+ // Initialize dynamic scaling parameters
50+ this . minConcurrency = config . minConcurrency ?? 1 ;
51+ this . maxConcurrency = config . maxConcurrency ?? 10 ;
52+ this . maxBatchSize = config . maxBatchSize ?? config . batchSize * 5 ;
53+ this . memoryPressureThreshold = config . memoryPressureThreshold ?? config . batchSize * 20 ;
54+
55+ // Start with minimum concurrency
56+ this . limiter = pLimit ( this . minConcurrency ) ;
57+
2458 this . startFlushTimer ( ) ;
59+ this . startMetricsReporter ( ) ;
2560 }
2661
2762 addToBatch ( items : T [ ] ) : void {
2863 this . currentBatch . push ( ...items ) ;
64+ this . totalQueuedItems += items . length ;
2965
30- if ( this . currentBatch . length >= this . BATCH_SIZE ) {
31- this . batchQueue . push ( this . currentBatch ) ;
32- this . currentBatch = [ ] ;
33- this . flushNextBatch ( ) ;
34- this . resetFlushTimer ( ) ;
66+ // Check if we need to create a batch
67+ if ( this . currentBatch . length >= this . currentBatchSize ) {
68+ this . createBatch ( ) ;
3569 }
70+
71+ // Adjust concurrency based on queue pressure
72+ this . adjustConcurrency ( ) ;
73+ }
74+
75+ private createBatch ( ) : void {
76+ if ( this . currentBatch . length === 0 ) return ;
77+
78+ this . batchQueue . push ( this . currentBatch ) ;
79+ this . currentBatch = [ ] ;
80+ this . flushBatches ( ) ;
81+ this . resetFlushTimer ( ) ;
3682 }
3783
3884 private startFlushTimer ( ) : void {
@@ -48,23 +94,169 @@ export class DynamicFlushScheduler<T> {
4894
4995 private checkAndFlush ( ) : void {
5096 if ( this . currentBatch . length > 0 ) {
51- this . batchQueue . push ( this . currentBatch ) ;
52- this . currentBatch = [ ] ;
97+ this . createBatch ( ) ;
5398 }
54- this . flushNextBatch ( ) ;
99+ this . flushBatches ( ) ;
55100 }
56101
57- private async flushNextBatch ( ) : Promise < void > {
58- if ( this . batchQueue . length === 0 ) return ;
102+ private async flushBatches ( ) : Promise < void > {
103+ const batchesToFlush : T [ ] [ ] = [ ] ;
104+
105+ // Dequeue all available batches up to current concurrency limit
106+ while ( this . batchQueue . length > 0 && batchesToFlush . length < this . limiter . concurrency ) {
107+ const batch = this . batchQueue . shift ( ) ;
108+ if ( batch ) {
109+ batchesToFlush . push ( batch ) ;
110+ }
111+ }
112+
113+ if ( batchesToFlush . length === 0 ) return ;
59114
60- const batchToFlush = this . batchQueue . shift ( ) ;
61- try {
62- await this . callback ( nanoid ( ) , batchToFlush ! ) ;
115+ // Schedule all batches for concurrent processing
116+ const flushPromises = batchesToFlush . map ( ( batch ) =>
117+ this . limiter ( async ( ) => {
118+ const flushId = nanoid ( ) ;
119+ const itemCount = batch . length ;
120+
121+ try {
122+ const startTime = Date . now ( ) ;
123+ await this . callback ( flushId , batch ) ;
124+
125+ const duration = Date . now ( ) - startTime ;
126+ this . totalQueuedItems -= itemCount ;
127+ this . consecutiveFlushFailures = 0 ;
128+ this . lastFlushTime = Date . now ( ) ;
129+ this . metrics . flushedBatches ++ ;
130+ this . metrics . totalItemsFlushed += itemCount ;
131+
132+ logger . debug ( "Batch flushed successfully" , {
133+ flushId,
134+ itemCount,
135+ duration,
136+ remainingQueueDepth : this . totalQueuedItems ,
137+ activeConcurrency : this . limiter . activeCount ,
138+ pendingConcurrency : this . limiter . pendingCount ,
139+ } ) ;
140+ } catch ( error ) {
141+ this . consecutiveFlushFailures ++ ;
142+ this . metrics . failedBatches ++ ;
143+
144+ logger . error ( "Error flushing batch" , {
145+ flushId,
146+ itemCount,
147+ error,
148+ consecutiveFailures : this . consecutiveFlushFailures ,
149+ } ) ;
150+
151+ // Re-queue the batch at the front if it fails
152+ this . batchQueue . unshift ( batch ) ;
153+ this . totalQueuedItems += itemCount ;
154+
155+ // Back off on failures
156+ if ( this . consecutiveFlushFailures > 3 ) {
157+ this . adjustConcurrency ( true ) ;
158+ }
159+ }
160+ } )
161+ ) ;
162+
163+ // Don't await here - let them run concurrently
164+ Promise . allSettled ( flushPromises ) . then ( ( ) => {
165+ // After flush completes, check if we need to flush more
63166 if ( this . batchQueue . length > 0 ) {
64- this . flushNextBatch ( ) ;
167+ this . flushBatches ( ) ;
65168 }
66- } catch ( error ) {
67- console . error ( "Error inserting batch:" , error ) ;
169+ } ) ;
170+ }
171+
172+ private adjustConcurrency ( backOff : boolean = false ) : void {
173+ const currentConcurrency = this . limiter . concurrency ;
174+ let newConcurrency = currentConcurrency ;
175+
176+ if ( backOff ) {
177+ // Reduce concurrency on failures
178+ newConcurrency = Math . max ( this . minConcurrency , Math . floor ( currentConcurrency * 0.75 ) ) ;
179+ } else {
180+ // Calculate pressure metrics
181+ const queuePressure = this . totalQueuedItems / this . memoryPressureThreshold ;
182+ const timeSinceLastFlush = Date . now ( ) - this . lastFlushTime ;
183+
184+ if ( queuePressure > 0.8 || timeSinceLastFlush > this . FLUSH_INTERVAL * 2 ) {
185+ // High pressure - increase concurrency
186+ newConcurrency = Math . min ( this . maxConcurrency , currentConcurrency + 2 ) ;
187+ } else if ( queuePressure < 0.2 && currentConcurrency > this . minConcurrency ) {
188+ // Low pressure - decrease concurrency
189+ newConcurrency = Math . max ( this . minConcurrency , currentConcurrency - 1 ) ;
190+ }
191+ }
192+
193+ // Adjust batch size based on pressure
194+ if ( this . totalQueuedItems > this . memoryPressureThreshold ) {
195+ this . currentBatchSize = Math . min (
196+ this . maxBatchSize ,
197+ Math . floor ( this . BATCH_SIZE * ( 1 + queuePressure ) )
198+ ) ;
199+ } else {
200+ this . currentBatchSize = this . BATCH_SIZE ;
201+ }
202+
203+ // Update concurrency if changed
204+ if ( newConcurrency !== currentConcurrency ) {
205+ this . limiter = pLimit ( newConcurrency ) ;
206+
207+ logger . info ( "Adjusted flush concurrency" , {
208+ previousConcurrency : currentConcurrency ,
209+ newConcurrency,
210+ queuePressure,
211+ totalQueuedItems : this . totalQueuedItems ,
212+ currentBatchSize : this . currentBatchSize ,
213+ } ) ;
214+ }
215+ }
216+
217+ private startMetricsReporter ( ) : void {
218+ // Report metrics every 30 seconds
219+ setInterval ( ( ) => {
220+ logger . info ( "DynamicFlushScheduler metrics" , {
221+ totalQueuedItems : this . totalQueuedItems ,
222+ batchQueueLength : this . batchQueue . length ,
223+ currentBatchLength : this . currentBatch . length ,
224+ currentConcurrency : this . limiter . concurrency ,
225+ activeConcurrent : this . limiter . activeCount ,
226+ pendingConcurrent : this . limiter . pendingCount ,
227+ currentBatchSize : this . currentBatchSize ,
228+ metrics : this . metrics ,
229+ } ) ;
230+ } , 30000 ) ;
231+ }
232+
233+ // Method to get current status
234+ getStatus ( ) {
235+ return {
236+ queuedItems : this . totalQueuedItems ,
237+ batchQueueLength : this . batchQueue . length ,
238+ currentBatchSize : this . currentBatch . length ,
239+ concurrency : this . limiter . concurrency ,
240+ activeFlushes : this . limiter . activeCount ,
241+ pendingFlushes : this . limiter . pendingCount ,
242+ metrics : { ...this . metrics } ,
243+ } ;
244+ }
245+
246+ // Graceful shutdown
247+ async shutdown ( ) : Promise < void > {
248+ if ( this . flushTimer ) {
249+ clearInterval ( this . flushTimer ) ;
250+ }
251+
252+ // Flush any remaining items
253+ if ( this . currentBatch . length > 0 ) {
254+ this . createBatch ( ) ;
255+ }
256+
257+ // Wait for all pending flushes to complete
258+ while ( this . batchQueue . length > 0 || this . limiter . activeCount > 0 ) {
259+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
68260 }
69261 }
70- }
262+ }
0 commit comments