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.
Files changed (69) hide show
  1. package/dist/dashboard/assets/{BannedPage-B7OjIxCp.js → BannedPage-Dskhyp0l.js} +1 -1
  2. package/dist/dashboard/assets/BannedPage-Dskhyp0l.js.gz +0 -0
  3. package/dist/dashboard/assets/{CliToolDetailPage-Bt-6y24w.js → CliToolDetailPage--SJhk7qQ.js} +1 -1
  4. package/dist/dashboard/assets/CliToolDetailPage--SJhk7qQ.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolsPage-C6LunsuE.js → CliToolsPage-C203VT-c.js} +1 -1
  6. package/dist/dashboard/assets/CliToolsPage-C203VT-c.js.gz +0 -0
  7. package/dist/dashboard/assets/{CombosPage-CgyfN2Yy.js → CombosPage-BcS7kGG1.js} +1 -1
  8. package/dist/dashboard/assets/CombosPage-BcS7kGG1.js.gz +0 -0
  9. package/dist/dashboard/assets/{ConsoleLogPage-BHWU7y6j.js → ConsoleLogPage-ChmiSLwO.js} +1 -1
  10. package/dist/dashboard/assets/ConsoleLogPage-ChmiSLwO.js.gz +0 -0
  11. package/dist/dashboard/assets/{CustomizationPage-xIjU736m.js → CustomizationPage-uujbBYwC.js} +1 -1
  12. package/dist/dashboard/assets/{CustomizationPage-xIjU736m.js.gz → CustomizationPage-uujbBYwC.js.gz} +0 -0
  13. package/dist/dashboard/assets/{HealthEventsModal-DeT1zkYG.js → HealthEventsModal-fKAWd-OQ.js} +1 -1
  14. package/dist/dashboard/assets/HealthEventsModal-fKAWd-OQ.js.gz +0 -0
  15. package/dist/dashboard/assets/{LoginPage-ChVqIAam.js → LoginPage-DoOUthbG.js} +1 -1
  16. package/dist/dashboard/assets/LoginPage-DoOUthbG.js.gz +0 -0
  17. package/dist/dashboard/assets/{OverviewPage-BD4GkRtS.js → OverviewPage-C5Z-UBz1.js} +1 -1
  18. package/dist/dashboard/assets/OverviewPage-C5Z-UBz1.js.gz +0 -0
  19. package/dist/dashboard/assets/{ProviderDetailPage-BJXwqpKj.js → ProviderDetailPage-CFu1qGcT.js} +1 -1
  20. package/dist/dashboard/assets/{ProviderDetailPage-BJXwqpKj.js.gz → ProviderDetailPage-CFu1qGcT.js.gz} +0 -0
  21. package/dist/dashboard/assets/{ProvidersPage-Ck64riM5.js → ProvidersPage-CnlJZbxF.js} +1 -1
  22. package/dist/dashboard/assets/ProvidersPage-CnlJZbxF.js.gz +0 -0
  23. package/dist/dashboard/assets/{ProxyPage-Br_Leqq0.js → ProxyPage-B9mBTweh.js} +1 -1
  24. package/dist/dashboard/assets/ProxyPage-B9mBTweh.js.gz +0 -0
  25. package/dist/dashboard/assets/{QuotaPage-D2gyhfa8.js → QuotaPage-BueSWX5D.js} +1 -1
  26. package/dist/dashboard/assets/QuotaPage-BueSWX5D.js.gz +0 -0
  27. package/dist/dashboard/assets/{SettingsPage-1dbYb8cM.js → SettingsPage-B0x00MVY.js} +1 -1
  28. package/dist/dashboard/assets/SettingsPage-B0x00MVY.js.gz +0 -0
  29. package/dist/dashboard/assets/{SetupPage-DUTFy9wH.js → SetupPage-DQ9oDRpi.js} +1 -1
  30. package/dist/dashboard/assets/SetupPage-DQ9oDRpi.js.gz +0 -0
  31. package/dist/dashboard/assets/{SortableList-BZEhfDpZ.js → SortableList-DrKeMcGN.js} +1 -1
  32. package/dist/dashboard/assets/SortableList-DrKeMcGN.js.gz +0 -0
  33. package/dist/dashboard/assets/{StudioPage-GBDtsJjh.js → StudioPage-qjuYtccc.js} +1 -1
  34. package/dist/dashboard/assets/StudioPage-qjuYtccc.js.gz +0 -0
  35. package/dist/dashboard/assets/{UsagePage-CxidMvxq.js → UsagePage-Be7aNviQ.js} +1 -1
  36. package/dist/dashboard/assets/UsagePage-Be7aNviQ.js.gz +0 -0
  37. package/dist/dashboard/assets/{entry-BrksMru8.js → entry-DOddwoc0.js} +3 -3
  38. package/dist/dashboard/assets/entry-DOddwoc0.js.gz +0 -0
  39. package/dist/dashboard/assets/{index-DU61iviS.js → index-BeLRhsmt.js} +2 -2
  40. package/dist/dashboard/assets/index-BeLRhsmt.js.gz +0 -0
  41. package/dist/dashboard/assets/{select-Ciq_KFQ1.js → select-BfZ3SCTM.js} +1 -1
  42. package/dist/dashboard/assets/select-BfZ3SCTM.js.gz +0 -0
  43. package/dist/dashboard/index.html +1 -1
  44. package/dist/dashboard/index.html.gz +0 -0
  45. package/dist/dashboard/precompressed-assets.json +21 -21
  46. package/package.json +1 -1
  47. package/scripts/cli/tray.ts +34 -10
  48. package/src/providers/operations/account-health-service.ts +52 -3
  49. package/src/transport/dispatch/streaming/finalize.ts +64 -2
  50. package/src/workers/scheduler.ts +73 -6
  51. package/dist/dashboard/assets/BannedPage-B7OjIxCp.js.gz +0 -0
  52. package/dist/dashboard/assets/CliToolDetailPage-Bt-6y24w.js.gz +0 -0
  53. package/dist/dashboard/assets/CliToolsPage-C6LunsuE.js.gz +0 -0
  54. package/dist/dashboard/assets/CombosPage-CgyfN2Yy.js.gz +0 -0
  55. package/dist/dashboard/assets/ConsoleLogPage-BHWU7y6j.js.gz +0 -0
  56. package/dist/dashboard/assets/HealthEventsModal-DeT1zkYG.js.gz +0 -0
  57. package/dist/dashboard/assets/LoginPage-ChVqIAam.js.gz +0 -0
  58. package/dist/dashboard/assets/OverviewPage-BD4GkRtS.js.gz +0 -0
  59. package/dist/dashboard/assets/ProvidersPage-Ck64riM5.js.gz +0 -0
  60. package/dist/dashboard/assets/ProxyPage-Br_Leqq0.js.gz +0 -0
  61. package/dist/dashboard/assets/QuotaPage-D2gyhfa8.js.gz +0 -0
  62. package/dist/dashboard/assets/SettingsPage-1dbYb8cM.js.gz +0 -0
  63. package/dist/dashboard/assets/SetupPage-DUTFy9wH.js.gz +0 -0
  64. package/dist/dashboard/assets/SortableList-BZEhfDpZ.js.gz +0 -0
  65. package/dist/dashboard/assets/StudioPage-GBDtsJjh.js.gz +0 -0
  66. package/dist/dashboard/assets/UsagePage-CxidMvxq.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-BrksMru8.js.gz +0 -0
  68. package/dist/dashboard/assets/index-DU61iviS.js.gz +0 -0
  69. 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
- if (this.pullActive && !clientDisconnect) return;
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) return;
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();
@@ -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
- const lease =
224
- this.lease !== undefined && task.shared === true
225
- ? await this.lease.tryAcquire(task.name, leaseTtlMs(task.intervalMs))
226
- : undefined;
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.running.delete(task.name);
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;