83 lines
6.4 KiB
TypeScript
83 lines
6.4 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import { test } from "node:test"
|
|
import { createServerPollBoundaryCoordinator } from "./server-poll-boundary.js"
|
|
import { createServerPartitionMaintenanceLease,SERVER_PARTITION_WRITER_INVENTORY_SQL } from "./server-partition-maintenance-lease.js"
|
|
import { PARTITION_SPECS } from "../db/partitions.js"
|
|
import type { ServerPollGrant } from "./server-poll-execution.js"
|
|
|
|
const request={serverId:1,receiptId:"11111111-1111-4111-8111-111111111111"},proof="a".repeat(64)
|
|
const options={deadlineAt:10000,serverIds:[1,2,3]}
|
|
function fixture(){
|
|
const events:string[]=[],requirements:number[][]=[],released:boolean[]=[]
|
|
let clock=1000,grant:ServerPollGrant={global:true,serverIds:[1,2,3],authorityProof:proof},closed=false,hook:(()=>Promise<void>)|undefined
|
|
const boundary=createServerPollBoundaryCoordinator(async(_request,ids)=>{events.push("fresh");requirements.push(ids);await hook?.();return structuredClone(grant)},request,()=>clock)
|
|
const inventory=PARTITION_SPECS.map((spec,index)=>({parent:spec.parent,oid:String(index+1),relname:spec.parent,nspname:"public",trigger_type:31,trigger_enabled:"A",trigger_name:"server_partition_writer_guard",trigger_args:0,unconditional:true,constraint_oid:"0",function_schema:"public",function_name:"server_partition_writer_guard",function_language:"plpgsql",security_definer:false,function_source:"BEGIN PERFORM pg_advisory_xact_lock_shared(8723110::bigint); IF TG_OP='DELETE' THEN RETURN OLD; END IF; RETURN NEW; END;"}))
|
|
const lease=createServerPartitionMaintenanceLease({boundary,now:()=>clock,assertAuthority:async()=>{events.push("legacy")},connect:async()=>({release:unsafe=>{released.push(!!unsafe)},query:async text=>{events.push(text);if(text===SERVER_PARTITION_WRITER_INVENTORY_SQL)return {rows:inventory};if(text.includes("partition-lease:unlock"))return {rows:[{unlocked:true}]};return {rows:[]}}})})
|
|
return {boundary,lease,events,requirements,released,fence:()=>boundary.fence([1],proof,options,()=>{if(closed)throw Error("closed local holder")}),
|
|
clock:(value:number)=>{clock=value},grant:(value:ServerPollGrant)=>{grant=value},close:()=>{closed=true},hook:(value:(()=>Promise<void>)|undefined)=>{hook=value}}
|
|
}
|
|
test("one source SQL obtains exactly one fresh complete grant on each side, with all holder IDs",async()=>{
|
|
const f=fixture(),fence=f.fence()
|
|
await f.lease.withLease(()=>f.lease.transaction(true,fence,options,async query=>{
|
|
const start=f.events.length;await query("SELECT owned_target")
|
|
assert.deepEqual(f.events.slice(start),["fresh","SELECT owned_target","fresh"])
|
|
assert.deepEqual(f.requirements.slice(-2),[[1,2,3],[1,2,3]])
|
|
}),options)
|
|
assert.deepEqual(f.released,[false])
|
|
})
|
|
test("every later SQL obtains new decisions; grant is never cached across queries",async()=>{
|
|
const f=fixture(),fence=f.fence()
|
|
await assert.rejects(f.lease.withLease(()=>f.lease.transaction(true,fence,options,async query=>{
|
|
await query("SELECT first")
|
|
f.grant({global:true,serverIds:[1,2],authorityProof:proof})
|
|
await query("SELECT forbidden_second")
|
|
}),options))
|
|
assert(f.events.includes("SELECT first"));assert(!f.events.includes("SELECT forbidden_second"));assert.deepEqual(f.released,[true])
|
|
})
|
|
test("epoch drift after a target SQL refuses its acknowledgement and destroys ownership",async()=>{
|
|
const f=fixture(),fence=f.fence();let target=false
|
|
f.hook(async()=>{if(target)f.grant({global:true,serverIds:[1,2,3],authorityProof:"b".repeat(64)})})
|
|
await assert.rejects(f.lease.withLease(()=>f.lease.transaction(true,fence,options,async query=>{
|
|
// Set the post-dispatch hook from the physical result, not a wall clock.
|
|
const original=f.events.push.bind(f.events)
|
|
f.events.push=(...items)=>{if(items.includes("SELECT epoch_target"))target=true;return original(...items)}
|
|
await query("SELECT epoch_target")
|
|
}),options))
|
|
assert.equal(f.events.filter(item=>item==="SELECT epoch_target").length,1);assert.deepEqual(f.released,[true]);assert(!f.events.includes("COMMIT"))
|
|
})
|
|
test("unowned/wrapped fences retain the original generic checks and are never silently merged",async()=>{
|
|
const f=fixture(),owned=f.fence(),wrapped=async()=>owned()
|
|
assert.equal(f.boundary.combine([wrapped],[1],options),undefined)
|
|
await f.lease.withLease(()=>f.lease.transaction(true,wrapped,options,async query=>{
|
|
const start=f.events.length;await query("SELECT generic")
|
|
assert.equal(f.events.slice(start).filter(item=>item==="legacy").length,4)
|
|
assert.equal(f.events.slice(start).filter(item=>item==="fresh").length,2)
|
|
}),options)
|
|
})
|
|
test("abort, original deadline and closed local scope still fence async authority gaps",async()=>{
|
|
for(const fault of ["abort","expiry","local"]){
|
|
const f=fixture(),controller=new AbortController()
|
|
const fence=f.boundary.fence([1],proof,{...options,signal:controller.signal},()=>{if(fault==="local"&&f.events.includes("close"))throw Error("closed")})
|
|
f.hook(async()=>{if(fault==="abort")controller.abort();else if(fault==="expiry")f.clock(10001);else f.events.push("close")})
|
|
await assert.rejects(fence());assert.equal(f.events.filter(item=>item==="fresh").length,1)
|
|
}
|
|
})
|
|
test("requirements merge only within the same session and preserve original proof/deadline",async()=>{
|
|
const f=fixture(),other=fixture(),a=f.boundary.fence([1],proof,{deadlineAt:5000}),b=f.boundary.fence([2],proof,{deadlineAt:6000})
|
|
assert.equal(f.boundary.combine([other.fence()],[3],options),undefined)
|
|
const combined=f.boundary.combine([a,b],[3],options)!
|
|
await combined();assert.deepEqual(f.requirements.at(-1),[1,2,3]);f.clock(5000);await assert.rejects(combined())
|
|
assert.throws(()=>f.boundary.combine([a,f.boundary.fence([1],"b".repeat(64),options)],[1],options))
|
|
})
|
|
test("an abandoned callback query cannot dispatch after its transaction already completed",async()=>{
|
|
const f=fixture(),fence=f.fence();let leaked:Promise<unknown>|undefined,release!:()=>void,blocked=false
|
|
const gate=new Promise<void>(resolve=>{release=resolve})
|
|
await f.lease.withLease(()=>f.lease.transaction(true,fence,options,async query=>{
|
|
f.hook(async()=>{if(!blocked){blocked=true;await gate}})
|
|
leaked=query("SELECT abandoned");void leaked.catch(()=>{})
|
|
// Let the query enter its authority await, then return without awaiting it.
|
|
await Promise.resolve();await Promise.resolve()
|
|
}),options)
|
|
release();await assert.rejects(leaked!);assert(!f.events.includes("SELECT abandoned"));assert.deepEqual(f.released,[false])
|
|
})
|