Merge remote-tracking branch 'origin/staging' into codex/chat-background-run-notice-20261106
This commit is contained in:
@@ -173,6 +173,7 @@ export interface PersonalReportJobDataClient {
|
||||
rpc(
|
||||
functionName:
|
||||
| "claim_personal_report_job"
|
||||
| "claim_personal_report_job_for_types"
|
||||
| "heartbeat_personal_report_job"
|
||||
| "complete_personal_report_job"
|
||||
| "fail_personal_report_job"
|
||||
@@ -679,12 +680,17 @@ export function createPersonalReportJobService(
|
||||
const workerId = requireWorkerId(input.workerId);
|
||||
const leaseSeconds = requireInteger(input.leaseSeconds ?? 60, "leaseSeconds", 5, 3600);
|
||||
const jobId = optionalUuid(input.jobId, "jobId");
|
||||
const reportTypes = input.reportTypes && input.reportTypes.length > 0 ? [...input.reportTypes] : null;
|
||||
const { data, error } = await client.rpc("claim_personal_report_job", {
|
||||
// A lane limited to some report types (report queue, 2026-11-05) uses its own
|
||||
// function, so the original claim signature is never overloaded.
|
||||
const reportTypes = input.reportTypes && input.reportTypes.length > 0 && jobId === null ? [...input.reportTypes] : null;
|
||||
const { data, error } = reportTypes ? await client.rpc("claim_personal_report_job_for_types", {
|
||||
p_worker_id: workerId,
|
||||
p_lease_seconds: leaseSeconds,
|
||||
p_report_types: reportTypes,
|
||||
}) : await client.rpc("claim_personal_report_job", {
|
||||
p_worker_id: workerId,
|
||||
p_lease_seconds: leaseSeconds,
|
||||
p_job_id: jobId,
|
||||
p_report_types: reportTypes,
|
||||
});
|
||||
if (error) throw storageError(error);
|
||||
const row = firstRpcRow(data);
|
||||
|
||||
@@ -7,10 +7,12 @@ begin;
|
||||
-- annual_full) already goes through personal_report_jobs, one job at a time
|
||||
-- per server. The worker now runs a few lanes at once; a lane may take only
|
||||
-- some report types, so a short annual or synastry report is never stuck
|
||||
-- behind long personal reports. claim_personal_report_job gains an optional
|
||||
-- p_report_types filter (null = any type, the old behaviour). The old
|
||||
-- three-argument signature is dropped so positional calls with three
|
||||
-- arguments resolve to the new function and keep working.
|
||||
-- behind long personal reports. Such a lane claims with
|
||||
-- claim_personal_report_job_for_types, a separate function with a report-type
|
||||
-- filter; claim_personal_report_job is left exactly as it was. (A first version
|
||||
-- overloaded claim_personal_report_job with a fourth, defaulted argument; a
|
||||
-- database that also replays 20260814040000 then held both signatures and a
|
||||
-- three-argument call was ambiguous. A new name cannot collide.)
|
||||
--
|
||||
-- personal_report_queue_view gives one job's place in the queue: the types of
|
||||
-- the jobs that will be claimed before it, the jobs running now with how long
|
||||
@@ -19,13 +21,10 @@ begin;
|
||||
-- (auth.uid()) and returns report types and times, never another user's
|
||||
-- identifiers. The report route calls it with the user's own client.
|
||||
|
||||
drop function if exists public.claim_personal_report_job(text, integer, uuid);
|
||||
|
||||
create or replace function public.claim_personal_report_job(
|
||||
create or replace function public.claim_personal_report_job_for_types(
|
||||
p_worker_id text,
|
||||
p_lease_seconds integer default 60,
|
||||
p_job_id uuid default null,
|
||||
p_report_types text[] default null
|
||||
p_lease_seconds integer,
|
||||
p_report_types text[]
|
||||
)
|
||||
returns setof public.personal_report_jobs
|
||||
language plpgsql
|
||||
@@ -40,23 +39,22 @@ begin
|
||||
if p_lease_seconds not between 5 and 3600 then
|
||||
raise exception using errcode = '22023', message = 'personal_report_job_lease_seconds_invalid';
|
||||
end if;
|
||||
if p_report_types is null or cardinality(p_report_types) = 0 then
|
||||
raise exception using errcode = '22023', message = 'personal_report_job_report_types_invalid';
|
||||
end if;
|
||||
|
||||
select job.id
|
||||
into v_job_id
|
||||
from public.personal_report_jobs as job
|
||||
where (p_job_id is null or job.id = p_job_id)
|
||||
and job.status in ('queued', 'retrying')
|
||||
where job.status in ('queued', 'retrying')
|
||||
and (job.next_attempt_at is null or job.next_attempt_at <= clock_timestamp())
|
||||
and job.attempt_count < job.max_attempts
|
||||
and (
|
||||
p_report_types is null
|
||||
or exists (
|
||||
select 1
|
||||
from public.personal_reports as report
|
||||
where report.user_id = job.user_id
|
||||
and report.request_id = job.request_id
|
||||
and report.report_type = any(p_report_types)
|
||||
)
|
||||
and exists (
|
||||
select 1
|
||||
from public.personal_reports as report
|
||||
where report.user_id = job.user_id
|
||||
and report.request_id = job.request_id
|
||||
and report.report_type = any(p_report_types)
|
||||
)
|
||||
order by job.next_attempt_at nulls first, job.created_at, job.id
|
||||
for update skip locked
|
||||
@@ -86,8 +84,8 @@ begin
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke all on function public.claim_personal_report_job(text, integer, uuid, text[]) from public, anon, authenticated;
|
||||
grant execute on function public.claim_personal_report_job(text, integer, uuid, text[]) to service_role;
|
||||
revoke all on function public.claim_personal_report_job_for_types(text, integer, text[]) from public, anon, authenticated;
|
||||
grant execute on function public.claim_personal_report_job_for_types(text, integer, text[]) to service_role;
|
||||
|
||||
create or replace function public.personal_report_queue_view(p_job_id uuid)
|
||||
returns jsonb
|
||||
|
||||
@@ -99,17 +99,21 @@ test("a lane claims only its report types", async () => {
|
||||
const service = createSupabasePersonalReportJobService(client as never);
|
||||
assert.equal(await service.claimLease({ workerId: "w:lane-2", reportTypes: ["annual_full", "synastry_full"] }), null);
|
||||
assert.equal(await service.claimLease({ workerId: "w:lane-1" }), null);
|
||||
assert.deepEqual(calls.map((call) => call.p_report_types), [["annual_full", "synastry_full"], null]);
|
||||
assert.deepEqual(calls.map((call) => [call.name, call.p_report_types ?? null]), [
|
||||
["claim_personal_report_job_for_types", ["annual_full", "synastry_full"]],
|
||||
["claim_personal_report_job", null],
|
||||
]);
|
||||
});
|
||||
|
||||
test("migration: the claim filter, the dropped three-argument claim, and an owner-only queue view", () => {
|
||||
test("migration: a separate typed claim (the original claim is untouched) and an owner-only queue view", () => {
|
||||
const sql = read("../supabase/migrations/20261105010000_personal_report_job_lanes.sql");
|
||||
assert.match(sql, /drop function if exists public\.claim_personal_report_job\(text, integer, uuid\);/);
|
||||
assert.match(sql, /p_report_types text\[\] default null/);
|
||||
// Overloading claim_personal_report_job made three-argument calls ambiguous in the staging DB gate.
|
||||
assert.doesNotMatch(sql, /function public\.claim_personal_report_job\(/);
|
||||
assert.match(sql, /create or replace function public\.claim_personal_report_job_for_types\(\s*p_worker_id text,\s*p_lease_seconds integer,\s*p_report_types text\[\]\s*\)/);
|
||||
assert.match(sql, /report\.report_type = any\(p_report_types\)/);
|
||||
assert.match(sql, /for update skip locked/);
|
||||
assert.match(sql, /revoke all on function public\.claim_personal_report_job\(text, integer, uuid, text\[\]\) from public, anon, authenticated/);
|
||||
assert.match(sql, /grant execute on function public\.claim_personal_report_job\(text, integer, uuid, text\[\]\) to service_role/);
|
||||
assert.match(sql, /revoke all on function public\.claim_personal_report_job_for_types\(text, integer, text\[\]\) from public, anon, authenticated/);
|
||||
assert.match(sql, /grant execute on function public\.claim_personal_report_job_for_types\(text, integer, text\[\]\) to service_role/);
|
||||
// The view answers only the job's own user; it can count others' jobs (security definer).
|
||||
assert.match(sql, /owner uuid := \(select auth\.uid\(\)\);/);
|
||||
assert.match(sql, /where job\.id = p_job_id and job\.user_id = owner/);
|
||||
|
||||
Reference in New Issue
Block a user