@pikku/kysely 0.13.10 → 0.13.11

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,25 @@
1
+ ## 0.13.11
2
+
3
+ ### Patch Changes
4
+
5
+ - f5ce870: Recover workflow runs stalled by a crash mid-dispatch.
6
+
7
+ Arming a step is two writes to two systems — the step row, then the queue or
8
+ scheduler job — so a process that died between them left a run `running` with
9
+ nothing in flight. It parked on a step that would never complete and never
10
+ error, so the run neither finished nor failed, and nothing swept it up.
11
+
12
+ `workflowService.recoverStalledRuns()` re-drives those runs through
13
+ `resumeWorkflow`. Replay is memoized per step, so resuming a run that was not
14
+ actually stuck changes nothing; runs mid-sleep or with a step in flight are
15
+ excluded outright. It is not self-starting — call it from a scheduled task.
16
+
17
+ Stores opt in by overriding `findStalledRunIds`; implemented here for the
18
+ Kysely and in-memory services, and a no-op elsewhere.
19
+
20
+ - Updated dependencies [f5ce870]
21
+ - @pikku/core@0.12.78
22
+
1
23
  ## 0.13.10
2
24
 
3
25
  ### Patch Changes
@@ -79,6 +79,7 @@ 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
+ protected findStalledRunIds(before: Date, limit: number): Promise<string[]>;
82
83
  getRunState(runId: string): Promise<Record<string, unknown>>;
83
84
  protected upsertWorkflowVersionImpl(name: string, graphHash: string, graph: any, source: string, status?: WorkflowVersionStatus): Promise<void>;
84
85
  protected updateWorkflowVersionStatusImpl(name: string, graphHash: string, status: WorkflowVersionStatus): Promise<void>;
@@ -464,6 +464,28 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
464
464
  .where('workflowRunId', '=', runId)
465
465
  .execute();
466
466
  }
467
+ async findStalledRunIds(before, limit) {
468
+ const rows = await this.db
469
+ .selectFrom('workflowRuns as r')
470
+ .select('r.workflowRunId')
471
+ .where('r.status', '=', 'running')
472
+ .where(({ not, exists, selectFrom }) => not(exists(selectFrom('workflowStep as s')
473
+ .select('s.workflowStepId')
474
+ .whereRef('s.workflowRunId', '=', 'r.workflowRunId')
475
+ .where('s.status', 'in', ['running', 'scheduled', 'suspended']))))
476
+ // The run row only moves on a status change, so idleness has to hold for
477
+ // the steps too — expressed as "no step touched since `before`" rather
478
+ // than max(updated_at) so it stays one index-friendly NOT EXISTS.
479
+ .where('r.updatedAt', '<', before)
480
+ .where(({ not, exists, selectFrom }) => not(exists(selectFrom('workflowStep as s')
481
+ .select('s.workflowStepId')
482
+ .whereRef('s.workflowRunId', '=', 'r.workflowRunId')
483
+ .where('s.updatedAt', '>=', before))))
484
+ .orderBy('r.updatedAt', 'asc')
485
+ .limit(limit)
486
+ .execute();
487
+ return rows.map((row) => row.workflowRunId);
488
+ }
467
489
  async getRunState(runId) {
468
490
  const row = await this.db
469
491
  .selectFrom('workflowRuns')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/kysely",
3
- "version": "0.13.10",
3
+ "version": "0.13.11",
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.77"
24
+ "@pikku/core": "^0.12.78"
25
25
  },
26
26
  "dependencies": {
27
27
  "kysely": "^0.29.0"
@@ -600,6 +600,45 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
600
600
  .execute()
601
601
  }
602
602
 
603
+ protected async findStalledRunIds(
604
+ before: Date,
605
+ limit: number
606
+ ): Promise<string[]> {
607
+ const rows = await this.db
608
+ .selectFrom('workflowRuns as r')
609
+ .select('r.workflowRunId')
610
+ .where('r.status', '=', 'running')
611
+ .where(({ not, exists, selectFrom }) =>
612
+ not(
613
+ exists(
614
+ selectFrom('workflowStep as s')
615
+ .select('s.workflowStepId')
616
+ .whereRef('s.workflowRunId', '=', 'r.workflowRunId')
617
+ .where('s.status', 'in', ['running', 'scheduled', 'suspended'])
618
+ )
619
+ )
620
+ )
621
+ // The run row only moves on a status change, so idleness has to hold for
622
+ // the steps too — expressed as "no step touched since `before`" rather
623
+ // than max(updated_at) so it stays one index-friendly NOT EXISTS.
624
+ .where('r.updatedAt', '<', before)
625
+ .where(({ not, exists, selectFrom }) =>
626
+ not(
627
+ exists(
628
+ selectFrom('workflowStep as s')
629
+ .select('s.workflowStepId')
630
+ .whereRef('s.workflowRunId', '=', 'r.workflowRunId')
631
+ .where('s.updatedAt', '>=', before)
632
+ )
633
+ )
634
+ )
635
+ .orderBy('r.updatedAt', 'asc')
636
+ .limit(limit)
637
+ .execute()
638
+
639
+ return rows.map((row) => row.workflowRunId)
640
+ }
641
+
603
642
  async getRunState(runId: string): Promise<Record<string, unknown>> {
604
643
  const row = await this.db
605
644
  .selectFrom('workflowRuns')