1
- import type { ClickHouse , TaskRunV1 } from "@internal/clickhouse" ;
1
+ import type { ClickHouse , TaskRunV1 , RawTaskRunPayloadV1 } from "@internal/clickhouse" ;
2
2
import { RedisOptions } from "@internal/redis" ;
3
3
import { LogicalReplicationClient , Transaction , type PgoutputMessage } from "@internal/replication" ;
4
4
import { Logger } from "@trigger.dev/core/logger" ;
@@ -25,6 +25,8 @@ export type RunsReplicationServiceOptions = {
25
25
flushBatchSize ?: number ;
26
26
} ;
27
27
28
+ type TaskRunInsert = { _version : bigint ; run : TaskRun ; event : "insert" | "update" } ;
29
+
28
30
export class RunsReplicationService {
29
31
private _lastLsn : string | null = null ;
30
32
private _isSubscribed = false ;
@@ -36,7 +38,7 @@ export class RunsReplicationService {
36
38
| null = null ;
37
39
38
40
private _replicationClient : LogicalReplicationClient ;
39
- private _concurrentFlushScheduler : ConcurrentFlushScheduler < { _version : bigint ; run : TaskRun } > ;
41
+ private _concurrentFlushScheduler : ConcurrentFlushScheduler < TaskRunInsert > ;
40
42
private logger : Logger ;
41
43
private _lastReplicationLagMs : number | null = null ;
42
44
private _transactionCounter ?: Counter ;
@@ -64,10 +66,7 @@ export class RunsReplicationService {
64
66
ackIntervalSeconds : 10 ,
65
67
} ) ;
66
68
67
- this . _concurrentFlushScheduler = new ConcurrentFlushScheduler < {
68
- _version : bigint ;
69
- run : TaskRun ;
70
- } > ( {
69
+ this . _concurrentFlushScheduler = new ConcurrentFlushScheduler < TaskRunInsert > ( {
71
70
batchSize : options . flushBatchSize ?? 50 ,
72
71
flushInterval : options . flushIntervalMs ?? 100 ,
73
72
maxConcurrency : options . maxFlushConcurrency ?? 100 ,
@@ -128,7 +127,9 @@ export class RunsReplicationService {
128
127
}
129
128
}
130
129
131
- async start ( ) {
130
+ async start ( insertStrategy ?: "streaming" | "batching" ) {
131
+ this . _insertStrategy = insertStrategy ?? this . _insertStrategy ;
132
+
132
133
this . logger . info ( "Starting replication client" , {
133
134
lastLsn : this . _lastLsn ,
134
135
} ) ;
@@ -216,6 +217,20 @@ export class RunsReplicationService {
216
217
return ;
217
218
}
218
219
220
+ const relevantEvents = transaction . events . filter (
221
+ ( event ) => event . tag === "insert" || event . tag === "update"
222
+ ) ;
223
+
224
+ if ( relevantEvents . length === 0 ) {
225
+ this . logger . debug ( "No relevant events" , {
226
+ transaction,
227
+ } ) ;
228
+
229
+ await this . _replicationClient . acknowledge ( transaction . commitEndLsn ) ;
230
+
231
+ return ;
232
+ }
233
+
219
234
this . logger . debug ( "Handling transaction" , {
220
235
transaction,
221
236
} ) ;
@@ -227,13 +242,21 @@ export class RunsReplicationService {
227
242
228
243
if ( this . _insertStrategy === "streaming" ) {
229
244
await this . _concurrentFlushScheduler . addToBatch (
230
- transaction . events . map ( ( event ) => ( { _version, run : event . data } ) )
245
+ relevantEvents . map ( ( event ) => ( {
246
+ _version,
247
+ run : event . data ,
248
+ event : event . tag as "insert" | "update" ,
249
+ } ) )
231
250
) ;
232
251
} else {
233
252
const [ flushError ] = await tryCatch (
234
253
this . #flushBatch(
235
254
nanoid ( ) ,
236
- transaction . events . map ( ( event ) => ( { _version, run : event . data } ) )
255
+ relevantEvents . map ( ( event ) => ( {
256
+ _version,
257
+ run : event . data ,
258
+ event : event . tag as "insert" | "update" ,
259
+ } ) )
237
260
)
238
261
) ;
239
262
@@ -247,7 +270,7 @@ export class RunsReplicationService {
247
270
await this . _replicationClient . acknowledge ( transaction . commitEndLsn ) ;
248
271
}
249
272
250
- async #flushBatch( flushId : string , batch : Array < { _version : bigint ; run : TaskRun } > ) {
273
+ async #flushBatch( flushId : string , batch : Array < TaskRunInsert > ) {
251
274
if ( batch . length === 0 ) {
252
275
this . logger . debug ( "No runs to flush" , {
253
276
flushId,
@@ -260,19 +283,37 @@ export class RunsReplicationService {
260
283
batchSize : batch . length ,
261
284
} ) ;
262
285
263
- const preparedRuns = await Promise . all ( batch . map ( this . #prepareRun. bind ( this ) ) ) ;
264
- const runsToInsert = preparedRuns . filter ( Boolean ) ;
286
+ const preparedInserts = await Promise . all ( batch . map ( this . #prepareRunInserts. bind ( this ) ) ) ;
265
287
266
- if ( runsToInsert . length === 0 ) {
267
- this . logger . debug ( "No runs to insert" , {
268
- flushId,
269
- batchSize : batch . length ,
270
- } ) ;
271
- return ;
272
- }
288
+ const taskRunInserts = preparedInserts
289
+ . map ( ( { taskRunInsert } ) => taskRunInsert )
290
+ . filter ( Boolean ) ;
291
+
292
+ const payloadInserts = preparedInserts
293
+ . map ( ( { payloadInsert } ) => payloadInsert )
294
+ . filter ( Boolean ) ;
295
+
296
+ this . logger . info ( "Flushing inserts" , {
297
+ flushId,
298
+ taskRunInserts : taskRunInserts . length ,
299
+ payloadInserts : payloadInserts . length ,
300
+ } ) ;
273
301
302
+ await Promise . all ( [
303
+ this . #insertTaskRunInserts( taskRunInserts ) ,
304
+ this . #insertPayloadInserts( payloadInserts ) ,
305
+ ] ) ;
306
+
307
+ this . logger . info ( "Flushed inserts" , {
308
+ flushId,
309
+ taskRunInserts : taskRunInserts . length ,
310
+ payloadInserts : payloadInserts . length ,
311
+ } ) ;
312
+ }
313
+
314
+ async #insertTaskRunInserts( taskRunInserts : TaskRunV1 [ ] ) {
274
315
const [ insertError , insertResult ] = await this . options . clickhouse . taskRuns . insert (
275
- runsToInsert ,
316
+ taskRunInserts ,
276
317
{
277
318
params : {
278
319
clickhouse_settings : {
@@ -283,51 +324,100 @@ export class RunsReplicationService {
283
324
) ;
284
325
285
326
if ( insertError ) {
286
- this . logger . error ( "Error inserting runs " , {
327
+ this . logger . error ( "Error inserting task run inserts " , {
287
328
error : insertError ,
288
- flushId,
289
- batchSize : batch . length ,
290
329
} ) ;
291
- } else {
292
- this . logger . info ( "Flushed batch" , {
293
- flushId,
294
- insertResult,
330
+ }
331
+
332
+ return insertResult ;
333
+ }
334
+
335
+ async #insertPayloadInserts( payloadInserts : RawTaskRunPayloadV1 [ ] ) {
336
+ const [ insertError , insertResult ] = await this . options . clickhouse . taskRuns . insertPayloads (
337
+ payloadInserts ,
338
+ {
339
+ params : {
340
+ clickhouse_settings : {
341
+ wait_for_async_insert : this . _insertStrategy === "batching" ? 1 : 0 ,
342
+ } ,
343
+ } ,
344
+ }
345
+ ) ;
346
+
347
+ if ( insertError ) {
348
+ this . logger . error ( "Error inserting payload inserts" , {
349
+ error : insertError ,
295
350
} ) ;
296
351
}
352
+
353
+ return insertResult ;
297
354
}
298
355
299
- async #prepareRun( batchedRun : {
300
- run : TaskRun ;
301
- _version : bigint ;
302
- } ) : Promise < TaskRunV1 | undefined > {
356
+ async #prepareRunInserts(
357
+ batchedRun : TaskRunInsert
358
+ ) : Promise < { taskRunInsert ?: TaskRunV1 ; payloadInsert ?: RawTaskRunPayloadV1 } > {
303
359
this . logger . debug ( "Preparing run" , {
304
360
batchedRun,
305
361
} ) ;
306
362
307
- const { run, _version } = batchedRun ;
363
+ const { run, _version, event } = batchedRun ;
308
364
309
365
if ( ! run . environmentType ) {
310
- return undefined ;
366
+ return {
367
+ taskRunInsert : undefined ,
368
+ payloadInsert : undefined ,
369
+ } ;
311
370
}
312
371
313
372
if ( ! run . organizationId ) {
314
- return undefined ;
373
+ return {
374
+ taskRunInsert : undefined ,
375
+ payloadInsert : undefined ,
376
+ } ;
315
377
}
316
378
317
- const [ payload , output ] = await Promise . all ( [
318
- this . #prepareJson( run . payload , run . payloadType ) ,
319
- this . #prepareJson( run . output , run . outputType ) ,
379
+ if ( event === "update" ) {
380
+ const taskRunInsert = await this . #prepareTaskRunInsert(
381
+ run ,
382
+ run . organizationId ,
383
+ run . environmentType ,
384
+ _version
385
+ ) ;
386
+
387
+ return {
388
+ taskRunInsert,
389
+ payloadInsert : undefined ,
390
+ } ;
391
+ }
392
+
393
+ const [ taskRunInsert , payloadInsert ] = await Promise . all ( [
394
+ this . #prepareTaskRunInsert( run , run . organizationId , run . environmentType , _version ) ,
395
+ this . #preparePayloadInsert( run , _version ) ,
320
396
] ) ;
321
397
398
+ return {
399
+ taskRunInsert,
400
+ payloadInsert,
401
+ } ;
402
+ }
403
+
404
+ async #prepareTaskRunInsert(
405
+ run : TaskRun ,
406
+ organizationId : string ,
407
+ environmentType : string ,
408
+ _version : bigint
409
+ ) : Promise < TaskRunV1 > {
410
+ const output = await this . #prepareJson( run . output , run . outputType ) ;
411
+
322
412
return {
323
413
environment_id : run . runtimeEnvironmentId ,
324
- organization_id : run . organizationId ,
414
+ organization_id : organizationId ,
325
415
project_id : run . projectId ,
326
416
run_id : run . id ,
327
417
updated_at : run . updatedAt . getTime ( ) ,
328
418
created_at : run . createdAt . getTime ( ) ,
329
419
status : run . status ,
330
- environment_type : run . environmentType ,
420
+ environment_type : environmentType ,
331
421
friendly_id : run . friendlyId ,
332
422
engine : run . engine ,
333
423
task_identifier : run . taskIdentifier ,
@@ -347,7 +437,7 @@ export class RunsReplicationService {
347
437
usage_duration_ms : run . usageDurationMs ,
348
438
cost_in_cents : run . costInCents ,
349
439
base_cost_in_cents : run . baseCostInCents ,
350
- tags : run . runTags ,
440
+ tags : run . runTags ?? [ ] ,
351
441
task_version : run . taskVersion ,
352
442
sdk_version : run . sdkVersion ,
353
443
cli_version : run . cliVersion ,
@@ -358,22 +448,31 @@ export class RunsReplicationService {
358
448
is_test : run . isTest ,
359
449
idempotency_key : run . idempotencyKey ,
360
450
expiration_ttl : run . ttl ,
361
- payload,
362
451
output,
363
452
_version : _version . toString ( ) ,
364
453
} ;
365
454
}
366
455
456
+ async #preparePayloadInsert( run : TaskRun , _version : bigint ) : Promise < RawTaskRunPayloadV1 > {
457
+ const payload = await this . #prepareJson( run . payload , run . payloadType ) ;
458
+
459
+ return {
460
+ run_id : run . id ,
461
+ created_at : run . createdAt . getTime ( ) ,
462
+ payload,
463
+ } ;
464
+ }
465
+
367
466
async #prepareJson(
368
467
data : string | undefined | null ,
369
468
dataType : string
370
- ) : Promise < unknown | undefined > {
469
+ ) : Promise < { data : unknown } > {
371
470
if ( ! data ) {
372
- return undefined ;
471
+ return { data : undefined } ;
373
472
}
374
473
375
474
if ( dataType !== "application/json" && dataType !== "application/super+json" ) {
376
- return undefined ;
475
+ return { data : undefined } ;
377
476
}
378
477
379
478
const packet = {
@@ -384,7 +483,7 @@ export class RunsReplicationService {
384
483
const parsedData = await parsePacket ( packet ) ;
385
484
386
485
if ( ! parsedData ) {
387
- return undefined ;
486
+ return { data : undefined } ;
388
487
}
389
488
390
489
return { data : parsedData } ;
@@ -453,6 +552,24 @@ export class ConcurrentFlushScheduler<T> {
453
552
} ,
454
553
registers : [ this . metricsRegister ] ,
455
554
} ) ;
555
+
556
+ new Gauge ( {
557
+ name : "concurrent_flush_scheduler_active_concurrency" ,
558
+ help : "Number of active concurrency" ,
559
+ collect ( ) {
560
+ this . set ( scheduler . concurrencyLimiter . activeCount ) ;
561
+ } ,
562
+ registers : [ this . metricsRegister ] ,
563
+ } ) ;
564
+
565
+ new Gauge ( {
566
+ name : "concurrent_flush_scheduler_pending_concurrency" ,
567
+ help : "Number of pending concurrency" ,
568
+ collect ( ) {
569
+ this . set ( scheduler . concurrencyLimiter . pendingCount ) ;
570
+ } ,
571
+ registers : [ this . metricsRegister ] ,
572
+ } ) ;
456
573
}
457
574
}
458
575
0 commit comments