duron 0.3.0-beta.6 → 0.3.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,7 @@
2
2
  "version": "8",
3
3
  "dialect": "postgres",
4
4
  "id": "4e93c0bf-b135-40b4-b130-0cf0968bffe3",
5
- "prevIds": [
6
- "00000000-0000-0000-0000-000000000000"
7
- ],
5
+ "prevIds": ["00000000-0000-0000-0000-000000000000"],
8
6
  "ddl": [
9
7
  {
10
8
  "name": "duron",
@@ -1268,14 +1266,10 @@
1268
1266
  },
1269
1267
  {
1270
1268
  "nameExplicit": false,
1271
- "columns": [
1272
- "job_id"
1273
- ],
1269
+ "columns": ["job_id"],
1274
1270
  "schemaTo": "duron",
1275
1271
  "tableTo": "jobs",
1276
- "columnsTo": [
1277
- "id"
1278
- ],
1272
+ "columnsTo": ["id"],
1279
1273
  "onUpdate": "NO ACTION",
1280
1274
  "onDelete": "CASCADE",
1281
1275
  "name": "job_steps_job_id_jobs_id_fkey",
@@ -1285,14 +1279,10 @@
1285
1279
  },
1286
1280
  {
1287
1281
  "nameExplicit": false,
1288
- "columns": [
1289
- "job_id"
1290
- ],
1282
+ "columns": ["job_id"],
1291
1283
  "schemaTo": "duron",
1292
1284
  "tableTo": "jobs",
1293
- "columnsTo": [
1294
- "id"
1295
- ],
1285
+ "columnsTo": ["id"],
1296
1286
  "onUpdate": "NO ACTION",
1297
1287
  "onDelete": "CASCADE",
1298
1288
  "name": "metrics_job_id_jobs_id_fkey",
@@ -1302,14 +1292,10 @@
1302
1292
  },
1303
1293
  {
1304
1294
  "nameExplicit": false,
1305
- "columns": [
1306
- "step_id"
1307
- ],
1295
+ "columns": ["step_id"],
1308
1296
  "schemaTo": "duron",
1309
1297
  "tableTo": "job_steps",
1310
- "columnsTo": [
1311
- "id"
1312
- ],
1298
+ "columnsTo": ["id"],
1313
1299
  "onUpdate": "NO ACTION",
1314
1300
  "onDelete": "CASCADE",
1315
1301
  "name": "metrics_step_id_job_steps_id_fkey",
@@ -1318,9 +1304,7 @@
1318
1304
  "table": "metrics"
1319
1305
  },
1320
1306
  {
1321
- "columns": [
1322
- "id"
1323
- ],
1307
+ "columns": ["id"],
1324
1308
  "nameExplicit": false,
1325
1309
  "name": "job_steps_pkey",
1326
1310
  "schema": "duron",
@@ -1328,9 +1312,7 @@
1328
1312
  "entityType": "pks"
1329
1313
  },
1330
1314
  {
1331
- "columns": [
1332
- "id"
1333
- ],
1315
+ "columns": ["id"],
1334
1316
  "nameExplicit": false,
1335
1317
  "name": "jobs_pkey",
1336
1318
  "schema": "duron",
@@ -1338,9 +1320,7 @@
1338
1320
  "entityType": "pks"
1339
1321
  },
1340
1322
  {
1341
- "columns": [
1342
- "id"
1343
- ],
1323
+ "columns": ["id"],
1344
1324
  "nameExplicit": false,
1345
1325
  "name": "metrics_pkey",
1346
1326
  "schema": "duron",
@@ -1349,11 +1329,7 @@
1349
1329
  },
1350
1330
  {
1351
1331
  "nameExplicit": true,
1352
- "columns": [
1353
- "job_id",
1354
- "name",
1355
- "parent_step_id"
1356
- ],
1332
+ "columns": ["job_id", "name", "parent_step_id"],
1357
1333
  "nullsNotDistinct": true,
1358
1334
  "name": "unique_job_step_name_parent",
1359
1335
  "entityType": "uniques",
@@ -1383,4 +1359,4 @@
1383
1359
  }
1384
1360
  ],
