Files
multica/server/pkg/db/generated/project_resource.sql.go
小七 f9b3065ae1 fix(handler): serialize project resource read-modify-write with a row lock (#9240)
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)
2026-10-26 15:20:52 +08:00

342 lines
9.3 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: project_resource.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const countProjectResources = `-- name: CountProjectResources :one
SELECT count(*) FROM project_resource WHERE project_id = $1
`
func (q *Queries) CountProjectResources(ctx context.Context, projectID pgtype.UUID) (int64, error) {
row := q.db.QueryRow(ctx, countProjectResources, projectID)
var count int64
err := row.Scan(&count)
return count, err
}
const createProjectResource = `-- 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 id, project_id, workspace_id, resource_type, resource_ref, label, position, created_at, created_by
`
type CreateProjectResourceParams struct {
ProjectID pgtype.UUID `json:"project_id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
ResourceType string `json:"resource_type"`
ResourceRef []byte `json:"resource_ref"`
Label pgtype.Text `json:"label"`
Position int32 `json:"position"`
CreatedBy pgtype.UUID `json:"created_by"`
}
func (q *Queries) CreateProjectResource(ctx context.Context, arg CreateProjectResourceParams) (ProjectResource, error) {
row := q.db.QueryRow(ctx, createProjectResource,
arg.ProjectID,
arg.WorkspaceID,
arg.ResourceType,
arg.ResourceRef,
arg.Label,
arg.Position,
arg.CreatedBy,
)
var i ProjectResource
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.WorkspaceID,
&i.ResourceType,
&i.ResourceRef,
&i.Label,
&i.Position,
&i.CreatedAt,
&i.CreatedBy,
)
return i, err
}
const deleteProjectResource = `-- name: DeleteProjectResource :exec
DELETE FROM project_resource WHERE id = $1
`
func (q *Queries) DeleteProjectResource(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteProjectResource, id)
return err
}
const getProjectResource = `-- name: GetProjectResource :one
SELECT id, project_id, workspace_id, resource_type, resource_ref, label, position, created_at, created_by FROM project_resource
WHERE id = $1
`
func (q *Queries) GetProjectResource(ctx context.Context, id pgtype.UUID) (ProjectResource, error) {
row := q.db.QueryRow(ctx, getProjectResource, id)
var i ProjectResource
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.WorkspaceID,
&i.ResourceType,
&i.ResourceRef,
&i.Label,
&i.Position,
&i.CreatedAt,
&i.CreatedBy,
)
return i, err
}
const getProjectResourceCounts = `-- name: GetProjectResourceCounts :many
SELECT project_id, count(*)::bigint AS resource_count
FROM project_resource
WHERE project_id = ANY($1::uuid[])
GROUP BY project_id
`
type GetProjectResourceCountsRow struct {
ProjectID pgtype.UUID `json:"project_id"`
ResourceCount int64 `json:"resource_count"`
}
func (q *Queries) GetProjectResourceCounts(ctx context.Context, projectIds []pgtype.UUID) ([]GetProjectResourceCountsRow, error) {
rows, err := q.db.Query(ctx, getProjectResourceCounts, projectIds)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetProjectResourceCountsRow{}
for rows.Next() {
var i GetProjectResourceCountsRow
if err := rows.Scan(&i.ProjectID, &i.ResourceCount); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getProjectResourceInWorkspace = `-- name: GetProjectResourceInWorkspace :one
SELECT id, project_id, workspace_id, resource_type, resource_ref, label, position, created_at, created_by FROM project_resource
WHERE id = $1 AND workspace_id = $2
`
type GetProjectResourceInWorkspaceParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
func (q *Queries) GetProjectResourceInWorkspace(ctx context.Context, arg GetProjectResourceInWorkspaceParams) (ProjectResource, error) {
row := q.db.QueryRow(ctx, getProjectResourceInWorkspace, arg.ID, arg.WorkspaceID)
var i ProjectResource
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.WorkspaceID,
&i.ResourceType,
&i.ResourceRef,
&i.Label,
&i.Position,
&i.CreatedAt,
&i.CreatedBy,
)
return i, err
}
const getProjectResourceInWorkspaceForUpdate = `-- name: GetProjectResourceInWorkspaceForUpdate :one
SELECT id, project_id, workspace_id, resource_type, resource_ref, label, position, created_at, created_by FROM project_resource
WHERE id = $1 AND workspace_id = $2
FOR UPDATE
`
type GetProjectResourceInWorkspaceForUpdateParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
// 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).
func (q *Queries) GetProjectResourceInWorkspaceForUpdate(ctx context.Context, arg GetProjectResourceInWorkspaceForUpdateParams) (ProjectResource, error) {
row := q.db.QueryRow(ctx, getProjectResourceInWorkspaceForUpdate, arg.ID, arg.WorkspaceID)
var i ProjectResource
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.WorkspaceID,
&i.ResourceType,
&i.ResourceRef,
&i.Label,
&i.Position,
&i.CreatedAt,
&i.CreatedBy,
)
return i, err
}
const listProjectResources = `-- name: ListProjectResources :many
SELECT id, project_id, workspace_id, resource_type, resource_ref, label, position, created_at, created_by FROM project_resource
WHERE project_id = $1
ORDER BY position ASC, created_at ASC
`
func (q *Queries) ListProjectResources(ctx context.Context, projectID pgtype.UUID) ([]ProjectResource, error) {
rows, err := q.db.Query(ctx, listProjectResources, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ProjectResource{}
for rows.Next() {
var i ProjectResource
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.WorkspaceID,
&i.ResourceType,
&i.ResourceRef,
&i.Label,
&i.Position,
&i.CreatedAt,
&i.CreatedBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listProjectResourcesForProjects = `-- name: ListProjectResourcesForProjects :many
SELECT id, project_id, workspace_id, resource_type, resource_ref, label, position, created_at, created_by FROM project_resource
WHERE project_id = ANY($1::uuid[])
ORDER BY project_id, position ASC, created_at ASC
`
func (q *Queries) ListProjectResourcesForProjects(ctx context.Context, projectIds []pgtype.UUID) ([]ProjectResource, error) {
rows, err := q.db.Query(ctx, listProjectResourcesForProjects, projectIds)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ProjectResource{}
for rows.Next() {
var i ProjectResource
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.WorkspaceID,
&i.ResourceType,
&i.ResourceRef,
&i.Label,
&i.Position,
&i.CreatedAt,
&i.CreatedBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listProjectResourcesInWorkspace = `-- name: ListProjectResourcesInWorkspace :many
SELECT id, project_id, workspace_id, resource_type, resource_ref, label, position, created_at, created_by FROM project_resource
WHERE project_id = $1 AND workspace_id = $2
ORDER BY position ASC, created_at ASC
`
type ListProjectResourcesInWorkspaceParams struct {
ProjectID pgtype.UUID `json:"project_id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
// 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.
func (q *Queries) ListProjectResourcesInWorkspace(ctx context.Context, arg ListProjectResourcesInWorkspaceParams) ([]ProjectResource, error) {
rows, err := q.db.Query(ctx, listProjectResourcesInWorkspace, arg.ProjectID, arg.WorkspaceID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ProjectResource{}
for rows.Next() {
var i ProjectResource
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.WorkspaceID,
&i.ResourceType,
&i.ResourceRef,
&i.Label,
&i.Position,
&i.CreatedAt,
&i.CreatedBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateProjectResource = `-- name: UpdateProjectResource :one
UPDATE project_resource
SET resource_ref = $2,
label = $3,
position = $4
WHERE id = $1
RETURNING id, project_id, workspace_id, resource_type, resource_ref, label, position, created_at, created_by
`
type UpdateProjectResourceParams struct {
ID pgtype.UUID `json:"id"`
ResourceRef []byte `json:"resource_ref"`
Label pgtype.Text `json:"label"`
Position int32 `json:"position"`
}
func (q *Queries) UpdateProjectResource(ctx context.Context, arg UpdateProjectResourceParams) (ProjectResource, error) {
row := q.db.QueryRow(ctx, updateProjectResource,
arg.ID,
arg.ResourceRef,
arg.Label,
arg.Position,
)
var i ProjectResource
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.WorkspaceID,
&i.ResourceType,
&i.ResourceRef,
&i.Label,
&i.Position,
&i.CreatedAt,
&i.CreatedBy,
)
return i, err
}