mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
* MUL-6582 feat(plugins): add durable scheduled hooks Co-authored-by: multica-agent <[email protected]> * MUL-6582: add schedule-pulse example plugin Ship a real scheduled plugin that records each unique delivery_id in workspace storage, so a host retry does not look like a second pulse. The cmd/server test installs this example manifest and drives two scheduler replicas through a dropped first attempt. Co-authored-by: multica-agent <[email protected]> * MUL-6582 fix(migrations): make schedule indexes retry-safe Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]>
347 lines
12 KiB
SQL
347 lines
12 KiB
SQL
-- name: LockPluginPackageKey :exec
|
|
-- Serializes publish, install and delete for one (workspace, plugin key).
|
|
--
|
|
-- Relationships are application-owned by repository policy, so there are no
|
|
-- foreign keys to make "this version still exists" true across statements.
|
|
-- Without this lock the interleaving `delete counts 0 installs` → `install
|
|
-- reads the version` → `delete commits` → `install commits` leaves an
|
|
-- installation pointing at a version that no longer exists, and its panel 404s
|
|
-- forever with nothing in the product able to explain why.
|
|
--
|
|
-- Keyed on the plugin key rather than the package id because publish has no
|
|
-- package id yet — the row it would lock is the one it may be about to create.
|
|
SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0));
|
|
|
|
-- name: CreatePluginPackage :one
|
|
INSERT INTO plugin_package (workspace_id, plugin_key, name, created_by)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING *;
|
|
|
|
-- name: UpdatePluginPackageName :one
|
|
-- The display name follows the newest published version. The key never moves:
|
|
-- it is the identity an installation was consented under.
|
|
UPDATE plugin_package
|
|
SET name = $2,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING *;
|
|
|
|
-- name: GetWorkspacePluginPackageByKey :one
|
|
SELECT * FROM plugin_package
|
|
WHERE workspace_id = $1 AND plugin_key = $2;
|
|
|
|
-- name: GetWorkspacePluginPackage :one
|
|
SELECT * FROM plugin_package
|
|
WHERE workspace_id = $1 AND id = $2;
|
|
|
|
-- name: ListWorkspacePluginPackages :many
|
|
SELECT * FROM plugin_package
|
|
WHERE workspace_id = $1
|
|
ORDER BY name ASC;
|
|
|
|
-- name: DeletePluginPackage :exec
|
|
DELETE FROM plugin_package WHERE id = $1;
|
|
|
|
-- name: CreatePluginPackageVersion :one
|
|
-- Published versions are only ever inserted. Nothing updates one, and the
|
|
-- (package_id, version) unique index is what makes a second publish of the same
|
|
-- version a conflict instead of a silent overwrite.
|
|
INSERT INTO plugin_package_version (
|
|
package_id, workspace_id, version, manifest, digest, size_bytes, published_by
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING *;
|
|
|
|
-- name: ListPluginPackageVersions :many
|
|
SELECT * FROM plugin_package_version
|
|
WHERE package_id = $1
|
|
ORDER BY created_at DESC;
|
|
|
|
-- name: GetWorkspacePluginPackageVersion :one
|
|
SELECT * FROM plugin_package_version
|
|
WHERE workspace_id = $1 AND id = $2;
|
|
|
|
-- name: DeletePluginPackageVersionsByPackage :exec
|
|
DELETE FROM plugin_package_version WHERE package_id = $1;
|
|
|
|
-- name: CountInstallationsOfPackageVersions :one
|
|
-- Whether any workspace still runs a version of this package. Publishing is
|
|
-- workspace-private, so this is scoped to the same workspace by construction.
|
|
SELECT count(*) FROM plugin_installation
|
|
WHERE package_version_id IN (
|
|
SELECT id FROM plugin_package_version WHERE package_id = $1
|
|
);
|
|
|
|
-- name: CreatePluginPackageFile :exec
|
|
INSERT INTO plugin_package_file (version_id, path, content, size_bytes, sha256)
|
|
VALUES ($1, $2, $3, $4, $5);
|
|
|
|
-- name: GetPluginPackageFile :one
|
|
SELECT * FROM plugin_package_file
|
|
WHERE version_id = $1 AND path = $2;
|
|
|
|
-- name: ListPluginPackageFilePaths :many
|
|
-- Paths and sizes only: the publisher's file list never needs the bytes.
|
|
SELECT path, size_bytes, sha256 FROM plugin_package_file
|
|
WHERE version_id = $1
|
|
ORDER BY path ASC;
|
|
|
|
-- name: DeletePluginPackageFilesByPackage :exec
|
|
DELETE FROM plugin_package_file
|
|
WHERE version_id IN (SELECT id FROM plugin_package_version WHERE package_id = $1);
|
|
|
|
-- name: CreatePluginInstallation :one
|
|
INSERT INTO plugin_installation (
|
|
workspace_id, plugin_key, package_version_id, version, manifest, granted_scopes, installed_by
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING *;
|
|
|
|
-- name: UpdatePluginInstallationManifest :one
|
|
-- Upgrade path: the installation is re-pointed at another published version and
|
|
-- takes that version's consented manifest snapshot. Config values survive on
|
|
-- purpose; fields the new manifest dropped are pruned by the service before this
|
|
-- runs.
|
|
UPDATE plugin_installation
|
|
SET package_version_id = $2,
|
|
version = $3,
|
|
manifest = $4,
|
|
granted_scopes = $5,
|
|
config = $6,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING *;
|
|
|
|
-- name: UpdatePluginInstallationConfig :one
|
|
UPDATE plugin_installation
|
|
SET config = $2,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING *;
|
|
|
|
-- name: SetPluginInstallationEnabled :one
|
|
UPDATE plugin_installation
|
|
SET enabled = $2,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING *;
|
|
|
|
-- name: GetPluginInstallation :one
|
|
SELECT * FROM plugin_installation WHERE id = $1;
|
|
|
|
-- name: GetWorkspacePluginInstallation :one
|
|
SELECT * FROM plugin_installation
|
|
WHERE workspace_id = $1 AND id = $2;
|
|
|
|
-- name: GetWorkspacePluginInstallationByKey :one
|
|
SELECT * FROM plugin_installation
|
|
WHERE workspace_id = $1 AND plugin_key = $2;
|
|
|
|
-- name: ListWorkspacePluginInstallations :many
|
|
SELECT * FROM plugin_installation
|
|
WHERE workspace_id = $1
|
|
ORDER BY created_at ASC;
|
|
|
|
-- name: DeletePluginInstallation :exec
|
|
DELETE FROM plugin_installation WHERE id = $1;
|
|
|
|
-- name: CreatePluginHookSchedule :one
|
|
INSERT INTO plugin_hook_schedule (
|
|
installation_id, workspace_id, hook_key, cron_expression, timezone,
|
|
next_run_at, enabled
|
|
) VALUES ($1, $2, $3, $4, $5, sqlc.narg(next_run_at), $6)
|
|
RETURNING *;
|
|
|
|
-- name: GetPluginHookSchedule :one
|
|
SELECT * FROM plugin_hook_schedule WHERE id = $1;
|
|
|
|
-- name: ListPluginHookSchedulesByInstallation :many
|
|
SELECT * FROM plugin_hook_schedule
|
|
WHERE installation_id = $1
|
|
ORDER BY hook_key ASC;
|
|
|
|
-- name: ListEnabledPluginHookSchedules :many
|
|
-- Do not filter by next_run_at: it is display-only, while retries and recovery
|
|
-- derive eligibility from cron + activated_at + sys_cron_executions.
|
|
SELECT * FROM plugin_hook_schedule
|
|
WHERE enabled
|
|
ORDER BY id ASC;
|
|
|
|
-- name: UpdatePluginHookScheduleDefinition :one
|
|
-- A cron/timezone change creates a new scheduler generation. The old
|
|
-- sys_cron_executions rows remain immutable history under the prior scope id.
|
|
UPDATE plugin_hook_schedule
|
|
SET cron_expression = $2,
|
|
timezone = $3,
|
|
generation = gen_random_uuid(),
|
|
activated_at = now(),
|
|
next_run_at = sqlc.narg(next_run_at),
|
|
enabled = $4,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING *;
|
|
|
|
-- name: DisablePluginHookSchedules :exec
|
|
UPDATE plugin_hook_schedule
|
|
SET enabled = FALSE, next_run_at = NULL, updated_at = now()
|
|
WHERE installation_id = $1;
|
|
|
|
-- name: ReactivatePluginHookSchedule :one
|
|
-- Re-enable starts a new epoch so occurrences while the installation was off
|
|
-- are never caught up. An already-sent request from the old generation may
|
|
-- finish, but no unstarted old plan survives the generation check.
|
|
UPDATE plugin_hook_schedule
|
|
SET enabled = TRUE,
|
|
generation = gen_random_uuid(),
|
|
activated_at = now(),
|
|
next_run_at = sqlc.narg(next_run_at),
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING *;
|
|
|
|
-- name: UpdatePluginHookScheduleNextRun :execrows
|
|
UPDATE plugin_hook_schedule
|
|
SET next_run_at = sqlc.narg(next_run_at), updated_at = now()
|
|
WHERE id = $1 AND generation = $2 AND enabled;
|
|
|
|
-- name: DeletePluginHookSchedule :exec
|
|
DELETE FROM plugin_hook_schedule WHERE id = $1;
|
|
|
|
-- name: DeletePluginHookSchedulesByInstallation :exec
|
|
DELETE FROM plugin_hook_schedule WHERE installation_id = $1;
|
|
|
|
-- name: UpsertPluginStorageValue :one
|
|
INSERT INTO plugin_storage (installation_id, scope_type, scope_id, key, value)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
ON CONFLICT (installation_id, scope_type, scope_id, key)
|
|
DO UPDATE SET value = EXCLUDED.value, updated_at = now()
|
|
RETURNING *;
|
|
|
|
-- name: GetPluginStorageValue :one
|
|
SELECT * FROM plugin_storage
|
|
WHERE installation_id = $1 AND scope_type = $2 AND scope_id = $3 AND key = $4;
|
|
|
|
-- name: ListPluginStorageKeys :many
|
|
SELECT key, octet_length(value)::bigint AS size_bytes, updated_at
|
|
FROM plugin_storage
|
|
WHERE installation_id = $1 AND scope_type = $2 AND scope_id = $3
|
|
ORDER BY key ASC;
|
|
|
|
-- name: DeletePluginStorageValue :execrows
|
|
DELETE FROM plugin_storage
|
|
WHERE installation_id = $1 AND scope_type = $2 AND scope_id = $3 AND key = $4;
|
|
|
|
-- name: GetPluginStorageUsage :one
|
|
-- Quota accounting for one (installation, scope) pair. The candidate key is
|
|
-- excluded so overwriting an existing key is measured as a replacement rather
|
|
-- than an addition. octet_length, not char_length: the service compares these
|
|
-- against byte budgets, and a UTF-8 character is up to 4 bytes.
|
|
SELECT COUNT(*)::bigint AS key_count,
|
|
COALESCE(SUM(octet_length(value)), 0)::bigint AS total_bytes
|
|
FROM plugin_storage
|
|
WHERE installation_id = $1 AND scope_type = $2 AND scope_id = $3 AND key <> $4;
|
|
|
|
-- name: DeletePluginStorageByInstallation :exec
|
|
DELETE FROM plugin_storage WHERE installation_id = $1;
|
|
|
|
-- name: UpsertPluginSecret :exec
|
|
INSERT INTO plugin_secret (installation_id, key, ciphertext)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (installation_id, key)
|
|
DO UPDATE SET ciphertext = EXCLUDED.ciphertext, updated_at = now();
|
|
|
|
-- name: ListPluginSecretKeys :many
|
|
-- Deliberately never selects ciphertext: the presence of a secret is readable,
|
|
-- its value is not.
|
|
SELECT key, updated_at FROM plugin_secret
|
|
WHERE installation_id = $1
|
|
ORDER BY key ASC;
|
|
|
|
-- name: GetPluginSecret :one
|
|
SELECT * FROM plugin_secret
|
|
WHERE installation_id = $1 AND key = $2;
|
|
|
|
-- name: DeletePluginSecret :execrows
|
|
DELETE FROM plugin_secret WHERE installation_id = $1 AND key = $2;
|
|
|
|
-- name: DeletePluginSecretsByInstallation :exec
|
|
DELETE FROM plugin_secret WHERE installation_id = $1;
|
|
|
|
-- name: SetPluginInstallationToken :exec
|
|
UPDATE plugin_installation
|
|
SET token_hash = $2, token_rotated_at = now(), updated_at = now()
|
|
WHERE id = $1;
|
|
|
|
-- name: GetPluginInstallationByTokenHash :one
|
|
-- Looked up by hash, so the plaintext token exists only in the caller's request.
|
|
SELECT * FROM plugin_installation WHERE token_hash = $1;
|
|
|
|
-- name: CreatePluginInvocation :one
|
|
INSERT INTO plugin_invocation (
|
|
id, installation_id, workspace_id, hook_key, trigger, status, event_type,
|
|
delivery_id, planned_at, attempt, latency_ms, error
|
|
) VALUES (
|
|
COALESCE(sqlc.narg('id')::uuid, gen_random_uuid()),
|
|
$1, $2, $3, $4, $5, sqlc.narg(event_type), sqlc.narg(delivery_id),
|
|
sqlc.narg(planned_at), $6, $7, sqlc.narg(error)
|
|
)
|
|
RETURNING *;
|
|
|
|
-- name: ListPluginInvocations :many
|
|
SELECT * FROM plugin_invocation
|
|
WHERE installation_id = $1
|
|
ORDER BY created_at DESC
|
|
LIMIT $2;
|
|
|
|
-- name: CountRecentPluginInvocations :one
|
|
-- Feeds the per-hook rate limit. Counts attempts, not distinct calls: a hook
|
|
-- retrying into a dead endpoint is exactly the traffic the limit exists to cap.
|
|
SELECT count(*) FROM plugin_invocation
|
|
WHERE installation_id = $1 AND hook_key = $2 AND created_at > $3;
|
|
|
|
-- name: CountRecentPluginFailures :one
|
|
-- Consecutive-failure signal for the event circuit breaker. Bounded by time so
|
|
-- a breaker that tripped hours ago does not keep a hook shut forever.
|
|
SELECT count(*) FROM plugin_invocation
|
|
WHERE installation_id = $1 AND hook_key = $2 AND created_at > $3 AND status <> 'ok';
|
|
|
|
-- name: DeletePluginInvocationsByInstallation :exec
|
|
DELETE FROM plugin_invocation WHERE installation_id = $1;
|
|
|
|
-- name: DeleteExpiredPluginInvocations :execrows
|
|
-- TTL sweep. This table is operational telemetry, not history to keep.
|
|
DELETE FROM plugin_invocation WHERE created_at < $1;
|
|
|
|
-- name: UpsertPluginSkill :one
|
|
-- A plugin's skill resource, as an ordinary workspace skill.
|
|
--
|
|
-- Upsert on (workspace_id, name) because that is the table's own uniqueness
|
|
-- rule and an upgrade re-installs the same skill. The WHERE clause is the
|
|
-- important half: it refuses to overwrite a skill a PERSON created, or one
|
|
-- another installation owns. A plugin claiming a name someone already used must
|
|
-- fail the install loudly, not silently replace their work.
|
|
INSERT INTO skill (workspace_id, name, description, content, config, created_by, plugin_installation_id)
|
|
VALUES ($1, $2, $3, $4, '{}'::jsonb, sqlc.narg(created_by), $5)
|
|
ON CONFLICT (workspace_id, name) DO UPDATE SET
|
|
description = EXCLUDED.description,
|
|
content = EXCLUDED.content,
|
|
updated_at = now()
|
|
WHERE skill.plugin_installation_id = EXCLUDED.plugin_installation_id
|
|
RETURNING *;
|
|
|
|
-- name: ListPluginSkills :many
|
|
SELECT * FROM skill WHERE plugin_installation_id = $1 ORDER BY name ASC;
|
|
|
|
-- name: DeletePluginSkillsByInstallation :exec
|
|
DELETE FROM skill WHERE plugin_installation_id = $1;
|
|
|
|
-- name: DeletePluginSkillsNotIn :exec
|
|
-- Upgrade pruning: a skill this installation used to contribute but no longer
|
|
-- declares must go, or a renamed skill leaves its predecessor behind forever.
|
|
DELETE FROM skill
|
|
WHERE plugin_installation_id = $1 AND name <> ALL(@keep_names::text[]);
|
|
|
|
-- name: SetPluginMCPApprovals :one
|
|
UPDATE plugin_installation
|
|
SET mcp_approvals = $2, updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING *;
|