1385
1361
  "renames": []
1386
- }
1362
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "duron",
3
- "version": "0.3.0-beta.6",
3
+ "version": "0.3.0-beta.8",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
package/src/action-job.ts CHANGED
@@ -61,7 +61,7 @@ export class ActionJob<TAction extends Action<any, any, any>> {
61
61
  adapter: options.database,
62
62
  telemetry: options.telemetry,
63
63
  logger: options.logger,
64
- concurrencyLimit: options.action.concurrency,
64
+ concurrencyLimit: options.action.steps.concurrency,
65
65
  })
66
66
 
67
67
  this.#done = new Promise((resolve) => {
package/src/action.ts CHANGED
@@ -133,18 +133,13 @@ export interface StepDefinitionHandlerContext<TInput extends z.ZodObject, TVaria
133
133
  * The job ID this step belongs to.
134
134
  */
135
135
  jobId: string
136
-
137
136
  }
138
137
 
139
138
  /**
140
139
  * A reusable step definition created with createStep().
141
140
  * Can be executed within an action handler using ctx.run().
142
141
  */
143
- export interface StepDefinition<
144
- TInput extends z.ZodObject,
145
- TResult,
146
- TVariables = Record<string, unknown>,
147
- > {
142
+ export interface StepDefinition<TInput extends z.ZodObject, TResult, TVariables = Record<string, unknown>> {
148
143
  /**
149
144
  * The name of the step.
150
145
  * Can be a static string or a function that generates the name from the input.
@@ -358,9 +353,9 @@ export function createActionDefinitionSchema<
358
353
  * Function to determine the concurrency limit for a step.
359
354
  * The concurrency limit is stored with each step and used during fetch operations.
360
355
  * When fetching steps, the latest step's concurrency limit is used for each stepKey.
361
- * If not provided, defaults to 10.
356
+ * If not provided, defaults to 100.
362
357
  */
363
- concurrency: z.number().default(10).describe('How many steps can run concurrently for this action'),
358
+ concurrency: z.number().default(100).describe('How many steps can run concurrently for this action'),
364
359
  retry: RetryOptionsSchema.describe('How to retry on failure for the steps of this action'),
365
360
  expire: z
366
361
  .number()
@@ -368,7 +363,7 @@ export function createActionDefinitionSchema<
368
363
  .describe('How long a step can run for (milliseconds)'),
369
364
  })
370
365
  .default({
371
- concurrency: 10,
366
+ concurrency: 100,
372
367
  retry: { limit: 4, factor: 2, minTimeout: 1000, maxTimeout: 30000 },
373
368
  expire: 5 * 60 * 1000,
374
369
  }),
