mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
UpdateProjectResource derives every column it writes back from the row read at the top of the request, but the read and the write ran outside any transaction. Two concurrent partial updates could interleave as read-read-write-write, and the second writer's stale snapshot silently reverted the first writer's change: a label-only PUT reverted a racing ref update to an old Git ref, and a label-only PUT on a local_directory restored a stale execution mode (reported in #9238 with a deterministic interleaving that fails 15/15 under -race -count=3). Serialize the flow with SELECT ... FOR UPDATE inside a short transaction (GetProjectResourceInWorkspaceForUpdate + Begin/WithTx, same shape as CreateAgent). Competing updates block on the row until the write lands, then re-read fresh. This keeps the local_directory legacy label-mirror writeback intact, which a presence-aware column rewrite alone would have broken for mirror-triggered ref updates. Review follow-ups: - The regression test no longer leaves the race to the scheduler. Before resuming the paused label request, it waits until the ref request has either completed outright (the only exit on the pre-fix code, which never blocks) or is blocked waiting for the label request's row lock (the only exit with the fix, seen as wait_event_type='Lock' on the backend running the FOR UPDATE query, checked via pg_stat_activity). The two signals are mutually exclusive, so the test fails deterministically without the lock and passes with it. - findLocalDirectoryConflict and requireWorktreeCapableDaemon now read through caller-provided queries instead of the handler's pool: on the update path they run inside the transaction, so a locked request no longer borrows a second pooled connection per request. - Added a local_directory concurrency case: a label-only update racing an execution_mode update through the label-mirror ref rewrite. The regression tests fail on the pre-fix code ("stored ref = main, want the concurrent ref update (release)" / "stored execution_mode = worktree, want the concurrent mode update (cleared = in_place)") and pass with the lock, 20/20 runs each way. Fixes #9238 Generated-by: GLM-5.3-Flash (ZCode)
61 lines
1.9 KiB
SQL
61 lines
1.9 KiB
SQL
-- name: ListProjectResources :many
|
|
SELECT * FROM project_resource
|
|
WHERE project_id = $1
|
|
ORDER BY position ASC, created_at ASC;
|
|
|
|
-- name: ListProjectResourcesInWorkspace :many
|
|
-- Workspace-scoped read for the daemon claim path. project_resource carries its
|
|
-- own workspace_id, so a corrupt project reference cannot pull another tenant's
|
|
-- repository URLs or local paths into a claim response.
|
|
SELECT * FROM project_resource
|
|
WHERE project_id = $1 AND workspace_id = $2
|
|
ORDER BY position ASC, created_at ASC;
|
|
|
|
-- name: ListProjectResourcesForProjects :many
|
|
SELECT * FROM project_resource
|
|
WHERE project_id = ANY(sqlc.arg('project_ids')::uuid[])
|
|
ORDER BY project_id, position ASC, created_at ASC;
|
|
|
|
-- name: GetProjectResource :one
|
|
SELECT * FROM project_resource
|
|
WHERE id = $1;
|
|
|
|
-- name: GetProjectResourceInWorkspace :one
|
|
SELECT * FROM project_resource
|
|
WHERE id = $1 AND workspace_id = $2;
|
|
|
|
-- name: GetProjectResourceInWorkspaceForUpdate :one
|
|
-- Same row as GetProjectResourceInWorkspace, but takes a row lock held until
|
|
-- the caller's transaction ends, serializing the read-decide-write flow in
|
|
-- UpdateProjectResource against competing partial updates (#9238).
|
|
SELECT * FROM project_resource
|
|
WHERE id = $1 AND workspace_id = $2
|
|
FOR UPDATE;
|
|
|
|
-- name: CreateProjectResource :one
|
|
INSERT INTO project_resource (
|
|
project_id, workspace_id, resource_type, resource_ref, label, position, created_by
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6, $7
|
|
) RETURNING *;
|
|
|
|
-- name: UpdateProjectResource :one
|
|
UPDATE project_resource
|
|
SET resource_ref = $2,
|
|
label = $3,
|
|
position = $4
|
|
WHERE id = $1
|
|
RETURNING *;
|
|
|
|
-- name: DeleteProjectResource :exec
|
|
DELETE FROM project_resource WHERE id = $1;
|
|
|
|
-- name: CountProjectResources :one
|
|
SELECT count(*) FROM project_resource WHERE project_id = $1;
|
|
|
|
-- name: GetProjectResourceCounts :many
|
|
SELECT project_id, count(*)::bigint AS resource_count
|
|
FROM project_resource
|
|
WHERE project_id = ANY(sqlc.arg('project_ids')::uuid[])
|
|
GROUP BY project_id;
|