Repository navigation
fix(daemon): stop provider lease releases after shutdown closes the releaser #4134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
21344f4
74e46b9
45787dd
5a5671a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,60 +1,104 @@ | ||
| import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; | ||
| import type { ExpiredProviderLeaseReleaser } from '../provider-lease-expiry.ts'; | ||
| import type { DeviceLease } from '@agent-device/contracts/device'; | ||
| import { | ||
| leaseReleaseDiagnosticEvent, | ||
| type ExpiredProviderLeaseReleaser, | ||
| type ProviderLeaseReleaseOutcome, | ||
| } from '../provider-lease-expiry.ts'; | ||
| import type { LeaseRegistry } from '../../resources/lease-registry.ts'; | ||
| import { leaseReleaseRequestFor } from '../../resources/lease-registry-scope.ts'; | ||
|
|
||
| /** | ||
| * The leases this finalizer was refused, by id. The shutdown summary reports these: a release its | ||
| * provider never saw is not a release this daemon completed. | ||
| */ | ||
| export type DaemonLeaseReleaseSummary = Readonly<{ | ||
| refused: readonly string[]; | ||
| }>; | ||
|
|
||
| /** What one lease came to here. The lease travels with it: the report must name it whichever way this went. */ | ||
| type LeaseReleaseSettlement = | ||
| | { lease: DeviceLease; outcome: ProviderLeaseReleaseOutcome } | ||
| | { lease: DeviceLease; timedOut: true } | ||
| | { lease: DeviceLease; error: unknown }; | ||
|
Check notice on line 23 in packages/daemon/src/server/http/daemon-lease-finalizer.ts
|
||
|
|
||
| /** | ||
| * Ends every lease still active at daemon shutdown, whether a session held it or its owner kept it | ||
| * through `close` with `retainOnClose`. Each provider release is bounded by `timeoutMs`; one that | ||
| * runs past it stays with the releaser for the shutdown drain. | ||
| * runs past it stays with the releaser for the shutdown drain. A release the closed releaser refused is | ||
| * reported per lease and named in the returned summary, so the shutdown says the device was left with | ||
| * the provider instead of reading as a completed release. | ||
| */ | ||
| export async function finalizeDaemonLeases(params: { | ||
| leaseRegistry: LeaseRegistry; | ||
| expiredProviderLeaseReleaser: ExpiredProviderLeaseReleaser; | ||
| timeoutMs: number; | ||
| }): Promise<void> { | ||
| }): Promise<DaemonLeaseReleaseSummary> { | ||
| const { leaseRegistry, expiredProviderLeaseReleaser, timeoutMs } = params; | ||
| await Promise.all( | ||
| leaseRegistry.listActiveLeases().map(async (lease) => { | ||
| const settlements = await Promise.all( | ||
| leaseRegistry.listActiveLeases().map(async (lease): Promise<LeaseReleaseSettlement> => { | ||
| try { | ||
| const completed = await releaseWithinTimeout( | ||
| const outcome = await releaseWithinTimeout( | ||
| expiredProviderLeaseReleaser.release(lease), | ||
| timeoutMs, | ||
| ); | ||
| // The daemon stops either way, so the registry entry goes with it. What carries a lease past this | ||
| // process is its recovery record and its diagnostic, never this registry. | ||
| leaseRegistry.releaseLease(leaseReleaseRequestFor(lease)); | ||
| if (!completed) { | ||
| emitDiagnostic({ | ||
| level: 'warn', | ||
| phase: 'daemon_shutdown_lease_release_timed_out', | ||
| data: { leaseId: lease.leaseId, timeoutMs }, | ||
| }); | ||
| } | ||
| if (outcome === undefined) return { lease, timedOut: true }; | ||
| return { lease, outcome }; | ||
| } catch (error) { | ||
| emitDiagnostic({ | ||
| level: 'warn', | ||
| phase: 'daemon_shutdown_lease_release_failed', | ||
| data: { | ||
| leaseId: lease.leaseId, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }, | ||
| }); | ||
| return { lease, error }; | ||
| } | ||
| }), | ||
| ); | ||
| const refused: string[] = []; | ||
| for (const settlement of settlements) { | ||
| const { leaseId } = settlement.lease; | ||
| // A release whose budget ran out is reported here and answered by the drain, which owns the attempt. | ||
| if ('timedOut' in settlement) { | ||
| emitDiagnostic({ | ||
| level: 'warn', | ||
| phase: 'daemon_shutdown_lease_release_timed_out', | ||
| data: { leaseId, timeoutMs }, | ||
| }); | ||
| continue; | ||
| } | ||
| if ('error' in settlement) { | ||
| emitDiagnostic({ | ||
| level: 'warn', | ||
| phase: 'daemon_shutdown_lease_release_failed', | ||
| data: { | ||
| leaseId, | ||
| error: | ||
| settlement.error instanceof Error ? settlement.error.message : String(settlement.error), | ||
| }, | ||
| }); | ||
| continue; | ||
| } | ||
| // The mapping owns the event, so a refusal reads the same here as it does on the expiry route. | ||
| const event = leaseReleaseDiagnosticEvent(settlement.lease, settlement.outcome); | ||
| if (event) emitDiagnostic(event); | ||
| if (settlement.outcome.status === 'refused') refused.push(leaseId); | ||
| } | ||
| return Object.freeze({ refused: Object.freeze(refused) }); | ||
| } | ||
|
|
||
| async function releaseWithinTimeout(release: Promise<void>, timeoutMs: number): Promise<boolean> { | ||
| return await new Promise<boolean>((resolve, reject) => { | ||
| const timeout = setTimeout(() => resolve(false), timeoutMs); | ||
| async function releaseWithinTimeout<T>( | ||
|
thymikee marked this conversation as resolved.
Resolved
|
||
| release: Promise<T>, | ||
| timeoutMs: number, | ||
| ): Promise<T | undefined> { | ||
| return await new Promise<T | undefined>((resolve, reject) => { | ||
| const timeout = setTimeout(() => resolve(undefined), timeoutMs); | ||
| void release.then( | ||
| () => { | ||
| (value) => { | ||
| clearTimeout(timeout); | ||
| resolve(true); | ||
| resolve(value); | ||
| }, | ||
| (error: unknown) => { | ||
| clearTimeout(timeout); | ||
| reject(error); | ||
| }, | ||
| ); | ||
| }); | ||
|
Check warning on line 103 in packages/daemon/src/server/http/daemon-lease-finalizer.ts
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.