@@ -417,11 +412,10 @@ export const defineAction = <TVariables = Record<string, unknown>>() => {
417
412
  /**
418
413
  * Input type for createStep() - the definition object before transformation.
419
414
  */
420
- export type StepDefinitionInput<
421
- TInput extends z.ZodObject,
422
- TResult,
423
- TVariables = Record<string, unknown>,
424
- > = Omit<StepDefinition<TInput, TResult, TVariables>, '__stepDefinition'>
415
+ export type StepDefinitionInput<TInput extends z.ZodObject, TResult, TVariables = Record<string, unknown>> = Omit<
416
+ StepDefinition<TInput, TResult, TVariables>,
417
+ '__stepDefinition'
418
+ >
425
419
 
426
420
  /**
427
421
  * Creates a reusable step definition that can be executed within action handlers.
@@ -119,7 +119,9 @@ export default function createSchema(schemaName: string) {
119
119
  index('idx_job_steps_output_fts').using('gin', sql`to_tsvector('english', ${table.output}::text)`),
120
120
  // Unique constraint - step name is unique within a parent (name + parentStepId)
121
121
  // nullsNotDistinct ensures NULL parent_step_id values are treated as equal for uniqueness
122
- unique('unique_job_step_name_parent').on(table.job_id, table.name, table.parent_step_id).nullsNotDistinct(),
122
+ unique('unique_job_step_name_parent')
123
+ .on(table.job_id, table.name, table.parent_step_id)
124
+ .nullsNotDistinct(),
123
125
  check(
124
126
  'job_steps_status_check',
125
127
  sql`${table.status} IN ${sql.raw(`(${STEP_STATUSES.map((s) => `'${s}'`).join(',')})`)}`,
@@ -23,7 +23,46 @@ import {
23
23
  serializeError,
24
24
  UnhandledChildStepsError,
25
25
  } from './errors.js'
26
- import type { ObserveContext, Span, TelemetryAdapter } from './telemetry/adapter.js'
26
+ import type { ObserveContext, Span, TelemetryAdapter, Tracer, TracerSpan } from './telemetry/adapter.js'
27
+
28
+ // ============================================================================
29
+ // Noop Tracer (for fallback observe context)
30
+ // ============================================================================
31
+
32
+ const noopTracerSpan: TracerSpan = {
33
+ setAttribute() {
34
+ // No-op
35
+ },
36
+ setAttributes() {
37
+ // No-op
38
+ },
39
+ addEvent() {
40
+ // No-op
41
+ },
42
+ recordException() {
43
+ // No-op
44
+ },
45
+ setStatusOk() {
46
+ // No-op
47
+ },
48
+ setStatusError() {
49
+ // No-op
50
+ },
51
+ end() {
52
+ // No-op
53
+ },
54
+ isRecording() {
55
+ return false
56
+ },
57
+ }
58
+
59
+ const createNoopTracer = (name: string): Tracer => ({
60
+ name,
61
+ startSpan(): TracerSpan {
62
+ return noopTracerSpan
63
+ },
64
+ })
65
+
27
66
  import pRetry from './utils/p-retry.js'
28
67
  import waitForAbort from './utils/wait-for-abort.js'
29
68
 
@@ -251,6 +290,9 @@ export class StepManager {
251
290
  addSpanEvent: () => {
252
291
  // No-op
253
292
  },
293
+ getTracer: (name: string) => {
294
+ return createNoopTracer(name)
295
+ },
254
296
  }
255
297
  }
256
298
 
@@ -261,6 +303,21 @@ export class StepManager {
261
303
  * @returns Promise resolving to the step result
262
304
  */
263
305
  async push(task: TaskStep): Promise<any> {
306
+ // Warn about potential starvation when child steps are queued and all slots are occupied
307
+ if (task.parentStepId !== null && this.#queue.running() >= this.#queue.concurrency) {
308
+ this.#logger.warn(
309
+ {
310
+ jobId: this.#jobId,
311
+ actionName: this.#actionName,
312
+ stepName: task.name,
313
+ parentStepId: task.parentStepId,
314
+ running: this.#queue.running(),
315
+ waiting: this.#queue.length(),
316
+ concurrency: this.#queue.concurrency,
317
+ },
318
+ '[StepManager] Potential starvation: child step queued while all concurrency slots are occupied by parent steps. Consider increasing steps.concurrency.',
319
+ )
320
+ }
264
321
  return this.#queue.push(task)
265
322
  }
266
323
 
@@ -108,6 +108,106 @@ export interface AddSpanAttributeOptions {
108
108
  value: string | number | boolean
109
109
  }
110
110
 
