@ultimat3/jobs 31.4.1 → 31.5.0

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 (2) hide show
  1. package/package.json +7 -7
  2. package/src/limits.ts +21 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/jobs",
3
- "version": "31.4.1",
3
+ "version": "31.5.0",
4
4
  "description": "Durable background work: steps, transactional outbox, cron tasks, one driver interface",
5
5
  "keywords": [
6
6
  "ultimate",
@@ -45,11 +45,11 @@
45
45
  "test": "bun test"
46
46
  },
47
47
  "dependencies": {
48
- "@ultimat3/core": "31.4.1",
49
- "@ultimat3/db": "31.4.1",
50
- "@ultimat3/entity": "31.4.1",
51
- "@ultimat3/http": "31.4.1",
52
- "@ultimat3/schema": "31.4.1",
53
- "@ultimat3/time": "31.4.1"
48
+ "@ultimat3/core": "31.5.0",
49
+ "@ultimat3/db": "31.5.0",
50
+ "@ultimat3/entity": "31.5.0",
51
+ "@ultimat3/http": "31.5.0",
52
+ "@ultimat3/schema": "31.5.0",
53
+ "@ultimat3/time": "31.5.0"
54
54
  }
55
55
  }
package/src/limits.ts CHANGED
@@ -136,6 +136,17 @@ const compact = (window: StartWindow): void => {
136
136
  window.head = 0;
137
137
  };
138
138
 
139
+ /**
140
+ * Move the head past every stamp at or before `cutoff`. Spent stamps leave from the HEAD, never by
141
+ * copying the window: a filter per acquire made a window of n stamps O(n) a claim — 20k acquires
142
+ * were 3.9 s of event loop.
143
+ */
144
+ const expire = (window: StartWindow, cutoff: number): void => {
145
+ while (window.head < window.stamps.length && (window.stamps[window.head] ?? 0) <= cutoff) {
146
+ window.head += 1;
147
+ }
148
+ };
149
+
139
150
  /** Tenant key resolution. Structural on purpose: `jobs` must not import the auth types. */
140
151
  export function tenantKeyFrom(actor: { readonly orgId?: string } | undefined): string {
141
152
  return actor?.orgId ?? NO_TENANT;
@@ -221,13 +232,19 @@ export function concurrencyLimiter(
221
232
  * does the cap evict live state, and then the LEAST throttled tenants go first — discarding a
222
233
  * full window is what would hand a tenant a free rate reset, which is the order
223
234
  * `@ultimat3/http`'s `memoryRateLimitStore` evicts in and for the same reason.
235
+ *
236
+ * The drop, stated: past `maxTenants` tenants with a LIVE window, an evicted tenant regains the
237
+ * starts its window still held — at most `ratePerTenant.limit`, and by this order the fewest any
238
+ * tenant could. That is the price of a bounded map; the fleet gate is `JobDriver.leases`.
224
239
  */
225
240
  const sweep = (at: number): void => {
226
241
  lastSweepMs = at;
242
+ // Expired here, not only when the tenant's own key is next claimed: the cap below ranks by
243
+ // `live`, and an unread expired prefix made a mostly-spent window look like the busiest one.
227
244
  for (const [tenant, window] of starts) {
228
- if (live(window) === 0 || (window.stamps.at(-1) ?? 0) <= at - rateWindowMs) {
229
- starts.delete(tenant);
230
- }
245
+ expire(window, at - rateWindowMs);
246
+ if (live(window) === 0) starts.delete(tenant);
247
+ else compact(window);
231
248
  }
232
249
  for (const [key, refusal] of refusals) {
233
250
  if (refusal.atMs <= at - REFUSAL_TTL_MS) refusals.delete(key);
@@ -263,14 +280,7 @@ export function concurrencyLimiter(
263
280
  if (rate === undefined) return false;
264
281
  const window = starts.get(tenant);
265
282
  if (window === undefined) return rate.limit <= 0;
266
- // Spent stamps leave from the HEAD, never by copying the window: a filter per acquire made a
267
- // window of n stamps O(n) a claim — 20k acquires were 3.9 s of event loop.
268
- while (
269
- window.head < window.stamps.length &&
270
- (window.stamps[window.head] ?? 0) <= at - rate.windowMs
271
- ) {
272
- window.head += 1;
273
- }
283
+ expire(window, at - rate.windowMs);
274
284
  // An empty window is a tenant with no rate state at all — deleted rather than kept, which is
275
285
  // what kept one array per org that ever ran a job.
276
286
  if (live(window) === 0) {