world-postgres 4.3.x (latest): close() can resolve while a job is still running — fixed on main, not on the 4.x line
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 2/5
- Thời gian dự kiến
- 1-3 giờ
- Mức phù hợp với người mới
- 74/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- typescript
- Lĩnh vực
- backend
Hướng nghiên cứu
Bắt đầu trong dist/queue.js và so sánh đường dẫn shutdown của 4.3.x với phần triển khai tương ứng trên main. Chạy bản tái hiện Graphile-layer được cung cấp bằng một DATABASE_URL có role sở hữu CREATEDB, sau đó xác minh rằng shutdown vẫn ở trạng thái chờ cho đến khi job đang hoạt động hoàn tất và hàng của nó được xóa.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Summary
On the latest release line, PostgresWorld.close() can resolve while a step job is still running, so the application tears down its pool (and exits) underneath a job that has not yet acknowledged completion. The cause is the interaction between Graphile's own signal handlers and runner.stop().
This is already fixed on main. Both fixes are present in the published 5.0.0-beta.44 and absent from 4.3.7, which is what latest currently points at. The ask is a backport to the 4.3.x line, since 5.0 is still beta and moving the whole SDK ([email protected] → 5.0.0-beta.53) is a much larger step than the shutdown fix warrants.
The race on 4.3.7
dist/queue.js calls Graphile without noHandleSignals, so Graphile installs its own SIGTERM/SIGINT handlers:
runner = await run({
pgPool: pool,
concurrency: config.queueConcurrency || 10,
logger: graphileLogger,
pollInterval: 500,
taskList,
});
and close() does not await the pool after stopping the runner:
async close() {
closing = true;
// ...
if (runner) {
await runner.stop();
runner = null;
}
if (workerUtils) {
await workerUtils.release();
workerUtils = null;
}
// ...
}
In [email protected] (the pinned version), stop() only awaits the worker pool when it is still active:
const promises = [];
if (cron._active) {
promises.push(cron.release());
}
if (workerPool._active) {
promises.push(workerPool.gracefulShutdown());
}
await Promise.all(promises).then(release);
So on SIGTERM the ordering is:
- Graphile's own handler starts
workerPool.gracefulShutdown(), which clears_activebefore in-flight jobs finish. - The application calls
world.close()→runner.stop(). _activeis already false, sostop()awaits nothing, runsrelease()and resolves.close()continues:workerUtils.release(), pool teardown, process exit — while a job still needs a connection to persist its result.
The window is small but real, and it is exactly the case an application-managed shutdown is supposed to cover: the whole point of awaiting world.close() is that it means "jobs are done".
Reproduction
At the Graphile layer, with the same options world-postgres passes. Needs a DATABASE_URL whose role has CREATEDB; it creates and drops a disposable database. On [email protected] this prints the failure; with either upstream fix applied it prints PASS.
const { Pool } = require("pg");
const { run } = require("graphile-worker");
const verify = async () => {
const connectionString = process.env.DATABASE_URL;
if (!connectionString) throw new Error("DATABASE_URL with CREATEDB is required");
const databaseName = `gw_shutdown_check_${process.pid}_${Date.now()}`;
const admin = new Pool({ connectionString, connectionTimeoutMillis: 3000 });
const entered = Promise.withResolvers();
const release = Promise.withResolvers();
const originalKill = process.kill;
let taskPool, runner, stopping, created = false, jobFinished = false;
try {
await admin.query(`CREATE DATABASE "${databaseName}"`);
created = true;
const databaseUrl = new URL(connectionString);
databaseUrl.pathname = `/${databaseName}`;
taskPool = new Pool({ connectionString: databaseUrl.toString(), connectionTimeoutMillis: 3000 });
runner = await run({
concurrency: 1,
pgPool: taskPool,
pollInterval: 50,
taskList: {
hold: async () => {
entered.resolve();
await release.promise;
jobFinished = true;
},
},
});
await runner.addJob("hold", {});
await entered.promise;
// Deliver the signal Graphile is listening for, but keep this process alive.
process.kill = (pid, signal) => (pid === process.pid ? true : originalKill(pid, signal));
process.emit("SIGTERM", "SIGTERM");
let stopResolved = false;
stopping = runner.stop().then(() => { stopResolved = true; });
await new Promise((done) => setTimeout(done, 100));
if (stopResolved || jobFinished) {
throw new Error("Runner stopped before its active job finished");
}
release.resolve();
await stopping;
await runner.promise;
const result = await taskPool.query("SELECT count(*)::int AS count FROM graphile_worker.jobs");
if (!jobFinished || result.rows[0].count !== 0) {
throw new Error("Job completion was not acknowledged before stop resolved");
}
console.log("PASS: shutdown waited for the job and its acknowledgement");
} finally {
release.resolve();
if (runner) { await (stopping ?? runner.stop()); await runner.promise; }
await taskPool?.end();
if (created) await admin.query(`DROP DATABASE "${databaseName}"`);
await admin.end();
process.kill = originalKill;
}
};
verify().catch((error) => { console.error(error.message); process.exitCode = 1; });
Expected: the runner stays pending until the job completes and its row is gone.
Actual on 4.3.7's dependency set: Runner stopped before its active job finished.
The fix that already exists on main
5.0.0-beta.44, dist/queue.js:
runner = await run({
pgPool: pool,
concurrency: config.queueConcurrency || 50,
logger: graphileLogger,
...(config.applicationManagedShutdown === true && {
noHandleSignals: true,
}),
pollInterval: 500,
taskList,
});
const activeRunner = runner;
if (activeRunner) {
try {
await activeRunner.stop();
} catch (error) { /* "Runner is already stopped" */ }
await activeRunner.promise.catch(() => {});
runner = null;
}
Either change alone closes the race. Note that 5.0.0-beta.44 still pins [email protected], so this is a World-level fix and not something a Graphile bump would deliver.
Request
Backport one or both to 4.3.x. await runner.promise after runner.stop() is the smaller of the two and is safe regardless of who owns the signal, so it would be enough on its own.
If a backport is not planned, it would help to say so — then the answer for self-hosted 4.x users is "pin a patch until 5.0 is stable", which is what we are doing (a three-line patch adding promises.push(workerPool.promise) to Graphile's stop()), and knowing that is the intended state is useful.
Environment
@workflow/[email protected](latest),[email protected],[email protected]- Next.js 16 App Router, Bun 1.4.2 (
bun --bun next start), self-hosted on Kubernetes - Application-managed shutdown: the HTTP server owns SIGTERM and drains, then awaits
world.close()before exiting
- Ngôn ngữ chính
- TypeScript
- Star
- 2.5k
- Fork
- 385
- Merge trung bình
- 1 ngày 5 giờ
- Pull request đã merge (30 ngày)
- 167
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Không có hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của vercel/workflow
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
Maintainer thường phản hồi trong vòng 1 ngày
-
node-module-esbuild-plugin: getImportedIdentifier only checks the first named import when locating a violationCó thể đã có người làm @FelixFangX đã nhận 4 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
vercel/workflow#5057 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Feature: Expose isLastAttempt/maxAttempts in step metadataCó thể đã có người làm @joelfickson đã nhận 5 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
vercel/workflow#4979 · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
WorkflowServerReadableStream leaks the connection when cancelled before streams.get() resolvesCó thể đã có người làm @bluepegasus123 đã nhận 4 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
vercel/workflow#4644 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
world-vercel waitForTerminalStatus drops the abort signal on its fallback readCó thể đã có người làm @bluepegasus123 đã nhận 4 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
vercel/workflow#4643 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của vercel/workflow
Issue tương tự
-
bug
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 86/100
awslabs/visual-asset-management-system#937 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
t0ggles
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 82/100
nolimits4web/swiper#8255 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 15 ngày
-
render: --variables-file (and --variables <path>) fails with "io.readFile is not a function" since 0.8.212Có thể đã có người làm @miguel-heygen đã nhận hôm nay. Đang mởbug triage/needs-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
heygen-com/hyperframes#6830 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug launch local use
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
Maintainer thường phản hồi trong vòng 1 ngày
-
account: profile bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 5 ngày