111
+ /**
112
+ * Options for starting a custom span with the tracer.
113
+ */
114
+ export interface StartSpanOptions {
115
+ /**
116
+ * Span kind (internal, client, server, producer, consumer).
117
+ * @default 'internal'
118
+ */
119
+ kind?: 'internal' | 'client' | 'server' | 'producer' | 'consumer'
120
+
121
+ /**
122
+ * Initial attributes for the span.
123
+ */
124
+ attributes?: Record<string, string | number | boolean>
125
+
126
+ /**
127
+ * Parent span to use for context propagation.
128
+ * If not provided, uses the current active context.
129
+ */
130
+ parentSpan?: TracerSpan
131
+ }
132
+
133
+ /**
134
+ * A span created by the Tracer for manual instrumentation.
135
+ */
136
+ export interface TracerSpan {
137
+ /**
138
+ * Set an attribute on the span.
139
+ *
140
+ * @param key - The attribute key
141
+ * @param value - The attribute value
142
+ */
143
+ setAttribute(key: string, value: string | number | boolean): void
144
+
145
+ /**
146
+ * Set multiple attributes on the span.
147
+ *
148
+ * @param attributes - The attributes to set
149
+ */
150
+ setAttributes(attributes: Record<string, string | number | boolean>): void
151
+
152
+ /**
153
+ * Add an event to the span.
154
+ *
155
+ * @param name - The event name
156
+ * @param attributes - Optional event attributes
157
+ */
158
+ addEvent(name: string, attributes?: Record<string, string | number | boolean>): void
159
+
160
+ /**
161
+ * Record an exception on the span.
162
+ *
163
+ * @param error - The error to record
164
+ */
165
+ recordException(error: Error): void
166
+
167
+ /**
168
+ * Set the span status to OK.
169
+ */
170
+ setStatusOk(): void
171
+
172
+ /**
173
+ * Set the span status to error.
174
+ *
175
+ * @param message - Optional error message
176
+ */
177
+ setStatusError(message?: string): void
178
+
179
+ /**
180
+ * End the span.
181
+ * After calling this, no more operations can be performed on the span.
182
+ */
183
+ end(): void
184
+
185
+ /**
186
+ * Check if this span is recording.
187
+ */
188
+ isRecording(): boolean
189
+ }
190
+
191
+ /**
192
+ * A Tracer provides methods for creating spans.
193
+ * Similar to OpenTelemetry's Tracer interface.
194
+ */
195
+ export interface Tracer {
196
+ /**
197
+ * The name of this tracer.
198
+ */
199
+ readonly name: string
200
+
201
+ /**
202
+ * Start a new span.
203
+ *
204
+ * @param name - The name of the span
205
+ * @param options - Optional span configuration
206
+ * @returns A TracerSpan for manual instrumentation
207
+ */
208
+ startSpan(name: string, options?: StartSpanOptions): TracerSpan
209
+ }
210
+
111
211
  /**
112
212
  * Observe context provided to action and step handlers.
113
213
  */
@@ -136,6 +236,37 @@ export interface ObserveContext {
136
236
  * @param attributes - Optional event attributes
137
237
  */
138
238
  addSpanEvent(name: string, attributes?: Record<string, any>): void
239
+
240
+ /**
241
+ * Get a tracer for manual instrumentation.
242
+ * Similar to OpenTelemetry's `trace.getTracer()` method.
243
+ *
244
+ * @param name - The name of the tracer (typically your service or library name)
245
+ * @returns A Tracer for creating custom spans
246
+ *
247
+ * @example
248
+ * ```typescript
249
+ * const tracer = ctx.observe.getTracer('my-service')
250
+ *
251
+ * const span = tracer.startSpan('external-api-call', {
252
+ * kind: 'client',
253
+ * attributes: { 'api.endpoint': '/users' }
254
+ * })
255
+ *
256
+ * try {
257
+ * const result = await fetch('https://api.example.com/users')
258
+ * span.setStatusOk()
259
+ * return result
260
+ * } catch (error) {
261
+ * span.recordException(error)
262
+ * span.setStatusError(error.message)
263
+ * throw error
264
+ * } finally {
265
+ * span.end()
266
+ * }
267
+ * ```
268
+ */
269
+ getTracer(name: string): Tracer
139
270
  }
140
271
 
141
272
  // ============================================================================
@@ -369,6 +500,41 @@ export abstract class TelemetryAdapter {
369
500
  return this._addSpanAttribute(options)
370
501
  }
371
502
 
