mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
Addresses the fourth review round on MUL-5999.
1. The trigger is gone. Migration 284 now defines
lock_task_owner_workspaces(agent_id, issue_id, runtime_id), a predicate
that every statement writing agent_task_queue ownership calls in its own
WHERE clause:
INSERT INTO agent_task_queue (...)
SELECT ... WHERE lock_task_owner_workspaces($1, $3, $2)
Nine statements now carry it: the seven task INSERTs (agent.sql x5,
autopilot.sql, chat.sql), LinkTaskToIssue, and ReassignTasksToRuntime —
the last through a MATERIALIZED CTE so a bulk UPDATE evaluates the
VOLATILE predicate once instead of per row. Clearing runtime_id is
exempt: it cannot move a task into another workspace.
Calling it from the writing statement keeps the lock in the writer's own
transaction, which a separate caller-issued statement could not do for
the autocommit enqueue paths. TestTaskOwnershipWritesCallTheFence is the
trade for dropping the trigger: it scans the query files and fails the
build if a statement writes ownership without the fence.
2. The fence no longer defers to the foreign keys. The trigger version
counted only owner references that still resolved, so a deleted owner
made itself invisible: with all three gone it returned early, and with
one of three gone the count still matched. The predicate now compares
resolved owners against the number of owner arguments supplied and
returns false on any mismatch, so a stale write is rejected on its own
terms. TestLockTaskOwnerWorkspaces_RejectsUnresolvableOwner covers each
shape, including the one-of-three case.
3. Deterministic lock order. The workspace rows are locked with
ORDER BY w.id, so mirrored cross-workspace writes cannot take the same
pair in opposite orders. Honest note on the test: 25 mirrored concurrent
pairs never deadlock either way, because the current plan happens to
return the rows in a stable order — the ORDER BY is what makes that a
guarantee rather than an accident of the plan, and the test is a
regression test for the symptom.
4. The fail-closed test now tests what its name says. The previous one only
inserted a neighbour-owned task and could not have failed. The
replacement runs the actual race: a writer calls the real
CreateAgentTask, blocks on the workspace lock, teardown deletes the
workspace and commits, and the writer must come back with pgx.ErrNoRows
— the fence's own verdict. An FK doing the work instead surfaces as
SQLSTATE 23503, which the assertion rejects explicitly.
Cost: 5,000 predicate calls in 53.7 ms, about 11 us per fenced write, for
up to three indexed lookups and a shared row lock. Status, dispatch and
completion updates do not call it.
Refs MUL-5999
Co-authored-by: multica-agent <[email protected]>
410 lines
19 KiB
SQL
410 lines
19 KiB
SQL
-- name: ListAgentRuntimes :many
|
|
SELECT * FROM agent_runtime
|
|
WHERE workspace_id = $1
|
|
ORDER BY created_at ASC;
|
|
|
|
-- name: GetAgentRuntime :one
|
|
SELECT * FROM agent_runtime
|
|
WHERE id = $1;
|
|
|
|
-- name: GetAgentRuntimes :many
|
|
-- Batch variant of GetAgentRuntime (MUL-4257): loads every runtime in the
|
|
-- input set in one round trip so the machine-level batch claim handler can
|
|
-- resolve+authorize all of a daemon's runtimes without one point query per
|
|
-- runtime. Rows are returned only for ids that exist; the caller matches them
|
|
-- back by id and skips any that are missing.
|
|
SELECT * FROM agent_runtime
|
|
WHERE id = ANY(@ids::uuid[]);
|
|
|
|
-- name: LockAgentRuntime :one
|
|
-- Acquires a row-level exclusive lock on the runtime row. Used at the
|
|
-- top of the cascade-delete transaction so that:
|
|
-- 1. PostgreSQL's FK validation on agent.runtime_id (FK ... ON DELETE
|
|
-- RESTRICT) needs FOR KEY SHARE on the parent runtime row, which
|
|
-- conflicts with FOR UPDATE — so any concurrent INSERT or UPDATE
|
|
-- that would point a new/moved agent at this runtime blocks until
|
|
-- our transaction finishes; and
|
|
-- 2. concurrent UPDATE/DELETE of the runtime row itself (e.g. another
|
|
-- delete attempt) waits for us to commit.
|
|
-- Combined with ListUserAgentsByRuntimeForUpdate (which row-locks active and
|
|
-- archived user agents) this closes both plan drift and archived-agent restore
|
|
-- races under read-committed isolation.
|
|
SELECT * FROM agent_runtime
|
|
WHERE id = $1
|
|
FOR UPDATE;
|
|
|
|
-- name: GetAgentRuntimeForWorkspace :one
|
|
SELECT * FROM agent_runtime
|
|
WHERE id = $1 AND workspace_id = $2;
|
|
|
|
-- name: UpsertAgentRuntime :one
|
|
-- (xmax = 0) AS inserted distinguishes a fresh insert (true) from an upsert
|
|
-- that updated an existing row (false). Analytics reads this to fire
|
|
-- runtime_registered/runtime_ready only on first-time registration.
|
|
INSERT INTO agent_runtime (
|
|
workspace_id,
|
|
daemon_id,
|
|
name,
|
|
runtime_mode,
|
|
provider,
|
|
status,
|
|
device_info,
|
|
metadata,
|
|
owner_id,
|
|
last_seen_at
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, now())
|
|
-- Built-in runtimes carry no profile_id. The arbiter is the partial unique
|
|
-- index from migration 121 (WHERE profile_id IS NULL); the predicate must be
|
|
-- spelled out so Postgres selects that partial index, not the custom-runtime
|
|
-- one on (workspace_id, daemon_id, profile_id).
|
|
ON CONFLICT (workspace_id, daemon_id, provider) WHERE profile_id IS NULL
|
|
DO UPDATE SET
|
|
name = EXCLUDED.name,
|
|
runtime_mode = EXCLUDED.runtime_mode,
|
|
status = EXCLUDED.status,
|
|
device_info = EXCLUDED.device_info,
|
|
metadata = EXCLUDED.metadata,
|
|
owner_id = COALESCE(EXCLUDED.owner_id, agent_runtime.owner_id),
|
|
last_seen_at = now(),
|
|
updated_at = now()
|
|
RETURNING *, (xmax = 0) AS inserted;
|
|
|
|
-- name: UpsertAgentRuntimeWithProfile :one
|
|
-- Custom-runtime registration: a daemon resolved a workspace runtime_profile's
|
|
-- command_name on PATH and is registering an instance of it. The arbiter is the
|
|
-- partial unique index from migration 120 (WHERE profile_id IS NOT NULL), so a
|
|
-- single daemon can host the built-in provider AND any number of custom
|
|
-- profiles of the same protocol family. provider stays the protocol family so
|
|
-- task routing (agent.New(provider)) is unchanged; profile_id is the stable
|
|
-- identity. (xmax = 0) AS inserted mirrors UpsertAgentRuntime.
|
|
INSERT INTO agent_runtime (
|
|
workspace_id,
|
|
daemon_id,
|
|
name,
|
|
runtime_mode,
|
|
provider,
|
|
status,
|
|
device_info,
|
|
metadata,
|
|
owner_id,
|
|
profile_id,
|
|
last_seen_at
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, now())
|
|
ON CONFLICT (workspace_id, daemon_id, profile_id) WHERE profile_id IS NOT NULL
|
|
DO UPDATE SET
|
|
name = EXCLUDED.name,
|
|
runtime_mode = EXCLUDED.runtime_mode,
|
|
provider = EXCLUDED.provider,
|
|
status = EXCLUDED.status,
|
|
device_info = EXCLUDED.device_info,
|
|
metadata = EXCLUDED.metadata,
|
|
owner_id = COALESCE(EXCLUDED.owner_id, agent_runtime.owner_id),
|
|
last_seen_at = now(),
|
|
updated_at = now()
|
|
RETURNING *, (xmax = 0) AS inserted;
|
|
|
|
-- name: UpdateAgentRuntimeVisibility :one
|
|
-- Toggles a runtime between 'private' (only owner can bind agents) and
|
|
-- 'public' (any workspace member can). Default for new rows is 'private'
|
|
-- (see migration 083). Gated at the handler layer to owner / workspace
|
|
-- admin only.
|
|
UPDATE agent_runtime
|
|
SET visibility = @visibility, updated_at = now()
|
|
WHERE id = @id
|
|
RETURNING *;
|
|
|
|
-- name: UpdateAgentRuntimeCustomName :one
|
|
-- Sets or clears a runtime's user-facing custom name (MUL-4217). custom_name
|
|
-- overrides the daemon-proposed `name` for display; passing NULL reverts to
|
|
-- the default. Kept separate from the registration upserts above (which do
|
|
-- name = EXCLUDED.name on every heartbeat) so a custom name is never
|
|
-- clobbered by the daemon. Gated at the handler to owner / workspace admin.
|
|
UPDATE agent_runtime
|
|
SET custom_name = @custom_name, updated_at = now()
|
|
WHERE id = @id
|
|
RETURNING *;
|
|
|
|
-- name: UpdateAgentRuntimeCustomNameByDaemon :many
|
|
-- Machine-level rename (MUL-4217): applies one custom name to every runtime
|
|
-- sharing a daemon_id in the workspace, since a single machine hosts one
|
|
-- runtime per provider. @owner_id is NULL for workspace owners/admins (rename
|
|
-- the whole machine) or the actor's user id otherwise (only their own
|
|
-- runtimes on that machine), so a member cannot relabel someone else's
|
|
-- runtime that happens to share the host.
|
|
UPDATE agent_runtime
|
|
SET custom_name = @custom_name, updated_at = now()
|
|
WHERE workspace_id = @workspace_id
|
|
AND daemon_id = @daemon_id
|
|
AND (@owner_id::uuid IS NULL OR owner_id = @owner_id)
|
|
RETURNING *;
|
|
|
|
-- name: ListDaemonCustomNames :many
|
|
-- Lists the custom_name of every OTHER runtime on (workspace_id, daemon_id)
|
|
-- (MUL-4217). @exclude_id drops the just-registered row. The caller derives
|
|
-- the machine-level name in Go — the same "all runtimes share one non-null
|
|
-- name" rule the frontend applies in sharedCustomName — so a freshly-added
|
|
-- runtime on an already-named machine can inherit that name and keep the
|
|
-- machine's display name stable. A daemon hosts only a handful of runtimes
|
|
-- (one per provider), so this is a tiny read.
|
|
SELECT custom_name FROM agent_runtime
|
|
WHERE workspace_id = @workspace_id
|
|
AND daemon_id = @daemon_id
|
|
AND id <> @exclude_id;
|
|
|
|
|
|
-- name: TouchAgentRuntimeLastSeen :execrows
|
|
-- Bumps last_seen_at on an already-online runtime. Deliberately does NOT
|
|
-- touch status or updated_at: status is unchanged on the hot heartbeat path,
|
|
-- and avoiding updated_at keeps the row HOT-eligible (no index columns
|
|
-- change) and avoids invalidating any downstream consumer that watches
|
|
-- updated_at.
|
|
--
|
|
-- The status='online' predicate is load-bearing: callers read rt.Status from
|
|
-- a prior SELECT and may race with the sweeper, which can flip the row to
|
|
-- offline between that SELECT and this UPDATE. Without the predicate this
|
|
-- query would silently leave a freshly-heartbeated runtime stuck in offline.
|
|
-- Returning affected rows lets callers detect that race and fall back to
|
|
-- MarkAgentRuntimeOnline to flip the row back online.
|
|
UPDATE agent_runtime
|
|
SET last_seen_at = now()
|
|
WHERE id = $1 AND status = 'online';
|
|
|
|
-- name: TouchAgentRuntimesLastSeenBatch :execrows
|
|
-- Bulk variant of TouchAgentRuntimeLastSeen used by the BatchedHeartbeatScheduler:
|
|
-- coalesces N per-runtime "bump last_seen_at" requests into a single UPDATE so a
|
|
-- fleet beating every 15s costs ~1 DB transaction per batch tick instead of N.
|
|
--
|
|
-- Same load-bearing predicate as the single-id form: status='online' avoids
|
|
-- silently un-deleting a sweeper-flipped offline row, and we deliberately do
|
|
-- NOT touch updated_at so the rows stay HOT-eligible. Affected-rows < len(ids)
|
|
-- means some IDs raced to offline between Schedule and flush; their next beat
|
|
-- will fall through the recordHeartbeat sync path and call MarkAgentRuntimeOnline.
|
|
UPDATE agent_runtime
|
|
SET last_seen_at = now()
|
|
WHERE id = ANY(@ids::uuid[]) AND status = 'online';
|
|
|
|
-- name: MarkAgentRuntimeOnline :one
|
|
-- Used on the offline→online transition (and on first heartbeat after
|
|
-- registration). Writes status, last_seen_at, and updated_at because the
|
|
-- status flip is a real state change and we want updated_at to reflect it.
|
|
UPDATE agent_runtime
|
|
SET status = 'online', last_seen_at = now(), updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING *;
|
|
|
|
-- name: SetAgentRuntimeOffline :exec
|
|
UPDATE agent_runtime
|
|
SET status = 'offline', updated_at = now()
|
|
WHERE id = $1;
|
|
|
|
-- name: SelectStaleOnlineRuntimes :many
|
|
-- Lists online runtimes whose last_seen_at exceeds the stale window. The
|
|
-- sweeper uses this as a candidate set, then optionally filters via the
|
|
-- LivenessStore before flipping rows to offline (a fresh Redis liveness
|
|
-- record means the DB row is just lagging, not actually dead).
|
|
SELECT id, workspace_id, owner_id, daemon_id, provider FROM agent_runtime
|
|
WHERE status = 'online'
|
|
AND last_seen_at < now() - make_interval(secs => @stale_seconds::double precision);
|
|
|
|
-- name: MarkRuntimesOfflineByIDs :many
|
|
-- Flips a known set of runtime IDs from online to offline. Paired with
|
|
-- SelectStaleOnlineRuntimes in the sweeper so the candidate selection and
|
|
-- the actual write are decoupled (the LivenessStore filter sits between).
|
|
--
|
|
-- Re-checks the stale predicate inside the UPDATE so a concurrent heartbeat
|
|
-- between the SELECT (candidate gather), the LivenessStore filter, and this
|
|
-- UPDATE cannot demote a runtime that just refreshed last_seen_at. The
|
|
-- legacy MarkStaleRuntimesOffline UPDATE had this property implicitly
|
|
-- because the predicate and the write lived in one statement; here we
|
|
-- carry it forward explicitly so the SELECT/filter/UPDATE pipeline retains
|
|
-- the same race-freedom.
|
|
UPDATE agent_runtime
|
|
SET status = 'offline', updated_at = now()
|
|
WHERE status = 'online'
|
|
AND id = ANY(@ids::uuid[])
|
|
AND last_seen_at < now() - make_interval(secs => @stale_seconds::double precision)
|
|
RETURNING id, workspace_id, owner_id, daemon_id, provider;
|
|
|
|
-- name: FailTasksForOfflineRuntimes :many
|
|
-- Marks dispatched/running/waiting_local_directory tasks as failed when
|
|
-- their runtime is offline. This cleans up orphaned tasks after a daemon
|
|
-- crash or network partition.
|
|
UPDATE agent_task_queue
|
|
SET status = 'failed', completed_at = now(), error = 'runtime went offline',
|
|
failure_reason = 'runtime_offline',
|
|
wait_reason = NULL
|
|
WHERE status IN ('dispatched', 'running', 'waiting_local_directory')
|
|
AND runtime_id IN (
|
|
SELECT id FROM agent_runtime WHERE status = 'offline'
|
|
)
|
|
RETURNING *;
|
|
|
|
-- name: ListAgentRuntimesByOwner :many
|
|
SELECT * FROM agent_runtime
|
|
WHERE workspace_id = $1 AND owner_id = $2
|
|
ORDER BY created_at ASC;
|
|
|
|
-- name: ForceOfflineRuntimesByIDs :many
|
|
-- Unconditionally flips a known set of runtime IDs to offline. Distinct from
|
|
-- MarkRuntimesOfflineByIDs (which keeps a stale-window predicate so the
|
|
-- sweeper cannot demote a runtime that just heartbeated): this variant is
|
|
-- used by intentional revocation paths — e.g. removing a workspace member —
|
|
-- where the caller has already decided the runtime should be offline
|
|
-- regardless of recent liveness.
|
|
UPDATE agent_runtime
|
|
SET status = 'offline', updated_at = now()
|
|
WHERE id = ANY(@runtime_ids::uuid[]) AND status = 'online'
|
|
RETURNING id, workspace_id, owner_id, daemon_id, provider;
|
|
|
|
-- name: CancelAgentTasksByRuntimeOrAgent :many
|
|
-- Cancels every active task that either lives on one of the given runtimes
|
|
-- OR belongs to one of the given agents. Used by the member-revocation flow:
|
|
-- the runtime-side covers tasks queued against the leaving member's runtimes;
|
|
-- the agent-side covers tasks pinned to a different runtime that those agents
|
|
-- left behind from a prior UpdateAgent (agent.runtime_id can change, but
|
|
-- agent_task_queue.runtime_id does not get rewritten when it does, so a task
|
|
-- queued on runtime A by agent X — later moved to runtime B — survives the
|
|
-- runtime-only revoke and could still be claimed because ClaimAgentTask does
|
|
-- not gate on agent.archived_at).
|
|
--
|
|
-- We use 'cancelled' rather than 'failed' so the daemon's per-task status
|
|
-- poller (watchTaskCancellation) interrupts the running agent gracefully.
|
|
-- Returns the affected rows so the caller can broadcast task:cancelled and
|
|
-- reconcile per-agent status.
|
|
--
|
|
-- The status list must cover EVERY non-terminal status, not just the ones the
|
|
-- daemon is actively working: 'deferred' (migration 128, comment-routing
|
|
-- escalation) was missing here and only went unnoticed because the runtime
|
|
-- delete used to cascade those rows away. Since MUL-5559 the runtime delete
|
|
-- unbinds history rows instead, and agent_task_queue_active_requires_runtime
|
|
-- rejects an active row without a runtime — so a missed status now surfaces as
|
|
-- a failed delete (runtime_delete_not_drained) instead of silent data loss.
|
|
UPDATE agent_task_queue
|
|
SET status = 'cancelled', completed_at = now()
|
|
WHERE (runtime_id = ANY(@runtime_ids::uuid[]) OR agent_id = ANY(@agent_ids::uuid[]))
|
|
AND status IN ('queued', 'dispatched', 'running', 'waiting_local_directory', 'deferred')
|
|
RETURNING *;
|
|
|
|
-- name: CountUndrainedTasksByRuntimeOrAgent :one
|
|
-- Belt-and-braces gate for the runtime-delete transaction: after cancelling,
|
|
-- every task on this runtime OR owned by an agent being unbound must be terminal
|
|
-- (completed_at IS NOT NULL) before the unbind UPDATE runs. The agent-side
|
|
-- predicate must mirror CancelAgentTasksByRuntimeOrAgent: a task can remain
|
|
-- pinned to another runtime after its agent moves. Non-zero means some
|
|
-- non-terminal status escaped the cancel query — the handler aborts with 409
|
|
-- runtime_delete_not_drained rather than letting the CHECK constraint turn it
|
|
-- into an opaque 500, and rather than deleting rows to make it go away.
|
|
SELECT count(*) FROM agent_task_queue
|
|
WHERE (runtime_id = ANY(@runtime_ids::uuid[]) OR agent_id = ANY(@agent_ids::uuid[]))
|
|
AND completed_at IS NULL;
|
|
|
|
-- name: UnbindTasksFromRuntime :execrows
|
|
-- Detaches this runtime's task history so deleting the runtime row cannot
|
|
-- cascade it away (agent_task_queue.runtime_id is ON DELETE CASCADE, and
|
|
-- task_message / task_usage / task_token cascade from the task in turn).
|
|
-- Restricted to terminal rows: an active task must keep its runtime, per
|
|
-- agent_task_queue_active_requires_runtime. The caller runs
|
|
-- CancelAgentTasksByRuntimeOrAgent +
|
|
-- CountUndrainedTasksByRuntimeOrAgent first, so at this point "terminal" is
|
|
-- every row on the runtime.
|
|
UPDATE agent_task_queue
|
|
SET runtime_id = NULL
|
|
WHERE runtime_id = $1 AND completed_at IS NOT NULL;
|
|
|
|
-- name: UnbindUserAgentsFromRuntime :many
|
|
-- MUL-5559: the runtime-delete replacement for archive-then-hard-delete. Every
|
|
-- user agent bound to this runtime becomes unbound (runtime_id IS NULL) and
|
|
-- keeps its row, chats, labels, channel installations and autopilot config.
|
|
--
|
|
-- Deliberately NOT filtered on archived_at: an agent archived earlier is just
|
|
-- as much the user's data as an active one, and hard-deleting it was the same
|
|
-- bug. Deliberately restricted to kind = 'user': system agents are invisible
|
|
-- execution infrastructure with no UI to rebind them (see
|
|
-- DeleteSystemAgentsByRuntime), so leaving them unbound would strand rows no
|
|
-- one can repair.
|
|
UPDATE agent
|
|
SET runtime_id = NULL, updated_at = now()
|
|
WHERE runtime_id = $1 AND kind = 'user'
|
|
RETURNING *;
|
|
|
|
-- name: DeleteAgentRuntime :exec
|
|
DELETE FROM agent_runtime WHERE id = $1;
|
|
|
|
-- name: DeleteSystemAgentsByRuntime :exec
|
|
-- System agents are invisible execution infrastructure (for example the Agent
|
|
-- Builder). Remove them before deleting their runtime so the RESTRICT runtime
|
|
-- FK cannot block an otherwise dependency-free delete.
|
|
DELETE FROM agent WHERE runtime_id = $1 AND kind = 'system';
|
|
|
|
-- name: CountActiveAgentsByRuntime :one
|
|
SELECT count(*) FROM agent WHERE runtime_id = $1 AND archived_at IS NULL;
|
|
|
|
-- name: FindLegacyRuntimesByDaemonID :many
|
|
-- Looks up runtime rows keyed on a prior (hostname-derived) daemon_id. Used
|
|
-- at register-time to find rows owned by the same machine under its old
|
|
-- identity so agents/tasks can be re-pointed at the new UUID-keyed row.
|
|
--
|
|
-- Comparison is case-insensitive because os.Hostname() has been observed to
|
|
-- return different casings on the same machine (e.g. `Jiayuans-MacBook-Pro`
|
|
-- vs `jiayuans-macbook-pro`) across reboots/mDNS state changes. A case-
|
|
-- sensitive `=` would strand the old row; LOWER() on both sides handles drift
|
|
-- without forcing the daemon to enumerate cased permutations.
|
|
--
|
|
-- Returns many rather than one because case drift may have already minted
|
|
-- duplicate rows historically (e.g. `Foo.local` AND `foo.local` under the
|
|
-- same workspace+provider). A single-row lookup would consolidate only one
|
|
-- of them and leave the rest orphaned. Callers must merge every returned
|
|
-- row into the new UUID-keyed runtime.
|
|
SELECT * FROM agent_runtime
|
|
WHERE workspace_id = @workspace_id
|
|
AND provider = @provider
|
|
AND LOWER(daemon_id) = LOWER(@daemon_id);
|
|
|
|
-- name: ReassignAgentsToRuntime :execrows
|
|
-- Re-points every agent referencing old_runtime_id at new_runtime_id.
|
|
UPDATE agent
|
|
SET runtime_id = @new_runtime_id
|
|
WHERE runtime_id = @old_runtime_id;
|
|
|
|
-- name: ReassignTasksToRuntime :execrows
|
|
-- Fenced against workspace teardown: lock_task_owner_workspaces (migration 284)
|
|
-- locks the owners' workspace rows in the writer's own transaction and returns
|
|
-- false once they are gone, so this statement writes no row instead of stranding
|
|
-- a task in a workspace that has just been deleted (MUL-5999).
|
|
-- Re-points every queued/running/completed task referencing old_runtime_id.
|
|
-- Required before deleting the old runtime row because agent_task_queue has
|
|
-- an ON DELETE CASCADE FK that would otherwise drop historical tasks.
|
|
WITH fence AS MATERIALIZED (
|
|
-- Once per statement rather than once per row: the predicate is VOLATILE, so
|
|
-- calling it from the WHERE clause of a bulk UPDATE would re-run it for every
|
|
-- candidate row.
|
|
SELECT lock_task_owner_workspaces(NULL, NULL, @new_runtime_id) AS ok
|
|
)
|
|
UPDATE agent_task_queue
|
|
SET runtime_id = @new_runtime_id
|
|
WHERE runtime_id = @old_runtime_id
|
|
AND (SELECT ok FROM fence);
|
|
|
|
-- name: RecordRuntimeLegacyDaemonID :exec
|
|
-- Remembers the most recent hostname-derived daemon_id that was merged into
|
|
-- this row. Useful for debugging when tracing back why a given runtime row
|
|
-- subsumed an old one, and only overwrites NULL so the earliest merge is
|
|
-- preserved.
|
|
UPDATE agent_runtime
|
|
SET legacy_daemon_id = COALESCE(legacy_daemon_id, $2)
|
|
WHERE id = $1;
|
|
|
|
-- name: DeleteStaleOfflineRuntimes :many
|
|
-- Deletes runtimes that have been offline for longer than the TTL and have
|
|
-- no agents bound (active or archived). The FK constraint on agent.runtime_id
|
|
-- is ON DELETE RESTRICT, so we must exclude all agent references.
|
|
DELETE FROM agent_runtime
|
|
WHERE status = 'offline'
|
|
AND last_seen_at < now() - make_interval(secs => @stale_seconds::double precision)
|
|
AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM agent
|
|
WHERE agent.runtime_id = agent_runtime.id
|
|
)
|
|
RETURNING id, workspace_id;
|