@pikku/kysely 0.13.11 → 0.13.12

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,30 @@
1
+ ## 0.13.12
2
+
3
+ ### Patch Changes
4
+
5
+ - e848eb2: Add `relayUndispatchedSteps()`, which re-drives steps whose queue or scheduler
6
+ dispatch was lost.
7
+
8
+ Arming a step is two writes to two systems — the step row lands `pending`, then
9
+ a job is published — and nothing spans both, so a crash in between leaves a
10
+ durable row nothing will ever pick up. The run then neither finishes nor fails.
11
+
12
+ The step row is the outbox record and this is the relay. It is safe to
13
+ re-dispatch a step that already has a live job because `executeWorkflowStepInner`
14
+ claims the step under `withStepLock` before invoking anything: the loser reads
15
+ `running` and returns. Stores opt in by overriding `findUndispatchedSteps`; the
16
+ default returns nothing, so a store without an atomic step lock gains no
17
+ re-dispatches. Opted in: `kysely-postgres` and `kysely-mysql` (real locks) and
18
+ in-memory (inline, single-process, no queues). Not opted in: `mongodb` and
19
+ `kysely-sqlite`, whose `withStepLock` is a pass-through.
20
+
21
+ Not self-starting — call it from a scheduled task.
22
+
23
+ - Updated dependencies [e848eb2]
24
+ - Updated dependencies [b170489]
25
+ - Updated dependencies [ae4e898]
26
+ - @pikku/core@0.12.79
27
+
1
28
  ## 0.13.11
2
29
 
3
30
  ### Patch Changes
@@ -79,6 +79,20 @@ export declare class KyselyWorkflowService extends PikkuWorkflowService {
79
79
  */
80
80
  private jsonPathFor;
81
81
  protected updateRunStateImpl(runId: string, name: string, value: unknown): Promise<void>;
82
+ /**
83
+ * The undispatched-step query, shared by the dialects that can safely relay.
84
+ *
85
+ * Deliberately NOT an override of `findUndispatchedSteps`: this class's
86
+ * `withStepLock` is a pass-through, so a subclass inheriting it (kysely-sqlite)
87
+ * has no atomic claim, and the relay's redundant dispatches would become
88
+ * double executions. A dialect opts in by overriding `findUndispatchedSteps`
89
+ * to call this — which `kysely-postgres` and `kysely-mysql` do, having real
90
+ * locks.
91
+ */
92
+ protected queryUndispatchedSteps(before: Date, limit: number): Promise<Array<{
93
+ runId: string;
94
+ stepId: string;
95
+ }>>;
82
96
  protected findStalledRunIds(before: Date, limit: number): Promise<string[]>;
83
97
  getRunState(runId: string): Promise<Record<string, unknown>>;
84
98
  protected upsertWorkflowVersionImpl(name: string, graphHash: string, graph: any, source: string, status?: WorkflowVersionStatus): Promise<void>;
@@ -464,6 +464,34 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
464
464
  .where('workflowRunId', '=', runId)
465
465
  .execute();
466
466
  }
467
+ /**
468
+ * The undispatched-step query, shared by the dialects that can safely relay.
469
+ *
470
+ * Deliberately NOT an override of `findUndispatchedSteps`: this class's
471
+ * `withStepLock` is a pass-through, so a subclass inheriting it (kysely-sqlite)
472
+ * has no atomic claim, and the relay's redundant dispatches would become
473
+ * double executions. A dialect opts in by overriding `findUndispatchedSteps`
474
+ * to call this — which `kysely-postgres` and `kysely-mysql` do, having real
475
+ * locks.
476
+ */
477
+ async queryUndispatchedSteps(before, limit) {
478
+ const rows = await this.db
479
+ .selectFrom('workflowStep as s')
480
+ .innerJoin('workflowRuns as r', 'r.workflowRunId', 's.workflowRunId')
481
+ .select(['s.workflowStepId', 's.workflowRunId'])
482
+ .where('s.status', '=', 'pending')
483
+ .where('s.updatedAt', '<', before)
484
+ // Leftover `pending` steps on a settled run are not work; only a live run
485
+ // still has anything to dispatch.
486
+ .where('r.status', '=', 'running')
487
+ .orderBy('s.updatedAt', 'asc')
488
+ .limit(limit)
489
+ .execute();
490
+ return rows.map((row) => ({
491
+ runId: row.workflowRunId,
492
+ stepId: row.workflowStepId,
493
+ }));
494
+ }
467
495
  async findStalledRunIds(before, limit) {
468
496
  const rows = await this.db
469
497
  .selectFrom('workflowRuns as r')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/kysely",
3
- "version": "0.13.11",
3
+ "version": "0.13.12",
4
4
  "description": "Kysely-backed Pikku services — workflow state, secrets, sessions, scopes and audit",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "MIT",
@@ -21,7 +21,7 @@
21
21
  "prepublishOnly": "yarn build"
22
22
  },
23
23
  "peerDependencies": {
24
- "@pikku/core": "^0.12.78"
24
+ "@pikku/core": "^0.12.79"
25
25
  },
26
26
  "dependencies": {
27
27
  "kysely": "^0.29.0"
@@ -600,6 +600,39 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
600
600
  .execute()
601
601
  }
602
602
 
603
+ /**
604
+ * The undispatched-step query, shared by the dialects that can safely relay.
605
+ *
606
+ * Deliberately NOT an override of `findUndispatchedSteps`: this class's
607
+ * `withStepLock` is a pass-through, so a subclass inheriting it (kysely-sqlite)
608
+ * has no atomic claim, and the relay's redundant dispatches would become
609
+ * double executions. A dialect opts in by overriding `findUndispatchedSteps`
610
+ * to call this — which `kysely-postgres` and `kysely-mysql` do, having real
611
+ * locks.
612
+ */
613
+ protected async queryUndispatchedSteps(
614
+ before: Date,
615
+ limit: number
616
+ ): Promise<Array<{ runId: string; stepId: string }>> {
617
+ const rows = await this.db
618
+ .selectFrom('workflowStep as s')
619
+ .innerJoin('workflowRuns as r', 'r.workflowRunId', 's.workflowRunId')
620
+ .select(['s.workflowStepId', 's.workflowRunId'])
621
+ .where('s.status', '=', 'pending')
622
+ .where('s.updatedAt', '<', before)
623
+ // Leftover `pending` steps on a settled run are not work; only a live run
624
+ // still has anything to dispatch.
625
+ .where('r.status', '=', 'running')
626
+ .orderBy('s.updatedAt', 'asc')
627
+ .limit(limit)
628
+ .execute()
629
+
630
+ return rows.map((row) => ({
631
+ runId: row.workflowRunId,
632
+ stepId: row.workflowStepId,
633
+ }))
634
+ }
635
+
603
636
  protected async findStalledRunIds(
604
637
  before: Date,
605
638
  limit: number