503
+ // ============================================================================
504
+ // Tracer Methods
505
+ // ============================================================================
506
+
507
+ /**
508
+ * Get a tracer for manual instrumentation.
509
+ * Similar to OpenTelemetry's `trace.getTracer()` method.
510
+ *
511
+ * @param name - The name of the tracer (typically your service or library name)
512
+ * @returns A Tracer for creating custom spans
513
+ *
514
+ * @example
515
+ * ```typescript
516
+ * const tracer = telemetry.getTracer('my-service')
517
+ *
518
+ * const span = tracer.startSpan('process-order', {
519
+ * attributes: { 'order.id': orderId }
520
+ * })
521
+ *
522
+ * try {
523
+ * // Do some work
524
+ * span.addEvent('order.validated')
525
+ * span.setStatusOk()
526
+ * } catch (error) {
527
+ * span.recordException(error)
528
+ * span.setStatusError(error.message)
529
+ * } finally {
530
+ * span.end()
531
+ * }
532
+ * ```
533
+ */
534
+ getTracer(name: string): Tracer {
535
+ return this._getTracer(name)
536
+ }
537
+
372
538
  // ============================================================================
373
539
  // Context Methods
374
540
  // ============================================================================
@@ -404,6 +570,9 @@ export abstract class TelemetryAdapter {
404
570
  this.#logger?.error(err, 'Error adding span event')
405
571
  })
406
572
  },
573
+ getTracer: (name: string) => {
574
+ return this.getTracer(name)
575
+ },
407
576
  }
408
577
  }
409
578
 
@@ -465,4 +634,9 @@ export abstract class TelemetryAdapter {
465
634
  * Internal method to add a span attribute.
466
635
  */
467
636
  protected abstract _addSpanAttribute(options: AddSpanAttributeOptions): Promise<void>
637
+
638
+ /**
639
+ * Internal method to get a tracer for manual instrumentation.
640
+ */
641
+ protected abstract _getTracer(name: string): Tracer
468
642
  }
@@ -9,8 +9,11 @@ export {
9
9
  type Span,
10
10
  type StartDatabaseSpanOptions,
11
11
  type StartJobSpanOptions,
12
+ type StartSpanOptions,
12
13
  type StartStepSpanOptions,
13
14
  TelemetryAdapter,
15
+ type Tracer,
16
+ type TracerSpan,
14
17
  } from './adapter.js'
15
18
  export { LocalTelemetryAdapter, type LocalTelemetryAdapterOptions, localTelemetryAdapter } from './local.js'
16
19
  export { NoopTelemetryAdapter, noopTelemetryAdapter } from './noop.js'
@@ -7,8 +7,11 @@ import {
7
7
  type Span,
8
8
  type StartDatabaseSpanOptions,
9
9
  type StartJobSpanOptions,
10
+ type StartSpanOptions,
10
11
  type StartStepSpanOptions,
11
12
  TelemetryAdapter,
13
+ type Tracer,
14
+ type TracerSpan,
12
15
  } from './adapter.js'
13
16
 
14
17
  // ============================================================================
@@ -304,6 +307,96 @@ export class LocalTelemetryAdapter extends TelemetryAdapter {
304
307
  },
305
308
  })
306
309
  }
