Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/add-partitions-retrofit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,42 @@ describe("Pgslice.addPartitions (retrofit)", () => {
]);
});

test("defaults to a 5s lock_timeout for the add_partitions transaction", async ({
pgslice,
transaction,
}) => {
await pgslice.addPartitions(transaction, { table: "posts", future: 1 });

// addPartitions opens its own (sub)transaction; passing this test's
// transaction makes that a SAVEPOINT, and a released savepoint's SET LOCAL
// propagates to the parent — so reading it here proves it was applied
// inside the add_partitions transaction that ran the CREATE ... PARTITION OF.
const result = await transaction.one(
sql.type(z.object({ lock_timeout: z.string() }))`
SHOW lock_timeout
`,
);
expect(result.lock_timeout).toBe("5s");
});

test("honors a custom lockTimeout override", async ({
pgslice,
transaction,
}) => {
await pgslice.addPartitions(transaction, {
table: "posts",
future: 1,
lockTimeout: "10s",
});

const result = await transaction.one(
sql.type(z.object({ lock_timeout: z.string() }))`
SHOW lock_timeout
`,
);
expect(result.lock_timeout).toBe("10s");
});

test("new partitions inherit the parent composite key and gain no duplicate PK", async ({
pgslice,
transaction,
Expand Down
5 changes: 5 additions & 0 deletions src/commands/add-partitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export class AddPartitionsCommand extends BaseCommand {
description:
"Copy the parent table's privileges onto each new partition (default: true; disable with --no-inherit-grants)",
});
lockTimeout = Option.String("--lock-timeout", "5s", {
description:
"How long each partition-creation statement waits on the parent's lock before backing off (default: 5s)",
});

override async perform(pgslice: Pgslice): Promise<void> {
const created = await pgslice.start(async (tx) =>
Expand All @@ -60,6 +64,7 @@ export class AddPartitionsCommand extends BaseCommand {
future: this.future,
tablespace: this.tablespace,
inheritGrants: this.inheritGrants,
lockTimeout: this.lockTimeout,
}),
);

Expand Down
5 changes: 5 additions & 0 deletions src/commands/maintain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ export class MaintainCommand extends BaseCommand {
description:
"Copy each parent table's privileges onto its new partitions (default: true; disable with --no-inherit-grants)",
});
lockTimeout = Option.String("--lock-timeout", "5s", {
description:
"How long each partition-creation statement waits on a table's lock before backing off (default: 5s)",
});

override async perform(pgslice: Pgslice): Promise<number | void> {
// maintain emits structured records (start, per-table, final); write each as
Expand All @@ -98,6 +102,7 @@ export class MaintainCommand extends BaseCommand {
schema: this.schema,
tablespace: this.tablespace,
inheritGrants: this.inheritGrants,
lockTimeout: this.lockTimeout,
},
log,
),
Expand Down
39 changes: 39 additions & 0 deletions src/maintain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,45 @@ describe("Pgslice.maintain (fleet)", () => {
);
expect(grants.map((g) => g.privilege)).toContain("SELECT");
});

test("forwards a custom lockTimeout to the add_partitions transaction", async ({
pgslice,
transaction,
}) => {
await nativeParent(
transaction,
"posts",
"created_at",
TS,
"month",
"date",
);
await addChild(
transaction,
"posts",
"posts_y2026m01",
"2026-01-01",
"2026-02-01",
);

await pgslice.maintain(transaction, {
futureDaily: 1,
futureWeekly: 1,
futureMonthly: 1,
futureYearly: 1,
lockTimeout: "10s",
});

// addPartitions runs in a savepoint whose released SET LOCAL propagates to
// this transaction, so a "10s" reading here proves maintain forwarded the
// override all the way through to the partition-creation statements.
const result = await transaction.one(
sql.type(z.object({ lock_timeout: z.string() }))`
SHOW lock_timeout
`,
);
expect(result.lock_timeout).toBe("10s");
});
});

describe("classic pgslice tables", () => {
Expand Down
12 changes: 12 additions & 0 deletions src/pgslice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,17 @@ export class Pgslice {
// partitions against the existing ones under a non-UTC session.
await tx.query(sql.typeAlias("void")`SET LOCAL TIME ZONE 'UTC'`);

// Bound how long the CREATE ... PARTITION OF statements may wait on the
// parent's lock, so a scheduled run backs off instead of blocking
// writers queued behind it. On timeout the statement errors, the
// transaction rolls back, and the table is reported as a failed
// extension to be retried on the next run.
await tx.query(
sql.typeAlias(
"void",
)`SET LOCAL lock_timeout = ${sql.literalValue(options.lockTimeout ?? "5s")}`,
);

const targetTable = options.intermediate
? originalTable.intermediate
: originalTable;
Expand Down Expand Up @@ -588,6 +599,7 @@ export class Pgslice {
future: futureByPeriod[period],
tablespace: options.tablespace,
inheritGrants: options.inheritGrants,
lockTimeout: options.lockTimeout,
now,
});

Expand Down
12 changes: 12 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ export interface AddPartitionsOptions {
* period boundary uses a consistent horizon for every table.
*/
now?: Date;
/**
* How long each partition-creation statement may wait on the parent's lock
* before backing off, rather than blocking writers queued behind it. Any
* Postgres interval literal (e.g. "5s"). Defaults to "5s".
*/
lockTimeout?: string;
}

import type { MirroringTargetType } from "./mirroring.js";
Expand Down Expand Up @@ -256,6 +262,12 @@ export interface MaintainOptions {
schema?: string;
tablespace?: string;
inheritGrants?: boolean;
/**
* How long each partition-creation statement may wait on a table's lock
* before backing off, rather than blocking writers queued behind it. Any
* Postgres interval literal (e.g. "5s"). Defaults to "5s".
*/
lockTimeout?: string;
/**
* The reference "now" for choosing each table's partition horizon. Defaults
* to a single instant captured when the run starts, so a fleet run that
Expand Down
Loading