cartethyia 2.6.5 → 2.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dashboard/assets/{BannedPage-B7OjIxCp.js → BannedPage-Dskhyp0l.js} +1 -1
- package/dist/dashboard/assets/BannedPage-Dskhyp0l.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-Bt-6y24w.js → CliToolDetailPage--SJhk7qQ.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage--SJhk7qQ.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-C6LunsuE.js → CliToolsPage-C203VT-c.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-C203VT-c.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-CgyfN2Yy.js → CombosPage-BcS7kGG1.js} +1 -1
- package/dist/dashboard/assets/CombosPage-BcS7kGG1.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-BHWU7y6j.js → ConsoleLogPage-ChmiSLwO.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-ChmiSLwO.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-xIjU736m.js → CustomizationPage-uujbBYwC.js} +1 -1
- package/dist/dashboard/assets/{CustomizationPage-xIjU736m.js.gz → CustomizationPage-uujbBYwC.js.gz} +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-DeT1zkYG.js → HealthEventsModal-fKAWd-OQ.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-fKAWd-OQ.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-ChVqIAam.js → LoginPage-DoOUthbG.js} +1 -1
- package/dist/dashboard/assets/LoginPage-DoOUthbG.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-BD4GkRtS.js → OverviewPage-C5Z-UBz1.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-C5Z-UBz1.js.gz +0 -0
- package/dist/dashboard/assets/{ProviderDetailPage-BJXwqpKj.js → ProviderDetailPage-CFu1qGcT.js} +1 -1
- package/dist/dashboard/assets/{ProviderDetailPage-BJXwqpKj.js.gz → ProviderDetailPage-CFu1qGcT.js.gz} +0 -0
- package/dist/dashboard/assets/{ProvidersPage-Ck64riM5.js → ProvidersPage-CnlJZbxF.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-CnlJZbxF.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-Br_Leqq0.js → ProxyPage-B9mBTweh.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-B9mBTweh.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-D2gyhfa8.js → QuotaPage-BueSWX5D.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-BueSWX5D.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-1dbYb8cM.js → SettingsPage-B0x00MVY.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-B0x00MVY.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-DUTFy9wH.js → SetupPage-DQ9oDRpi.js} +1 -1
- package/dist/dashboard/assets/SetupPage-DQ9oDRpi.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-BZEhfDpZ.js → SortableList-DrKeMcGN.js} +1 -1
- package/dist/dashboard/assets/SortableList-DrKeMcGN.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-GBDtsJjh.js → StudioPage-qjuYtccc.js} +1 -1
- package/dist/dashboard/assets/StudioPage-qjuYtccc.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-CxidMvxq.js → UsagePage-Be7aNviQ.js} +1 -1
- package/dist/dashboard/assets/UsagePage-Be7aNviQ.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BrksMru8.js → entry-DOddwoc0.js} +3 -3
- package/dist/dashboard/assets/entry-DOddwoc0.js.gz +0 -0
- package/dist/dashboard/assets/{index-DU61iviS.js → index-BeLRhsmt.js} +2 -2
- package/dist/dashboard/assets/index-BeLRhsmt.js.gz +0 -0
- package/dist/dashboard/assets/{select-Ciq_KFQ1.js → select-BfZ3SCTM.js} +1 -1
- package/dist/dashboard/assets/select-BfZ3SCTM.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +21 -21
- package/package.json +1 -1
- package/scripts/cli/tray.ts +34 -10
- package/src/providers/operations/account-health-service.ts +52 -3
- package/src/transport/dispatch/streaming/finalize.ts +64 -2
- package/src/workers/scheduler.ts +73 -6
- package/dist/dashboard/assets/BannedPage-B7OjIxCp.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-Bt-6y24w.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-C6LunsuE.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CgyfN2Yy.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-BHWU7y6j.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-DeT1zkYG.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-ChVqIAam.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BD4GkRtS.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Ck64riM5.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-Br_Leqq0.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-D2gyhfa8.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-1dbYb8cM.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-DUTFy9wH.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-BZEhfDpZ.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-GBDtsJjh.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-CxidMvxq.js.gz +0 -0
- package/dist/dashboard/assets/entry-BrksMru8.js.gz +0 -0
- package/dist/dashboard/assets/index-DU61iviS.js.gz +0 -0
- package/dist/dashboard/assets/select-Ciq_KFQ1.js.gz +0 -0
|
@@ -96,6 +96,17 @@ export interface StreamFinalizeInput {
|
|
|
96
96
|
* stream back into the error path and emitting duplicate telemetry rows (and
|
|
97
97
|
* a second resource release) for one request.
|
|
98
98
|
*/
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* How long a deferred release waits for the reader it was handed to.
|
|
102
|
+
*
|
|
103
|
+
* Only a backstop: the pull or `attachController` it defers to normally
|
|
104
|
+
* releases first. Long enough that a reader mid-write is never cut off, short
|
|
105
|
+
* enough that a reader which never runs does not leave the flight registered
|
|
106
|
+
* indefinitely.
|
|
107
|
+
*/
|
|
108
|
+
const DEFERRED_RELEASE_BACKSTOP_MS = 30_000;
|
|
109
|
+
|
|
99
110
|
export class StreamFinalizer {
|
|
100
111
|
/**
|
|
101
112
|
* True only while a `pull()` is awaiting the upstream iterator. A watchdog
|
|
@@ -105,6 +116,8 @@ export class StreamFinalizer {
|
|
|
105
116
|
* abort handler instead.
|
|
106
117
|
*/
|
|
107
118
|
pullActive = false;
|
|
119
|
+
/** Backstop timer for an abort whose release was deferred to another reader. */
|
|
120
|
+
private releaseBackstop: ReturnType<typeof setTimeout> | undefined;
|
|
108
121
|
|
|
109
122
|
/**
|
|
110
123
|
* Max time between upstream events before the stream is declared stalled,
|
|
@@ -183,6 +196,29 @@ export class StreamFinalizer {
|
|
|
183
196
|
this.stallTimer = undefined;
|
|
184
197
|
}
|
|
185
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Guarantees {@link releaseStreamResources} runs after a deferred abort.
|
|
201
|
+
*
|
|
202
|
+
* Two branches of `onStreamAbort` hand the release to some other reader — an
|
|
203
|
+
* active pull, or `attachController` — and return. When that reader never
|
|
204
|
+
* runs, nothing releases: the flight stays in the in-flight registry and the
|
|
205
|
+
* gauge never returns to zero, which is indistinguishable from permanent
|
|
206
|
+
* load. A server-side abort (deadline, 504) is the case that reaches them,
|
|
207
|
+
* because only a client disconnect is handled inline.
|
|
208
|
+
*
|
|
209
|
+
* The timer is unref'd so it cannot keep the process alive during shutdown,
|
|
210
|
+
* and `releaseStreamResources` is idempotent (`streamReleased`), so a pull
|
|
211
|
+
* that does settle first simply makes this a no-op.
|
|
212
|
+
*/
|
|
213
|
+
private armDeferredReleaseBackstop(): void {
|
|
214
|
+
if (this.releaseBackstop !== undefined) return;
|
|
215
|
+
this.releaseBackstop = setTimeout(() => {
|
|
216
|
+
this.releaseBackstop = undefined;
|
|
217
|
+
void this.releaseStreamResources();
|
|
218
|
+
}, DEFERRED_RELEASE_BACKSTOP_MS);
|
|
219
|
+
this.releaseBackstop.unref?.();
|
|
220
|
+
}
|
|
221
|
+
|
|
186
222
|
/**
|
|
187
223
|
* Attaches the stream controller so an abort can settle even when no pull is
|
|
188
224
|
* currently pending (for example, the client stopped reading between chunks).
|
|
@@ -257,10 +293,28 @@ export class StreamFinalizer {
|
|
|
257
293
|
// Let an active pull emit the terminal frame for a server drain or
|
|
258
294
|
// deadline. With no pull, no reader will handle it, so this listener must
|
|
259
295
|
// emit and close through the attached controller itself.
|
|
260
|
-
|
|
296
|
+
// Deferring to the pull assumes the pull will settle and release. It may
|
|
297
|
+
// not: a pull awaits the upstream iterator, and an upstream that went
|
|
298
|
+
// quiet after the abort leaves that await pending — so nothing ever
|
|
299
|
+
// reaches `releaseStreamResources()` and the flight stays registered
|
|
300
|
+
// forever. A server-side abort (deadline, 504) is exactly the case, since
|
|
301
|
+
// only a client disconnect takes the path below.
|
|
302
|
+
//
|
|
303
|
+
// Arm a backstop instead of returning bare. If the pull does settle it
|
|
304
|
+
// releases first and this becomes a no-op, so the normal path is
|
|
305
|
+
// unchanged; if it never does, the flight is still reaped.
|
|
306
|
+
if (this.pullActive && !clientDisconnect) {
|
|
307
|
+
this.armDeferredReleaseBackstop();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
261
310
|
// Before ReadableStream.start attaches its controller, defer server-side
|
|
262
311
|
// failures so attachController can deliver the typed terminal frame.
|
|
263
|
-
if (!clientDisconnect && this.streamController === undefined)
|
|
312
|
+
if (!clientDisconnect && this.streamController === undefined) {
|
|
313
|
+
// Same assumption, same consequence: attachController is expected to
|
|
314
|
+
// deliver the frame and release. Nothing guarantees it runs.
|
|
315
|
+
this.armDeferredReleaseBackstop();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
264
318
|
if (clientDisconnect) {
|
|
265
319
|
const { candidate } = this.input;
|
|
266
320
|
if (!state.outcome) {
|
|
@@ -336,6 +390,14 @@ export class StreamFinalizer {
|
|
|
336
390
|
if (this.streamReleased) return;
|
|
337
391
|
this.streamReleased = true;
|
|
338
392
|
const { state, deps, prepared, lease, reservation, proxySlot } = this.input;
|
|
393
|
+
// The deferred-release backstop has done its job — or the reader it
|
|
394
|
+
// deferred to beat it here. Either way the timer must not outlive the
|
|
395
|
+
// release, and an unref'd timer left armed would fire into a released
|
|
396
|
+
// finalizer.
|
|
397
|
+
if (this.releaseBackstop !== undefined) {
|
|
398
|
+
clearTimeout(this.releaseBackstop);
|
|
399
|
+
this.releaseBackstop = undefined;
|
|
400
|
+
}
|
|
339
401
|
state.abortController.signal.removeEventListener("abort", this.handleAbort);
|
|
340
402
|
this.streamController = undefined;
|
|
341
403
|
this.clearClientKeepaliveTimer();
|
package/src/workers/scheduler.ts
CHANGED
|
@@ -72,6 +72,37 @@ export async function runGrowingWaves<T>(
|
|
|
72
72
|
|
|
73
73
|
export const DEFAULT_INITIAL_BATCH_SIZE = 2;
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* How long a tick may wait for the maintenance lease.
|
|
77
|
+
*
|
|
78
|
+
* `tryAcquire` is documented as one attempt that skips rather than waits, but
|
|
79
|
+
* nothing bounded it: a Redis call that never answered held the tick open
|
|
80
|
+
* indefinitely. This restores the documented behaviour by force-skipping, so a
|
|
81
|
+
* stalled lease store costs one interval instead of every future one.
|
|
82
|
+
*/
|
|
83
|
+
const LEASE_ACQUIRE_TIMEOUT_MS = 5_000;
|
|
84
|
+
|
|
85
|
+
/** Resolves with the value, or rejects if `promise` has not settled in time. */
|
|
86
|
+
async function withDeadline<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
|
87
|
+
// `clearTimeout` no-ops for an absent handle, so it needs no guard. A live
|
|
88
|
+
// timer would hold the process open, hence the unref.
|
|
89
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
90
|
+
try {
|
|
91
|
+
return await Promise.race([
|
|
92
|
+
promise,
|
|
93
|
+
new Promise<never>((_, reject) => {
|
|
94
|
+
timer = setTimeout(
|
|
95
|
+
() => reject(new Error(`lease acquisition did not settle within ${timeoutMs}ms`)),
|
|
96
|
+
timeoutMs,
|
|
97
|
+
);
|
|
98
|
+
timer.unref?.();
|
|
99
|
+
}),
|
|
100
|
+
]);
|
|
101
|
+
} finally {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
75
106
|
/**
|
|
76
107
|
* Tasks may be registered at any time: before `start()`, they are scheduled
|
|
77
108
|
* when `start()` runs; after `start()`, each `register()` immediately schedules
|
|
@@ -180,6 +211,18 @@ export class ScheduledTaskRegistry {
|
|
|
180
211
|
return this.running.has(name);
|
|
181
212
|
}
|
|
182
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Clears the shared gate so other names may run.
|
|
216
|
+
*
|
|
217
|
+
* Every path that latches the gate and then does not run the task must reach
|
|
218
|
+
* this: a latch that is never cleared stops every later tick of every task,
|
|
219
|
+
* because the gate is process-wide rather than per-name.
|
|
220
|
+
*/
|
|
221
|
+
private releaseGate(name: string): void {
|
|
222
|
+
this.running.delete(name);
|
|
223
|
+
if (this.busy === name) this.busy = undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
183
226
|
/** Runs one named task immediately, bypassing its interval — for tests and manual triggers. */
|
|
184
227
|
async runNow(name: string): Promise<boolean> {
|
|
185
228
|
const task = this.tasks.find((t) => t.name === name);
|
|
@@ -220,13 +263,37 @@ export class ScheduledTaskRegistry {
|
|
|
220
263
|
if (this.busy !== undefined) return;
|
|
221
264
|
this.running.add(task.name);
|
|
222
265
|
this.busy = task.name;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
266
|
+
let lease: Awaited<ReturnType<MaintenanceLeaseStore["tryAcquire"]>> | undefined;
|
|
267
|
+
try {
|
|
268
|
+
// The gate above is latched before this await so a concurrent tick cannot
|
|
269
|
+
// slip past while the lease is in flight. It is bounded: a lease store
|
|
270
|
+
// that never answers used to latch `busy` forever, and since the gate is
|
|
271
|
+
// shared across every task name, one stalled acquisition stopped the
|
|
272
|
+
// whole registry — including the sweep whose job is to clean up after
|
|
273
|
+
// something that never finishes.
|
|
274
|
+
lease =
|
|
275
|
+
this.lease !== undefined && task.shared === true
|
|
276
|
+
? await withDeadline(
|
|
277
|
+
this.lease.tryAcquire(task.name, leaseTtlMs(task.intervalMs)),
|
|
278
|
+
LEASE_ACQUIRE_TIMEOUT_MS,
|
|
279
|
+
)
|
|
280
|
+
: undefined;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
// A lease store that cannot answer is not this task's failure to run;
|
|
283
|
+
// without this the error propagated out of `runOnce` and the tick was
|
|
284
|
+
// lost with it.
|
|
285
|
+
log.error(`[scheduled-task:${task.name}] lease acquisition failed`, error as Error);
|
|
286
|
+
}
|
|
287
|
+
if (lease === undefined && this.lease !== undefined && task.shared === true) {
|
|
288
|
+
// Could not establish a lease: skip this tick rather than run the task
|
|
289
|
+
// unguarded, which is what sharing the lease was for.
|
|
290
|
+
this.skippedByLease.set(task.name, (this.skippedByLease.get(task.name) ?? 0) + 1);
|
|
291
|
+
this.lastSkipAt.set(task.name, this.now());
|
|
292
|
+
this.releaseGate(task.name);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
227
295
|
if (lease !== undefined && !lease.held) {
|
|
228
|
-
this.
|
|
229
|
-
this.busy = undefined;
|
|
296
|
+
this.releaseGate(task.name);
|
|
230
297
|
this.skippedByLease.set(task.name, (this.skippedByLease.get(task.name) ?? 0) + 1);
|
|
231
298
|
this.lastSkipAt.set(task.name, this.now());
|
|
232
299
|
return;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|