310
+
311
+ // ============================================================================
312
+ // Tracer Methods
313
+ // ============================================================================
314
+
315
+ protected _getTracer(name: string): Tracer {
316
+ const adapter = this
317
+
318
+ return {
319
+ name,
320
+
321
+ startSpan(spanName: string, options?: StartSpanOptions): TracerSpan {
322
+ const spanId = `tracer:${name}:${globalThis.crypto.randomUUID()}`
323
+ const startTime = Date.now()
324
+ let ended = false
325
+ const attributes: Record<string, string | number | boolean> = {
326
+ ...options?.attributes,
327
+ }
328
+
329
+ // Note: Local adapter tracer spans don't have a jobId context,
330
+ // so they can't be stored in the database. They're essentially no-ops
331
+ // but provide a consistent API for code that needs a tracer.
332
+ // For actual metrics storage, use ctx.observe within action/step handlers.
333
+
334
+ const tracerSpan: TracerSpan = {
335
+ setAttribute(key: string, value: string | number | boolean): void {
336
+ if (!ended) {
337
+ attributes[key] = value
338
+ }
339
+ },
340
+
341
+ setAttributes(attrs: Record<string, string | number | boolean>): void {
342
+ if (!ended) {
343
+ Object.assign(attributes, attrs)
344
+ }
345
+ },
346
+
347
+ addEvent(eventName: string, eventAttrs?: Record<string, string | number | boolean>): void {
348
+ if (!ended) {
349
+ adapter.logger?.debug({ spanId, event: eventName, attributes: eventAttrs }, 'Tracer span event')
350
+ }
351
+ },
352
+
353
+ recordException(error: Error): void {
354
+ if (!ended) {
355
+ attributes['error.message'] = error.message
356
+ attributes['error.name'] = error.name
357
+ adapter.logger?.debug({ spanId, error: error.message }, 'Tracer span exception')
358
+ }
359
+ },
360
+
361
+ setStatusOk(): void {
362
+ if (!ended) {
363
+ // biome-ignore lint/complexity/useLiteralKeys: Index signature requires bracket notation
364
+ attributes['status'] = 'ok'
365
+ }
366
+ },
367
+
368
+ setStatusError(message?: string): void {
369
+ if (!ended) {
370
+ // biome-ignore lint/complexity/useLiteralKeys: Index signature requires bracket notation
371
+ attributes['status'] = 'error'
372
+ if (message) {
373
+ attributes['status.message'] = message
374
+ }
375
+ }
376
+ },
377
+
378
+ end(): void {
379
+ if (!ended) {
380
+ ended = true
381
+ const duration = Date.now() - startTime
382
+ adapter.logger?.debug(
383
+ { spanId, spanName, tracerName: name, durationMs: duration, attributes },
384
+ 'Tracer span ended',
385
+ )
386
+ }
387
+ },
388
+
389
+ isRecording(): boolean {
390
+ return !ended
391
+ },
392
+ }
393
+
394
+ adapter.logger?.debug({ spanId, spanName, tracerName: name }, 'Tracer span started')
395
+
396
+ return tracerSpan
397
+ },
398
+ }
399
+ }
307
400
  }
308
401
 
309
402
  /**
@@ -8,8 +8,41 @@ import {
8
8
  type StartJobSpanOptions,
9
9
  type StartStepSpanOptions,
10
10
  TelemetryAdapter,
11
+ type Tracer,
12
+ type TracerSpan,
11
13
  } from './adapter.js'
12
14
 
15
+ // ============================================================================
16
+ // Noop Tracer Span
17
+ // ============================================================================
18
+
19
+ const noopTracerSpan: TracerSpan = {
20
+ setAttribute() {
21
+ // No-op
22
+ },
23
+ setAttributes() {
24
+ // No-op
25
+ },
26
+ addEvent() {
27
+ // No-op
28
+ },
29
+ recordException() {
30
+ // No-op
31
+ },
32
+ setStatusOk() {
33
+ // No-op
34
+ },
35
+ setStatusError() {
36
+ // No-op
37
+ },
38
+ end() {
39
+ // No-op
40
+ },
41
+ isRecording() {
42
+ return false
43
+ },
44
+ }
45
+
13
46
  // ============================================================================
14
47
  // Noop Telemetry Adapter
15
48
  // ============================================================================
@@ -84,6 +117,19 @@ export class NoopTelemetryAdapter extends TelemetryAdapter {
84
117
  protected async _addSpanAttribute(_options: AddSpanAttributeOptions): Promise<void> {
85
118
  // No-op
86
119
  }
120
+
121
+ // ============================================================================
122
+ // Tracer Methods
123
+ // ============================================================================
124
+
125
+ protected _getTracer(name: string): Tracer {
126
+ return {
127
+ name,
128
+ startSpan(): TracerSpan {
129
+ return noopTracerSpan
130
+ },
131
+ }
132
+ }
87
133
  }
88
134
 
89
135
  /**