Describe the issue
ctx.releaseSlot() succeeds but leaves the task's row in v1_task_runtime_slot. On a worker with one regular slot, another task cannot start until the released task completes or fails.
Environment
- TypeScript SDK
1.31.1.
- Local self-hosted
hatchet-lite-dev:v0.106.8, PostgreSQL 17.
Expected behavior
As described in Manual Slot Release, another task should be able to use the released slot while the original task remains running.
Reproduction and results
Register two tasks on a worker configured with slots: 1. Task A calls await ctx.releaseSlot(), submits task B, then sleeps for 6 s. Repeat on retries, giving each submitted child a distinct childKey.
The RPC succeeds. Immediately afterward, v1_task_runtime.worker_id is NULL, but v1_task_runtime_slot still has the original worker ID and units = 1.
| Release path |
Initial attempt: B starts after release |
Retry 1 |
Retry 2 |
B starts before A finishes? |
| SDK RPC |
8.267 s |
6.389 s |
7.486 s |
No |
| Corrected SQL, executed directly |
55 ms |
1.514 s |
1.544 s |
Yes |
A no-release control also kept B waiting until A finished. The corrected-SQL comparison uses the same engine and worker, but executes the candidate query directly against the local database; it is not a rebuilt-engine test.
Cause and proposed fix
In ManualSlotRelease, the final UPDATE depends on task, while slot deletion depends on locked_runtime, a SELECT ... FOR UPDATE. The UPDATE can modify the runtime row before the locking CTE executes. That locking read then skips the row modified by the same command, so slot deletion finds no rows.
Make the UPDATE depend on the locking CTE:
UPDATE
v1_task_runtime
SET
worker_id = NULL
FROM
- task
+ locked_runtime
WHERE
- (v1_task_runtime.task_id, v1_task_runtime.task_inserted_at, v1_task_runtime.retry_count) IN (SELECT id, inserted_at, retry_count FROM task)
+ (v1_task_runtime.task_id, v1_task_runtime.task_inserted_at, v1_task_runtime.retry_count) IN (SELECT task_id, task_inserted_at, retry_count FROM locked_runtime)
RETURNING
v1_task_runtime.*;
The generated query in pkg/repository/sqlcv1/tasks.sql.go would also need regeneration. The same problematic query was present in main when checked.
Here is a standalone SQL reproduction. It uses temporary tables and rolls back:
BEGIN;
CREATE TEMP TABLE repro_runtime (id int PRIMARY KEY, worker_id int);
CREATE TEMP TABLE repro_slot (task_id int);
INSERT INTO repro_runtime VALUES (1, 42);
INSERT INTO repro_slot VALUES (1);
WITH locked_runtime AS (
SELECT id FROM repro_runtime WHERE id = 1 FOR UPDATE
), deleted_slots AS (
DELETE FROM repro_slot
WHERE task_id IN (SELECT id FROM locked_runtime)
RETURNING task_id
)
UPDATE repro_runtime SET worker_id = NULL WHERE id = 1;
TABLE repro_slot; -- Incorrectly retains task_id = 1.
UPDATE repro_runtime SET worker_id = 42 WHERE id = 1;
WITH locked_runtime AS (
SELECT id FROM repro_runtime WHERE id = 1 FOR UPDATE
), deleted_slots AS (
DELETE FROM repro_slot
WHERE task_id IN (SELECT id FROM locked_runtime)
RETURNING task_id
)
UPDATE repro_runtime SET worker_id = NULL
WHERE id IN (SELECT id FROM locked_runtime);
TABLE repro_slot; -- Empty, as expected.
ROLLBACK;
AI disclosure
OpenCode with GPT-6 Astra assisted with investigation, running the local SDK and SQL reproductions, and drafting this report and candidate fix.
Describe the issue
ctx.releaseSlot()succeeds but leaves the task's row inv1_task_runtime_slot. On a worker with one regular slot, another task cannot start until the released task completes or fails.Environment
1.31.1.hatchet-lite-dev:v0.106.8, PostgreSQL 17.Expected behavior
As described in Manual Slot Release, another task should be able to use the released slot while the original task remains running.
Reproduction and results
Register two tasks on a worker configured with
slots: 1. Task A callsawait ctx.releaseSlot(), submits task B, then sleeps for6 s. Repeat on retries, giving each submitted child a distinctchildKey.The RPC succeeds. Immediately afterward,
v1_task_runtime.worker_idis NULL, butv1_task_runtime_slotstill has the original worker ID andunits = 1.8.267 s6.389 s7.486 s55 ms1.514 s1.544 sA no-release control also kept B waiting until A finished. The corrected-SQL comparison uses the same engine and worker, but executes the candidate query directly against the local database; it is not a rebuilt-engine test.
Cause and proposed fix
In ManualSlotRelease, the final UPDATE depends on
task, while slot deletion depends onlocked_runtime, aSELECT ... FOR UPDATE. The UPDATE can modify the runtime row before the locking CTE executes. That locking read then skips the row modified by the same command, so slot deletion finds no rows.Make the UPDATE depend on the locking CTE:
UPDATE v1_task_runtime SET worker_id = NULL FROM - task + locked_runtime WHERE - (v1_task_runtime.task_id, v1_task_runtime.task_inserted_at, v1_task_runtime.retry_count) IN (SELECT id, inserted_at, retry_count FROM task) + (v1_task_runtime.task_id, v1_task_runtime.task_inserted_at, v1_task_runtime.retry_count) IN (SELECT task_id, task_inserted_at, retry_count FROM locked_runtime) RETURNING v1_task_runtime.*;The generated query in
pkg/repository/sqlcv1/tasks.sql.gowould also need regeneration. The same problematic query was present inmainwhen checked.Here is a standalone SQL reproduction. It uses temporary tables and rolls back:
AI disclosure
OpenCode with GPT-6 Astra assisted with investigation, running the local SDK and SQL reproductions, and drafting this report and candidate fix.