@yielded/agent-storage-memory 0.1.0-beta.169 → 0.1.0-beta.170
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/MemoryMessageDeliveryStore.d.mts +1 -1
- package/dist/MemoryMessageDeliveryStore.mjs +28 -2
- package/dist/MemoryMessageDeliveryStore.mjs.map +1 -1
- package/dist/MemorySubmissionLedger.d.mts +1 -1
- package/dist/MemorySubscriptionStore.mjs +94 -18
- package/dist/MemorySubscriptionStore.mjs.map +1 -1
- package/dist/MemoryThreadStore.d.mts +1 -1
- package/dist/MemoryThreadStore.mjs +2 -9
- package/dist/MemoryThreadStore.mjs.map +1 -1
- package/dist/{MemoryThreadStoreKernel-DYNDjlhO.d.mts → MemoryThreadStoreKernel-h_k2t46J.d.mts} +2 -2
- package/dist/WorkSelection-CmqTuws3.mjs.map +1 -1
- package/package.json +1 -1
- package/src/MemoryMessageDeliveryStore.ts +43 -2
- package/src/MemorySubscriptionStore.ts +252 -99
- package/src/MemoryThreadStore.ts +2 -8
- package/src/internal/MemoryThreadStoreKernel.ts +1 -1
|
@@ -3,6 +3,7 @@ import { compareScheduleNames } from "@yielded/agent/schedule-transition";
|
|
|
3
3
|
import {
|
|
4
4
|
AcceptedEvent,
|
|
5
5
|
DeliveryChange,
|
|
6
|
+
defaultSubscriptionLimits,
|
|
6
7
|
SubscriptionChange,
|
|
7
8
|
SourcePartition,
|
|
8
9
|
SubscriptionDelivery,
|
|
@@ -26,10 +27,17 @@ import {
|
|
|
26
27
|
validateEventRetention,
|
|
27
28
|
subscriptionCanSelect,
|
|
28
29
|
subscriptionDeliveryCanSelect,
|
|
30
|
+
subscriptionUsesCapacity,
|
|
31
|
+
subscriptionDeliveryUsesCapacity,
|
|
29
32
|
} from "@yielded/agent/subscription-transition";
|
|
30
33
|
import { Clock, Effect, Layer, Ref, Result, Schema } from "effect";
|
|
31
34
|
|
|
32
|
-
interface
|
|
35
|
+
interface RegistrationCapacity {
|
|
36
|
+
readonly registrationExpiries: ReadonlyArray<number>;
|
|
37
|
+
readonly ownerRegistrationExpiries: ReadonlyMap<string, ReadonlyArray<number>>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface MemorySubscriptionState extends RegistrationCapacity {
|
|
33
41
|
readonly sequence: number;
|
|
34
42
|
readonly retentionDeadline: number | null;
|
|
35
43
|
readonly tombstoneCount: number;
|
|
@@ -45,10 +53,11 @@ interface MemorySubscriptionState {
|
|
|
45
53
|
readonly deliveries: ReadonlyMap<string, string>;
|
|
46
54
|
readonly registrationIndex: ReadonlyMap<string, RegistrationIndex>;
|
|
47
55
|
readonly candidateIndex: ReadonlyMap<string, ReadonlyArray<string>>;
|
|
48
|
-
readonly ownerRegistrationCounts: ReadonlyMap<string, number>;
|
|
49
56
|
readonly eventIndex: ReadonlyMap<string, EventIndex>;
|
|
50
57
|
readonly deliveryIndex: ReadonlyMap<string, DeliveryIndex>;
|
|
51
58
|
readonly ownerDeliveryCounts: ReadonlyMap<string, number>;
|
|
59
|
+
readonly liveDeliveryCount: number;
|
|
60
|
+
readonly pendingEventCount: number;
|
|
52
61
|
readonly scanCursors: SubscriptionScanCursors;
|
|
53
62
|
}
|
|
54
63
|
|
|
@@ -180,6 +189,71 @@ const insertKey = (keys: ReadonlyArray<string>, key: string): ReadonlyArray<stri
|
|
|
180
189
|
return [...keys.slice(0, at), key, ...keys.slice(at)];
|
|
181
190
|
};
|
|
182
191
|
|
|
192
|
+
// Count unexpired registrations without reading retained history. Keeping sorted expiry values
|
|
193
|
+
// also makes clock changes safe; expiry alone never deletes or rewrites the canonical record.
|
|
194
|
+
const expiryUpperBound = (expiries: ReadonlyArray<number>, nowMillis: number): number => {
|
|
195
|
+
let low = 0;
|
|
196
|
+
let high = expiries.length;
|
|
197
|
+
|
|
198
|
+
while (low < high) {
|
|
199
|
+
const middle = Math.floor((low + high) / 2);
|
|
200
|
+
const expiry = expiries[middle];
|
|
201
|
+
|
|
202
|
+
if (expiry !== undefined && expiry <= nowMillis) low = middle + 1;
|
|
203
|
+
else high = middle;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return low;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const registrationCount = (
|
|
210
|
+
current: MemorySubscriptionState,
|
|
211
|
+
nowMillis: number,
|
|
212
|
+
ownerId?: string,
|
|
213
|
+
): number => {
|
|
214
|
+
const expiries =
|
|
215
|
+
ownerId === undefined
|
|
216
|
+
? current.registrationExpiries
|
|
217
|
+
: (current.ownerRegistrationExpiries.get(ownerId) ?? []);
|
|
218
|
+
|
|
219
|
+
return expiries.length - expiryUpperBound(expiries, nowMillis);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const registrationExpiriesAfter = (
|
|
223
|
+
current: RegistrationCapacity,
|
|
224
|
+
before: SubscriptionRecord | undefined,
|
|
225
|
+
after: SubscriptionRecord,
|
|
226
|
+
): RegistrationCapacity => {
|
|
227
|
+
const expiry = (record: SubscriptionRecord | undefined) =>
|
|
228
|
+
record !== undefined && (record.state === "active" || record.state === "paused")
|
|
229
|
+
? (record.configuration.expiresAtMillis ?? Number.POSITIVE_INFINITY)
|
|
230
|
+
: null;
|
|
231
|
+
|
|
232
|
+
const previous = expiry(before);
|
|
233
|
+
const next = expiry(after);
|
|
234
|
+
|
|
235
|
+
const replace = (expiries: ReadonlyArray<number>): ReadonlyArray<number> => {
|
|
236
|
+
if (previous === next) return expiries;
|
|
237
|
+
const updated = [...expiries];
|
|
238
|
+
|
|
239
|
+
if (previous !== null) updated.splice(expiryUpperBound(updated, previous) - 1, 1);
|
|
240
|
+
if (next !== null) updated.splice(expiryUpperBound(updated, next), 0, next);
|
|
241
|
+
|
|
242
|
+
return updated;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const owners = new Map(current.ownerRegistrationExpiries);
|
|
246
|
+
const expiries = replace(owners.get(after.key.ownerId) ?? []);
|
|
247
|
+
|
|
248
|
+
if (expiries.length === 0) owners.delete(after.key.ownerId);
|
|
249
|
+
else owners.set(after.key.ownerId, expiries);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
registrationExpiries: replace(current.registrationExpiries),
|
|
253
|
+
ownerRegistrationExpiries: owners,
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
|
|
183
257
|
const recoveryCountsAfter = (
|
|
184
258
|
current: MemorySubscriptionState,
|
|
185
259
|
next: ReadonlyMap<string, RegistrationIndex>,
|
|
@@ -218,10 +292,13 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
218
292
|
deliveries: new Map(),
|
|
219
293
|
registrationIndex: new Map(),
|
|
220
294
|
candidateIndex: new Map(),
|
|
221
|
-
|
|
295
|
+
registrationExpiries: [],
|
|
296
|
+
ownerRegistrationExpiries: new Map(),
|
|
222
297
|
eventIndex: new Map(),
|
|
223
298
|
deliveryIndex: new Map(),
|
|
224
299
|
ownerDeliveryCounts: new Map(),
|
|
300
|
+
liveDeliveryCount: 0,
|
|
301
|
+
pendingEventCount: 0,
|
|
225
302
|
scanCursors: { events: "", deliveries: "", recovery: 0 },
|
|
226
303
|
});
|
|
227
304
|
|
|
@@ -247,6 +324,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
247
324
|
|
|
248
325
|
yield* requirePartition(record.key, "register-partition");
|
|
249
326
|
yield* failpoint.hit("subscription:register:before");
|
|
327
|
+
const nowMillis = yield* Clock.currentTimeMillis;
|
|
250
328
|
|
|
251
329
|
const result = yield* Effect.uninterruptible(
|
|
252
330
|
Ref.modify(
|
|
@@ -279,11 +357,16 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
279
357
|
limits.maxLifetimeMillis
|
|
280
358
|
)
|
|
281
359
|
return [Result.fail(error("capacity", "lifetime")), current];
|
|
282
|
-
if (
|
|
360
|
+
if (
|
|
361
|
+
subscriptionUsesCapacity(record, nowMillis) &&
|
|
362
|
+
registrationCount(current, nowMillis) >= limits.maxRegistrations
|
|
363
|
+
)
|
|
283
364
|
return [Result.fail(error("capacity", "registrations")), current];
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
365
|
+
if (
|
|
366
|
+
subscriptionUsesCapacity(record, nowMillis) &&
|
|
367
|
+
registrationCount(current, nowMillis, record.key.ownerId) >=
|
|
368
|
+
limits.maxRegistrationsPerOwner
|
|
369
|
+
)
|
|
287
370
|
return [Result.fail(error("capacity", "owner-registrations")), current];
|
|
288
371
|
const assigned = { ...record, ordinal: current.sequence + 1 };
|
|
289
372
|
const encoded = encode(SubscriptionRecord, assigned, "register-encode");
|
|
@@ -305,9 +388,6 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
305
388
|
const candidateKey = candidateIndexKey(assigned);
|
|
306
389
|
|
|
307
390
|
candidateIndex.set(candidateKey, [...(candidateIndex.get(candidateKey) ?? []), key]);
|
|
308
|
-
const ownerRegistrationCounts = new Map(current.ownerRegistrationCounts);
|
|
309
|
-
|
|
310
|
-
ownerRegistrationCounts.set(assigned.key.ownerId, ownerCount + 1);
|
|
311
391
|
|
|
312
392
|
return [
|
|
313
393
|
Result.succeed(assigned),
|
|
@@ -318,7 +398,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
318
398
|
registrationIndex,
|
|
319
399
|
recoveryCounts: recoveryCountsAfter(current, registrationIndex, [key]),
|
|
320
400
|
candidateIndex,
|
|
321
|
-
|
|
401
|
+
...registrationExpiriesAfter(current, undefined, assigned),
|
|
322
402
|
},
|
|
323
403
|
];
|
|
324
404
|
},
|
|
@@ -358,89 +438,105 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
358
438
|
},
|
|
359
439
|
);
|
|
360
440
|
|
|
361
|
-
const change: SubscriptionStore["Service"]["change"] = Effect.fnUntraced(
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
441
|
+
const change: SubscriptionStore["Service"]["change"] = Effect.fnUntraced(function* (
|
|
442
|
+
input,
|
|
443
|
+
expectedRevision,
|
|
444
|
+
inputChange,
|
|
445
|
+
inputLimits = defaultSubscriptionLimits,
|
|
446
|
+
) {
|
|
447
|
+
const key = yield* requireKey(input, "change-key");
|
|
448
|
+
const change = yield* validate(SubscriptionChange, inputChange, "change");
|
|
449
|
+
const limits = yield* validate(SubscriptionLimits, inputLimits, "change-limits");
|
|
450
|
+
|
|
451
|
+
yield* validate(Schema.Int.check(Schema.isGreaterThan(0)), expectedRevision, "revision");
|
|
452
|
+
yield* failpoint.hit("subscription:change:before");
|
|
453
|
+
const nowMillis = yield* Clock.currentTimeMillis;
|
|
454
|
+
|
|
455
|
+
const updated = yield* Effect.uninterruptible(
|
|
456
|
+
Ref.modify(
|
|
457
|
+
state,
|
|
458
|
+
(
|
|
459
|
+
current,
|
|
460
|
+
): readonly [
|
|
461
|
+
Result.Result<SubscriptionRecord, SubscriptionError>,
|
|
462
|
+
MemorySubscriptionState,
|
|
463
|
+
] => {
|
|
464
|
+
const storageKey = subscriptionKeyString(key);
|
|
465
|
+
const text = current.registrations.get(storageKey);
|
|
466
|
+
|
|
467
|
+
if (text === undefined) return [Result.fail(error("not-found", "subscription")), current];
|
|
468
|
+
const existing = decode(SubscriptionRecord, text, "change-record");
|
|
469
|
+
|
|
470
|
+
if (Result.isFailure(existing)) return [existing, current];
|
|
471
|
+
const updated = applySubscriptionChange(existing.success, expectedRevision, change);
|
|
472
|
+
|
|
473
|
+
if (Result.isFailure(updated)) return [updated, current];
|
|
474
|
+
if (
|
|
475
|
+
!subscriptionUsesCapacity(existing.success, nowMillis) &&
|
|
476
|
+
subscriptionUsesCapacity(updated.success, nowMillis)
|
|
477
|
+
) {
|
|
478
|
+
if (registrationCount(current, nowMillis) >= limits.maxRegistrations)
|
|
479
|
+
return [Result.fail(error("capacity", "registrations")), current];
|
|
480
|
+
if (
|
|
481
|
+
registrationCount(current, nowMillis, key.ownerId) >= limits.maxRegistrationsPerOwner
|
|
482
|
+
)
|
|
483
|
+
return [Result.fail(error("capacity", "owner-registrations")), current];
|
|
484
|
+
}
|
|
485
|
+
const revised = { ...updated.success, ordinal: current.sequence + 1 };
|
|
486
|
+
const encoded = encode(SubscriptionRecord, revised, "change-encode");
|
|
487
|
+
|
|
488
|
+
if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
|
|
489
|
+
const registrations = new Map(current.registrations);
|
|
490
|
+
|
|
491
|
+
registrations.set(storageKey, encoded.success);
|
|
492
|
+
const registrationIndex = new Map(current.registrationIndex);
|
|
493
|
+
|
|
494
|
+
registrationIndex.set(storageKey, {
|
|
495
|
+
key,
|
|
496
|
+
ordinal: revised.ordinal,
|
|
497
|
+
state: revised.state,
|
|
498
|
+
recoveryKey: revised.recovery === null ? null : candidateIndexKey(revised),
|
|
499
|
+
recoveryAt: revised.recovery?.nextAttemptAtMillis ?? null,
|
|
500
|
+
});
|
|
501
|
+
const candidateIndex = new Map(current.candidateIndex);
|
|
502
|
+
const oldKey = candidateIndexKey(existing.success);
|
|
503
|
+
const nextKey = candidateIndexKey(revised);
|
|
504
|
+
|
|
505
|
+
{
|
|
506
|
+
candidateIndex.set(
|
|
507
|
+
oldKey,
|
|
508
|
+
(candidateIndex.get(oldKey) ?? []).filter((key) => key !== storageKey),
|
|
509
|
+
);
|
|
510
|
+
candidateIndex.set(
|
|
511
|
+
nextKey,
|
|
512
|
+
[...(candidateIndex.get(nextKey) ?? []), storageKey].sort(
|
|
513
|
+
(a, b) =>
|
|
514
|
+
(registrationIndex.get(a)?.ordinal ?? 0) -
|
|
515
|
+
(registrationIndex.get(b)?.ordinal ?? 0),
|
|
516
|
+
),
|
|
517
|
+
);
|
|
518
|
+
}
|
|
408
519
|
|
|
520
|
+
return [
|
|
521
|
+
Result.succeed(revised),
|
|
409
522
|
{
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
candidateIndex
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
...current,
|
|
428
|
-
sequence: revised.ordinal,
|
|
429
|
-
registrations,
|
|
430
|
-
registrationIndex,
|
|
431
|
-
candidateIndex,
|
|
432
|
-
recoveryCounts: recoveryCountsAfter(current, registrationIndex, [storageKey]),
|
|
433
|
-
},
|
|
434
|
-
];
|
|
435
|
-
},
|
|
436
|
-
),
|
|
437
|
-
).pipe(Effect.flatMap(Effect.fromResult));
|
|
438
|
-
|
|
439
|
-
yield* failpoint.hit("subscription:change:after");
|
|
440
|
-
|
|
441
|
-
return updated;
|
|
442
|
-
},
|
|
443
|
-
);
|
|
523
|
+
...current,
|
|
524
|
+
sequence: revised.ordinal,
|
|
525
|
+
registrations,
|
|
526
|
+
registrationIndex,
|
|
527
|
+
candidateIndex,
|
|
528
|
+
...registrationExpiriesAfter(current, existing.success, revised),
|
|
529
|
+
recoveryCounts: recoveryCountsAfter(current, registrationIndex, [storageKey]),
|
|
530
|
+
},
|
|
531
|
+
];
|
|
532
|
+
},
|
|
533
|
+
),
|
|
534
|
+
).pipe(Effect.flatMap(Effect.fromResult));
|
|
535
|
+
|
|
536
|
+
yield* failpoint.hit("subscription:change:after");
|
|
537
|
+
|
|
538
|
+
return updated;
|
|
539
|
+
});
|
|
444
540
|
|
|
445
541
|
const cancel: SubscriptionStore["Service"]["cancel"] = Effect.fnUntraced(
|
|
446
542
|
function* (input, expectedRevision) {
|
|
@@ -518,6 +614,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
518
614
|
...current,
|
|
519
615
|
registrations,
|
|
520
616
|
registrationIndex,
|
|
617
|
+
...registrationExpiriesAfter(current, decoded.success, cancelled),
|
|
521
618
|
recoveryCounts: recoveryCountsAfter(current, registrationIndex, [storageKey]),
|
|
522
619
|
},
|
|
523
620
|
];
|
|
@@ -570,7 +667,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
570
667
|
if (Result.isFailure(horizon)) return [Result.fail(horizon.failure), current];
|
|
571
668
|
if (jsonBytes(event.payload) > limits.maxPayloadBytes)
|
|
572
669
|
return [Result.fail(error("capacity", "payload-bytes")), current];
|
|
573
|
-
if (current.
|
|
670
|
+
if (current.pendingEventCount >= limits.maxEvents)
|
|
574
671
|
return [Result.fail(error("capacity", "events")), current];
|
|
575
672
|
|
|
576
673
|
const accepted: AcceptedEvent = {
|
|
@@ -601,6 +698,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
601
698
|
sequence: accepted.cutoff,
|
|
602
699
|
events,
|
|
603
700
|
eventIndex,
|
|
701
|
+
pendingEventCount: current.pendingEventCount + 1,
|
|
604
702
|
eventKeys: insertKey(current.eventKeys, accepted.eventId),
|
|
605
703
|
retentionHorizon: limits.retention?.replayHorizonMillis ?? current.retentionHorizon,
|
|
606
704
|
retentionDeadline:
|
|
@@ -681,6 +779,11 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
681
779
|
"select-deliveries",
|
|
682
780
|
);
|
|
683
781
|
|
|
782
|
+
if (
|
|
783
|
+
new Set(deliveries.map((delivery) => subscriptionDeliveryKeyString(delivery.key))).size !==
|
|
784
|
+
deliveries.length
|
|
785
|
+
)
|
|
786
|
+
return yield* error("validation", "duplicate-delivery");
|
|
684
787
|
const limits = yield* validate(SubscriptionLimits, inputLimits, "select-limits");
|
|
685
788
|
|
|
686
789
|
yield* requirePartition(suppliedEvent, "select-partition");
|
|
@@ -724,6 +827,11 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
724
827
|
const registrationUpdates: Array<readonly [string, RegistrationIndex]> = [];
|
|
725
828
|
const owners = new Map(current.ownerDeliveryCounts);
|
|
726
829
|
|
|
830
|
+
let registrationCapacity: RegistrationCapacity = {
|
|
831
|
+
registrationExpiries: current.registrationExpiries,
|
|
832
|
+
ownerRegistrationExpiries: current.ownerRegistrationExpiries,
|
|
833
|
+
};
|
|
834
|
+
|
|
727
835
|
for (const delivery of deliveries) {
|
|
728
836
|
const recordText = current.registrations.get(
|
|
729
837
|
subscriptionKeyString(delivery.key.subscription),
|
|
@@ -800,9 +908,14 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
800
908
|
consumedKey,
|
|
801
909
|
{ ...indexed, state: "consumed", recoveryAt: null, recoveryKey: null },
|
|
802
910
|
]);
|
|
911
|
+
registrationCapacity = registrationExpiriesAfter(
|
|
912
|
+
registrationCapacity,
|
|
913
|
+
record.success,
|
|
914
|
+
consumed,
|
|
915
|
+
);
|
|
803
916
|
}
|
|
804
917
|
}
|
|
805
|
-
if (current.
|
|
918
|
+
if (current.liveDeliveryCount + additions.length > limits.maxDeliveries)
|
|
806
919
|
return [Result.fail(error("capacity", "deliveries")), current];
|
|
807
920
|
const registrations = new Map(current.registrations);
|
|
808
921
|
|
|
@@ -859,12 +972,15 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
859
972
|
events,
|
|
860
973
|
eventIndex,
|
|
861
974
|
deliveries: nextDeliveries,
|
|
975
|
+
liveDeliveryCount: current.liveDeliveryCount + additions.length,
|
|
976
|
+
pendingEventCount: current.pendingEventCount - Number(complete),
|
|
862
977
|
deliveryKeys: additions.reduce(
|
|
863
978
|
(keys, [key]) => insertKey(keys, key),
|
|
864
979
|
current.deliveryKeys,
|
|
865
980
|
),
|
|
866
981
|
deliveryIndex,
|
|
867
982
|
ownerDeliveryCounts: owners,
|
|
983
|
+
...registrationCapacity,
|
|
868
984
|
},
|
|
869
985
|
];
|
|
870
986
|
},
|
|
@@ -926,7 +1042,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
926
1042
|
}
|
|
927
1043
|
if (!subscriptionCanSelect(record.success, accepted.success, effectiveNowMillis, true))
|
|
928
1044
|
return [Result.fail(error("conflict", "catch-up-eligibility")), current];
|
|
929
|
-
if (current.
|
|
1045
|
+
if (current.liveDeliveryCount >= limits.maxDeliveries)
|
|
930
1046
|
return [Result.fail(error("capacity", "deliveries")), current];
|
|
931
1047
|
const ownerCount = current.ownerDeliveryCounts.get(record.success.key.ownerId) ?? 0;
|
|
932
1048
|
|
|
@@ -994,6 +1110,8 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
994
1110
|
deliveryIndex,
|
|
995
1111
|
deliveryKeys: insertKey(current.deliveryKeys, key),
|
|
996
1112
|
ownerDeliveryCounts,
|
|
1113
|
+
...registrationExpiriesAfter(current, record.success, consumed),
|
|
1114
|
+
liveDeliveryCount: current.liveDeliveryCount + 1,
|
|
997
1115
|
registrations,
|
|
998
1116
|
registrationIndex,
|
|
999
1117
|
recoveryCounts: recoveryCountsAfter(current, registrationIndex, [consumedKey]),
|
|
@@ -1106,10 +1224,11 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
1106
1224
|
);
|
|
1107
1225
|
|
|
1108
1226
|
const changeDelivery: SubscriptionStore["Service"]["changeDelivery"] = Effect.fnUntraced(
|
|
1109
|
-
function* (inputKey, inputDeliveryId, inputChange) {
|
|
1227
|
+
function* (inputKey, inputDeliveryId, inputChange, inputLimits = defaultSubscriptionLimits) {
|
|
1110
1228
|
const key = yield* validate(SubscriptionDeliveryKey, inputKey, "change-delivery-key");
|
|
1111
1229
|
const deliveryId = yield* validate(Digest, inputDeliveryId, "change-delivery-id");
|
|
1112
1230
|
const change = yield* validate(DeliveryChange, inputChange, "change-delivery-change");
|
|
1231
|
+
const limits = yield* validate(SubscriptionLimits, inputLimits, "change-delivery-limits");
|
|
1113
1232
|
|
|
1114
1233
|
yield* requirePartition(key.subscription, "change-delivery-partition");
|
|
1115
1234
|
yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:before`);
|
|
@@ -1162,6 +1281,20 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
1162
1281
|
if (transition.success === decoded.success)
|
|
1163
1282
|
return [Result.succeed(decoded.success), current];
|
|
1164
1283
|
const updated = transition.success;
|
|
1284
|
+
|
|
1285
|
+
const capacityDelta =
|
|
1286
|
+
Number(subscriptionDeliveryUsesCapacity(updated)) -
|
|
1287
|
+
Number(subscriptionDeliveryUsesCapacity(decoded.success));
|
|
1288
|
+
|
|
1289
|
+
const ownerId = key.subscription.ownerId;
|
|
1290
|
+
const ownerCount = current.ownerDeliveryCounts.get(ownerId) ?? 0;
|
|
1291
|
+
|
|
1292
|
+
if (capacityDelta > 0) {
|
|
1293
|
+
if (current.liveDeliveryCount >= limits.maxDeliveries)
|
|
1294
|
+
return [Result.fail(error("capacity", "deliveries")), current];
|
|
1295
|
+
if (ownerCount >= limits.maxDeliveriesPerOwner)
|
|
1296
|
+
return [Result.fail(error("capacity", "owner-deliveries")), current];
|
|
1297
|
+
}
|
|
1165
1298
|
const encoded = encode(SubscriptionDelivery, updated, "change-delivery-encode");
|
|
1166
1299
|
|
|
1167
1300
|
if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
|
|
@@ -1180,8 +1313,21 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
1180
1313
|
observeSettlement: updated.observeSettlement ?? false,
|
|
1181
1314
|
nextAttemptAtMillis: updated.retry.nextAttemptAtMillis,
|
|
1182
1315
|
});
|
|
1316
|
+
const ownerDeliveryCounts = new Map(current.ownerDeliveryCounts);
|
|
1317
|
+
|
|
1318
|
+
if (ownerCount + capacityDelta === 0) ownerDeliveryCounts.delete(ownerId);
|
|
1319
|
+
else ownerDeliveryCounts.set(ownerId, ownerCount + capacityDelta);
|
|
1183
1320
|
|
|
1184
|
-
return [
|
|
1321
|
+
return [
|
|
1322
|
+
Result.succeed(updated),
|
|
1323
|
+
{
|
|
1324
|
+
...current,
|
|
1325
|
+
deliveries,
|
|
1326
|
+
deliveryIndex,
|
|
1327
|
+
ownerDeliveryCounts,
|
|
1328
|
+
liveDeliveryCount: current.liveDeliveryCount + capacityDelta,
|
|
1329
|
+
},
|
|
1330
|
+
];
|
|
1185
1331
|
},
|
|
1186
1332
|
),
|
|
1187
1333
|
).pipe(Effect.flatMap(Effect.fromResult));
|
|
@@ -1350,6 +1496,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
1350
1496
|
const deliveryIndex = new Map(current.deliveryIndex);
|
|
1351
1497
|
const eventDeliveryCounts = new Map(current.eventDeliveryCounts);
|
|
1352
1498
|
const ownerDeliveryCounts = new Map(current.ownerDeliveryCounts);
|
|
1499
|
+
let liveDeliveryCount = current.liveDeliveryCount;
|
|
1353
1500
|
|
|
1354
1501
|
// Page indexes before decoding. Each pass reads at most 2 * limit event values and
|
|
1355
1502
|
// limit delivery values; relationship checks use maintained reference counts.
|
|
@@ -1432,9 +1579,14 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
1432
1579
|
event.eventId,
|
|
1433
1580
|
(eventDeliveryCounts.get(event.eventId) ?? 1) - 1,
|
|
1434
1581
|
);
|
|
1435
|
-
|
|
1582
|
+
if (subscriptionDeliveryUsesCapacity(delivery)) {
|
|
1583
|
+
liveDeliveryCount--;
|
|
1584
|
+
const owner = delivery.key.subscription.ownerId;
|
|
1585
|
+
const count = (ownerDeliveryCounts.get(owner) ?? 1) - 1;
|
|
1436
1586
|
|
|
1437
|
-
|
|
1587
|
+
if (count === 0) ownerDeliveryCounts.delete(owner);
|
|
1588
|
+
else ownerDeliveryCounts.set(owner, count);
|
|
1589
|
+
}
|
|
1438
1590
|
}
|
|
1439
1591
|
let tombstones = current.tombstoneCount;
|
|
1440
1592
|
|
|
@@ -1497,6 +1649,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition:
|
|
|
1497
1649
|
deliveryIndex,
|
|
1498
1650
|
eventDeliveryCounts,
|
|
1499
1651
|
ownerDeliveryCounts,
|
|
1652
|
+
liveDeliveryCount,
|
|
1500
1653
|
eventKeys: removeKeys(current.eventKeys, removedEvents),
|
|
1501
1654
|
deliveryKeys: removeKeys(current.deliveryKeys, removedDeliveries),
|
|
1502
1655
|
tombstoneCount: tombstones,
|
package/src/MemoryThreadStore.ts
CHANGED
|
@@ -185,7 +185,6 @@ interface StoredThread {
|
|
|
185
185
|
readonly applicationInputs: Array<CanonicalRecordEnvelope>;
|
|
186
186
|
readonly durableOwners: Array<CanonicalRecordEnvelope>;
|
|
187
187
|
readonly windowBoundaries: Map<string, Array<CanonicalRecordEnvelope>>;
|
|
188
|
-
readonly peerCount: number;
|
|
189
188
|
readonly workerRecords: Map<string, Array<CanonicalRecordEnvelope>>;
|
|
190
189
|
readonly byId: Map<string, CanonicalRecordEnvelope>;
|
|
191
190
|
readonly runInputs: Map<string, CanonicalRecordEnvelope | null>;
|
|
@@ -274,7 +273,6 @@ type NativeIndexes = Pick<
|
|
|
274
273
|
| "applicationInputs"
|
|
275
274
|
| "durableOwners"
|
|
276
275
|
| "windowBoundaries"
|
|
277
|
-
| "peerCount"
|
|
278
276
|
| "workerRecords"
|
|
279
277
|
| "byId"
|
|
280
278
|
| "runInputs"
|
|
@@ -292,7 +290,6 @@ const emptyIndexes = (): NativeIndexes => ({
|
|
|
292
290
|
applicationInputs: [],
|
|
293
291
|
durableOwners: [],
|
|
294
292
|
windowBoundaries: new Map(),
|
|
295
|
-
peerCount: 0,
|
|
296
293
|
workerRecords: new Map(),
|
|
297
294
|
byId: new Map(),
|
|
298
295
|
runInputs: new Map(),
|
|
@@ -516,7 +513,6 @@ const prepareIndexes = (
|
|
|
516
513
|
previous: NativeIndexes,
|
|
517
514
|
records: ReadonlyArray<CanonicalRecordEnvelope>,
|
|
518
515
|
) => {
|
|
519
|
-
let peerCount = previous.peerCount;
|
|
520
516
|
// Latest execution entries are replaced in this private map; owner histories only append.
|
|
521
517
|
// Rebuild staging is private: reuse it only in the synchronous cursor publication step.
|
|
522
518
|
const workerRecords = new Map<string, Array<CanonicalRecordEnvelope>>();
|
|
@@ -624,7 +620,6 @@ const prepareIndexes = (
|
|
|
624
620
|
);
|
|
625
621
|
}
|
|
626
622
|
|
|
627
|
-
if (payload._tag === "PeerMessagePrepared") peerCount++;
|
|
628
623
|
if (
|
|
629
624
|
isWorker &&
|
|
630
625
|
(payload._tag === "UserInputRecorded" || payload._tag === "SubmissionSettled") &&
|
|
@@ -691,7 +686,7 @@ const prepareIndexes = (
|
|
|
691
686
|
}
|
|
692
687
|
|
|
693
688
|
return {
|
|
694
|
-
indexes:
|
|
689
|
+
indexes: previous,
|
|
695
690
|
commit: () => {
|
|
696
691
|
commitIndex(previous.runRecords, runRecords);
|
|
697
692
|
commitIndex(previous.operationRecords, operationAppends);
|
|
@@ -1154,7 +1149,6 @@ const makeThreadStore = Effect.gen(function* () {
|
|
|
1154
1149
|
tailSequence: ZERO_CANONICAL_SEQUENCE,
|
|
1155
1150
|
byId: new Map(),
|
|
1156
1151
|
workerRecords: new Map(),
|
|
1157
|
-
peerCount: 0,
|
|
1158
1152
|
runInputs: new Map(),
|
|
1159
1153
|
runRecords: new Map(),
|
|
1160
1154
|
operationRecords: new Map(),
|
|
@@ -1527,7 +1521,7 @@ const makeThreadStore = Effect.gen(function* () {
|
|
|
1527
1521
|
|
|
1528
1522
|
return Math.min(
|
|
1529
1523
|
index.foreign.peer +
|
|
1530
|
-
(deliveryTransfer?.
|
|
1524
|
+
(deliveryTransfer?.peerCount(request.threadId, request.nowMillis, request.limit) ?? 0),
|
|
1531
1525
|
request.limit,
|
|
1532
1526
|
);
|
|
1533
1527
|
});
|
|
@@ -95,7 +95,7 @@ export interface MemoryDeliveryTransfer {
|
|
|
95
95
|
readonly startImport: (
|
|
96
96
|
threadId: ThreadId,
|
|
97
97
|
) => Effect.Effect<MemoryDeliveryImport, ThreadImportRejected | ThreadStoreError>;
|
|
98
|
-
readonly
|
|
98
|
+
readonly peerCount: (threadId: ThreadId, nowMillis: number, limit: number) => number;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/** Native control metadata only; callers already hold the journal's shared gate. */
|