mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-24 02:09:26 +08:00
fix(core): Stop SQLite reusing durable scheduler row ids (no-changelog)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a72a36dcde
commit
c9a3eb7c24
+1
-1
@@ -6,7 +6,7 @@
|
||||
<summary><strong>Table Definition</strong></summary>
|
||||
|
||||
```sql
|
||||
CREATE TABLE "scheduled_job" ("id" integer PRIMARY KEY NOT NULL, "name" varchar(255) NOT NULL, "taskType" varchar(128) NOT NULL, "payload" text NOT NULL DEFAULT ('{}'), "kind" varchar(16) NOT NULL, "cronExpression" varchar(255), "timezone" varchar(64), "intervalSeconds" integer, "fireAt" datetime(3), "enabled" boolean NOT NULL DEFAULT (true), "nextRunAt" datetime(3), "lastFiredAt" datetime(3), "maxAttempts" integer NOT NULL DEFAULT (1), "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "recurrenceUnit" varchar(16), "recurrenceSize" int, "misfirePolicy" varchar(16) NOT NULL DEFAULT ('coalesce'), "misfireGraceSeconds" int NOT NULL DEFAULT (60), "ownerType" varchar(32) NOT NULL, "ownerId" varchar(255) NOT NULL, "ownerMemberId" varchar(36), "orphanedAt" datetime(3), "concurrencyLimit" int, "timeoutSeconds" int NOT NULL DEFAULT (300), "leaseSeconds" int, CONSTRAINT "CHK_scheduled_job_concurrencyLimit" CHECK (("concurrencyLimit" IS NULL OR ("concurrencyLimit" >= 1 AND "concurrencyLimit" <= 2147483647 AND CAST("concurrencyLimit" AS INTEGER) = "concurrencyLimit"))), CONSTRAINT "CHK_scheduled_job_timeoutSeconds" CHECK (("timeoutSeconds" > 0 AND "timeoutSeconds" <= 2147483 AND CAST("timeoutSeconds" AS INTEGER) = "timeoutSeconds")), CONSTRAINT "CHK_scheduled_job_leaseSeconds" CHECK (("leaseSeconds" > 0)), CONSTRAINT "CHK_scheduled_job_misfireGraceSeconds" CHECK (((((("misfireGraceSeconds" > 0)))))), CONSTRAINT "CHK_scheduled_job_recurrence_unit" CHECK ((((((("recurrenceUnit" IN ('hours', 'days', 'weeks', 'months')))))))), CONSTRAINT "CHK_scheduled_job_recurrence_size" CHECK ((((((("recurrenceSize" >= 2))))))), CONSTRAINT "CHK_scheduled_job_cron_expression" CHECK ((((((("kind" <> 'cron' OR "cronExpression" IS NOT NULL))))))), CONSTRAINT "CHK_scheduled_job_interval_seconds" CHECK ((((((("kind" <> 'interval' OR "intervalSeconds" IS NOT NULL))))))), CONSTRAINT "CHK_scheduled_job_fire_at" CHECK ((((((("kind" <> 'one_off' OR "fireAt" IS NOT NULL))))))), CONSTRAINT "CHK_scheduled_job_recurring_cron" CHECK (((((("kind" <> 'recurring_cron' OR ("cronExpression" IS NOT NULL AND "recurrenceUnit" IS NOT NULL AND "recurrenceSize" IS NOT NULL))))))), CONSTRAINT "CHK_scheduled_job_misfirePolicy" CHECK ((((("misfirePolicy" IN ('coalesce', 'skip', 'coalesce_owner')))))), CONSTRAINT "CHK_scheduled_job_kind" CHECK ("kind" IN ('cron', 'interval', 'one_off', 'recurring_cron', 'hold')))
|
||||
CREATE TABLE "scheduled_job" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255) NOT NULL, "taskType" varchar(128) NOT NULL, "payload" text NOT NULL DEFAULT ('{}'), "kind" varchar(16) NOT NULL, "cronExpression" varchar(255), "timezone" varchar(64), "intervalSeconds" integer, "fireAt" datetime(3), "enabled" boolean NOT NULL DEFAULT (true), "nextRunAt" datetime(3), "lastFiredAt" datetime(3), "maxAttempts" integer NOT NULL DEFAULT (1), "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "recurrenceUnit" varchar(16), "recurrenceSize" int, "misfirePolicy" varchar(16) NOT NULL DEFAULT ('coalesce'), "misfireGraceSeconds" int NOT NULL DEFAULT (60), "ownerType" varchar(32) NOT NULL, "ownerId" varchar(255) NOT NULL, "ownerMemberId" varchar(36), "orphanedAt" datetime(3), "concurrencyLimit" int, "timeoutSeconds" int NOT NULL DEFAULT (300), "leaseSeconds" int, CONSTRAINT "CHK_scheduled_job_concurrencyLimit" CHECK (("concurrencyLimit" IS NULL OR ("concurrencyLimit" >= 1 AND "concurrencyLimit" <= 2147483647 AND CAST("concurrencyLimit" AS INTEGER) = "concurrencyLimit"))), CONSTRAINT "CHK_scheduled_job_timeoutSeconds" CHECK (("timeoutSeconds" > 0 AND "timeoutSeconds" <= 2147483 AND CAST("timeoutSeconds" AS INTEGER) = "timeoutSeconds")), CONSTRAINT "CHK_scheduled_job_leaseSeconds" CHECK (("leaseSeconds" > 0)), CONSTRAINT "CHK_scheduled_job_misfireGraceSeconds" CHECK (((((("misfireGraceSeconds" > 0)))))), CONSTRAINT "CHK_scheduled_job_recurrence_unit" CHECK ((((((("recurrenceUnit" IN ('hours', 'days', 'weeks', 'months')))))))), CONSTRAINT "CHK_scheduled_job_recurrence_size" CHECK ((((((("recurrenceSize" >= 2))))))), CONSTRAINT "CHK_scheduled_job_cron_expression" CHECK ((((((("kind" <> 'cron' OR "cronExpression" IS NOT NULL))))))), CONSTRAINT "CHK_scheduled_job_interval_seconds" CHECK ((((((("kind" <> 'interval' OR "intervalSeconds" IS NOT NULL))))))), CONSTRAINT "CHK_scheduled_job_fire_at" CHECK ((((((("kind" <> 'one_off' OR "fireAt" IS NOT NULL))))))), CONSTRAINT "CHK_scheduled_job_recurring_cron" CHECK (((((("kind" <> 'recurring_cron' OR ("cronExpression" IS NOT NULL AND "recurrenceUnit" IS NOT NULL AND "recurrenceSize" IS NOT NULL))))))), CONSTRAINT "CHK_scheduled_job_misfirePolicy" CHECK ((((("misfirePolicy" IN ('coalesce', 'skip', 'coalesce_owner')))))), CONSTRAINT "CHK_scheduled_job_kind" CHECK ("kind" IN ('cron', 'interval', 'one_off', 'recurring_cron', 'hold')))
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<summary><strong>Table Definition</strong></summary>
|
||||
|
||||
```sql
|
||||
CREATE TABLE "scheduled_task" ("id" integer PRIMARY KEY NOT NULL, "jobId" integer NOT NULL, "taskType" varchar(128) NOT NULL, "payload" text NOT NULL DEFAULT ('{}'), "scheduledFor" datetime(3) NOT NULL, "runAt" datetime(3) NOT NULL, "status" varchar(16) NOT NULL DEFAULT ('pending'), "attempts" integer NOT NULL DEFAULT (0), "maxAttempts" integer NOT NULL DEFAULT (1), "claimedBy" varchar(255), "leaseExpiresAt" datetime(3), "leaseEpoch" integer NOT NULL DEFAULT (0), "startedAt" datetime(3), "finishedAt" datetime(3), "errorMessage" text, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "dispatchedAt" datetime(3), "missedAfter" datetime(3), "timeoutSeconds" int NOT NULL DEFAULT 300 CONSTRAINT "CHK_scheduled_task_timeoutSeconds" CHECK ("timeoutSeconds" > 0 AND "timeoutSeconds" <= 2147483 AND CAST("timeoutSeconds" AS INTEGER) = "timeoutSeconds"), "leaseSeconds" int CONSTRAINT "CHK_scheduled_task_leaseSeconds" CHECK ("leaseSeconds" > 0), "mode" varchar(16) NOT NULL DEFAULT 'once' CONSTRAINT "CHK_scheduled_task_mode" CHECK ("mode" IN ('once', 'hold')), "heldSince" datetime(3), CONSTRAINT "CHK_scheduled_task_running_lease" CHECK ("status" <> 'running' OR "leaseExpiresAt" IS NOT NULL), CONSTRAINT "CHK_scheduled_task_status" CHECK ("status" IN ('pending', 'running', 'succeeded', 'failed', 'missed', 'cancelled')), CONSTRAINT "FK_scheduled_task_jobId" FOREIGN KEY ("jobId") REFERENCES "scheduled_job" ("id") ON DELETE CASCADE)
|
||||
CREATE TABLE "scheduled_task" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "jobId" integer NOT NULL, "taskType" varchar(128) NOT NULL, "payload" text NOT NULL DEFAULT ('{}'), "scheduledFor" datetime(3) NOT NULL, "runAt" datetime(3) NOT NULL, "status" varchar(16) NOT NULL DEFAULT ('pending'), "attempts" integer NOT NULL DEFAULT (0), "maxAttempts" integer NOT NULL DEFAULT (1), "claimedBy" varchar(255), "leaseExpiresAt" datetime(3), "leaseEpoch" integer NOT NULL DEFAULT (0), "startedAt" datetime(3), "finishedAt" datetime(3), "errorMessage" text, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "dispatchedAt" datetime(3), "missedAfter" datetime(3), "timeoutSeconds" int NOT NULL DEFAULT 300 CONSTRAINT "CHK_scheduled_task_timeoutSeconds" CHECK ("timeoutSeconds" > 0 AND "timeoutSeconds" <= 2147483 AND CAST("timeoutSeconds" AS INTEGER) = "timeoutSeconds"), "leaseSeconds" int CONSTRAINT "CHK_scheduled_task_leaseSeconds" CHECK ("leaseSeconds" > 0), "mode" varchar(16) NOT NULL DEFAULT 'once' CONSTRAINT "CHK_scheduled_task_mode" CHECK ("mode" IN ('once', 'hold')), "heldSince" datetime(3), CONSTRAINT "CHK_scheduled_task_running_lease" CHECK ("status" <> 'running' OR "leaseExpiresAt" IS NOT NULL), CONSTRAINT "CHK_scheduled_task_status" CHECK ("status" IN ('pending', 'running', 'succeeded', 'failed', 'missed', 'cancelled')), CONSTRAINT "FK_scheduled_task_jobId" FOREIGN KEY ("jobId") REFERENCES "scheduled_job" ("id") ON DELETE CASCADE)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { UnexpectedError } from 'n8n-workflow';
|
||||
|
||||
import type { MigrationContext, ReversibleMigration } from '../migration-types';
|
||||
|
||||
const tables = ['scheduled_job', 'scheduled_task'];
|
||||
const rowidKey = '"id" integer PRIMARY KEY NOT NULL';
|
||||
const autoincrementKey = '"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL';
|
||||
|
||||
/**
|
||||
* Rebuilds the scheduler tables with AUTOINCREMENT keys. A plain rowid table gives a
|
||||
* new row the id of a deleted highest row, so a stale lease holder could match it.
|
||||
*/
|
||||
export class AutoincrementSchedulerIds1792761455090 implements ReversibleMigration {
|
||||
// The rebuild drops scheduled_job. Disable foreign keys to preserve scheduled_task rows.
|
||||
withFKsDisabled = true as const;
|
||||
|
||||
async up(context: MigrationContext) {
|
||||
for (const table of tables) await this.rebuild(context, table, rowidKey, autoincrementKey);
|
||||
}
|
||||
|
||||
async down(context: MigrationContext) {
|
||||
for (const table of tables) await this.rebuild(context, table, autoincrementKey, rowidKey);
|
||||
}
|
||||
|
||||
private async rebuild(
|
||||
{ runQuery, escape, tablePrefix }: MigrationContext,
|
||||
table: string,
|
||||
fromKey: string,
|
||||
toKey: string,
|
||||
) {
|
||||
const name = `${tablePrefix}${table}`;
|
||||
const tableName = escape.tableName(table);
|
||||
const temporaryName = escape.tableName(`temporary_${table}`);
|
||||
const [{ sql: definition }] = await runQuery<Array<{ sql: string }>>(
|
||||
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = :name",
|
||||
{ name },
|
||||
);
|
||||
const indexes = await runQuery<Array<{ sql: string }>>(
|
||||
"SELECT sql FROM sqlite_master WHERE type = 'index' AND tbl_name = :name AND sql IS NOT NULL",
|
||||
{ name },
|
||||
);
|
||||
|
||||
const header = `CREATE TABLE ${tableName} (`;
|
||||
if (!definition.startsWith(header) || definition.split(fromKey).length !== 2) {
|
||||
throw new UnexpectedError(`Unexpected definition of table ${name}`, {
|
||||
extra: { definition },
|
||||
});
|
||||
}
|
||||
|
||||
await runQuery(
|
||||
definition.replace(header, `CREATE TABLE ${temporaryName} (`).replace(fromKey, toKey),
|
||||
);
|
||||
await runQuery(`INSERT INTO ${temporaryName} SELECT * FROM ${tableName}`);
|
||||
await runQuery(`DROP TABLE ${tableName}`);
|
||||
await runQuery(`ALTER TABLE ${temporaryName} RENAME TO ${tableName}`);
|
||||
for (const { sql } of indexes) await runQuery(sql);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user