@effect-app/infra 4.0.0-beta.263 → 4.0.0-beta.265

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.
@@ -1,4 +1,4 @@
1
- import type { OperationInput } from "@azure/cosmos"
1
+ import type { OperationInput, PatchRequestBody } from "@azure/cosmos"
2
2
  import * as Arr from "effect-app/Array"
3
3
  import * as Effect from "effect-app/Effect"
4
4
  import * as Layer from "effect-app/Layer"
@@ -84,6 +84,17 @@ interface RunnerDoc {
84
84
  runner: string
85
85
  healthy: boolean
86
86
  lastHeartbeat: number
87
+ /** Unique, stable machine id (mirrors SQL's auto-increment `machine_id`). */
88
+ readonly machineId: number
89
+ readonly _etag?: string
90
+ }
91
+
92
+ interface MachineIdCounterDoc {
93
+ readonly id: string
94
+ readonly _partitionKey: "runner"
95
+ readonly type: "machine-id-counter"
96
+ value: number
97
+ readonly _etag?: string
87
98
  }
88
99
 
89
100
  interface LockDoc {
@@ -331,13 +342,7 @@ const replyFromDoc = (doc: ReplyDoc): Reply.Encoded =>
331
342
  const shardIdFromString = (shardId: string): Envelope.Encoded["address"]["shardId"] =>
332
343
  ShardId.fromStringEncoded(shardId)
333
344
 
334
- const makeMachineId = (address: string) => {
335
- let hash = 0
336
- for (let i = 0; i < address.length; i++) {
337
- hash = Math.imul(31, hash) + address.charCodeAt(i) | 0
338
- }
339
- return Math.abs(hash)
340
- }
345
+ const machineIdCounterDocId = cosmosId("machine-id-counter")
341
346
 
342
347
  const createContainer = (prefix: string) =>
343
348
  Effect.fnUntraced(function*() {
@@ -395,29 +400,26 @@ export const makeMessageStorage = Effect.fnUntraced(function*(options?: {
395
400
  )
396
401
 
397
402
  const markReplyAcked = (requestId: string, replyId: string) =>
398
- Effect.tryPromise(() => container.item(cosmosId(replyId), replyPartition(requestId)).read<ReplyDoc>()).pipe(
399
- Effect.flatMap((resp) => {
400
- const doc = resp.resource
401
- if (!doc) return Effect.void
402
- doc.acked = true
403
- return Effect
404
- .tryPromise(() =>
405
- container.item(cosmosId(replyId), replyPartition(requestId)).replace(doc, {
406
- accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
407
- })
408
- )
409
- .pipe(Effect.tap(annotateItem), Effect.asVoid)
410
- }),
411
- Effect.catchIf(isNotFound, () => Effect.void),
412
- Effect.catchIf(isPreconditionFailed, () => Effect.void)
413
- )
403
+ Effect
404
+ .tryPromise(() =>
405
+ container.item(cosmosId(replyId), replyPartition(requestId)).patch<ReplyDoc>([
406
+ { op: "set", path: "/acked", value: true }
407
+ ])
408
+ )
409
+ .pipe(
410
+ Effect.tap(annotateItem),
411
+ Effect.asVoid,
412
+ Effect.catchIf(isNotFound, () => Effect.void),
413
+ Effect.catchIf(isPreconditionFailed, () => Effect.void)
414
+ )
414
415
 
415
- const replaceMessage = (doc: MessageDoc) =>
416
+ const claimMessageRead = (doc: MessageDoc, now: number) =>
416
417
  Effect
417
418
  .tryPromise(() =>
418
- container.item(doc.id, doc._partitionKey).replace(doc, {
419
- accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
420
- })
419
+ container.item(doc.id, doc._partitionKey).patch<MessageDoc>(
420
+ [{ op: "set", path: "/lastRead", value: now }],
421
+ { accessCondition: { type: "IfMatch", condition: doc._etag ?? "" } }
422
+ )
421
423
  )
422
424
  .pipe(
423
425
  Effect.tap(annotateItem),
@@ -425,28 +427,27 @@ export const makeMessageStorage = Effect.fnUntraced(function*(options?: {
425
427
  Effect.catchIf(isPreconditionFailed, () => Effect.succeed(false))
426
428
  )
427
429
 
428
- const markAckMessagesProcessed = (docs: ReadonlyArray<MessageDoc>) =>
430
+ const batchDocs = <A extends { readonly id: string; readonly _partitionKey: string }>(
431
+ docs: ReadonlyArray<A>,
432
+ operation: (doc: A) => OperationInput,
433
+ fallback: (doc: A) => Effect.Effect<void, unknown, never>
434
+ ): Effect.Effect<void, unknown, never> =>
429
435
  Effect.forEach(
430
436
  Arr.groupByT(docs, (doc) => doc._partitionKey),
431
437
  ([partitionKey, partitionDocs]) =>
432
438
  Effect.forEach(
433
439
  Arr.chunksOf(partitionDocs, maxCosmosBatchOperations),
434
440
  (chunk) => {
435
- const operations: Array<OperationInput> = chunk.map((doc) => ({
436
- operationType: "Patch" as const,
437
- id: doc.id,
438
- resourceBody: [{ op: "set" as const, path: "/processed", value: true }]
439
- }))
441
+ const operations: Array<OperationInput> = chunk.map(operation)
440
442
  return Effect
441
443
  .tryPromise(() => container.items.batch(operations, partitionKey))
442
444
  .pipe(
443
445
  Effect.tap(annotateItem),
444
446
  Effect.flatMap((resp) => {
445
447
  const failed = resp.result?.find((result) => !isSuccessfulStatus(result.statusCode))
446
- return failed === undefined
447
- ? Effect.void
448
- : Effect.fail(new Error(`cluster cosmos ack batch failed: ${failed.statusCode}`))
449
- })
448
+ return failed === undefined ? Effect.void : Effect.fail(failed.statusCode)
449
+ }),
450
+ Effect.catchIf(() => true, () => Effect.forEach(chunk, fallback, { discard: true }))
450
451
  )
451
452
  },
452
453
  { discard: true }
@@ -454,27 +455,50 @@ export const makeMessageStorage = Effect.fnUntraced(function*(options?: {
454
455
  { discard: true }
455
456
  )
456
457
 
457
- const updateMessage = (
458
- doc: MessageDoc,
459
- update: (doc: MessageDoc) => boolean
460
- ): Effect.Effect<void, unknown> =>
461
- Effect.suspend(function loop(current = doc): Effect.Effect<void, unknown> {
462
- if (!update(current)) return Effect.void
463
- return replaceMessage(current).pipe(
464
- Effect.flatMap((replaced) =>
465
- replaced
466
- ? Effect.void
467
- : readMessage(current.id, current._partitionKey).pipe(
468
- Effect.flatMap((found) =>
469
- Option.match(found, {
470
- onNone: () => Effect.void,
471
- onSome: loop
472
- })
473
- )
474
- )
475
- )
476
- )
477
- })
458
+ const patchDoc = <A extends { readonly id: string; readonly _partitionKey: string }>(
459
+ doc: A,
460
+ resourceBody: PatchRequestBody
461
+ ): Effect.Effect<void, unknown, never> =>
462
+ Effect.tryPromise(() => container.item(doc.id, doc._partitionKey).patch(resourceBody)).pipe(
463
+ Effect.tap(annotateItem),
464
+ Effect.asVoid,
465
+ Effect.catchIf(isNotFound, () => Effect.void)
466
+ )
467
+
468
+ const patchDocs = <A extends { readonly id: string; readonly _partitionKey: string }>(
469
+ docs: ReadonlyArray<A>,
470
+ resourceBody: (doc: A) => PatchRequestBody
471
+ ): Effect.Effect<void, unknown, never> =>
472
+ batchDocs(
473
+ docs,
474
+ (doc) => ({
475
+ operationType: "Patch" as const,
476
+ id: doc.id,
477
+ resourceBody: resourceBody(doc)
478
+ }),
479
+ (doc) => patchDoc(doc, resourceBody(doc))
480
+ )
481
+
482
+ const deleteDoc = <A extends { readonly id: string; readonly _partitionKey: string }>(
483
+ doc: A
484
+ ): Effect.Effect<void, unknown, never> =>
485
+ Effect.tryPromise(() => container.item(doc.id, doc._partitionKey).delete()).pipe(
486
+ Effect.tap(annotateItem),
487
+ Effect.asVoid,
488
+ Effect.catchIf(isNotFound, () => Effect.void)
489
+ )
490
+
491
+ const deleteDocs = <A extends { readonly id: string; readonly _partitionKey: string }>(
492
+ docs: ReadonlyArray<A>
493
+ ): Effect.Effect<void, unknown, never> =>
494
+ batchDocs(
495
+ docs,
496
+ (doc) => ({
497
+ operationType: "Delete" as const,
498
+ id: doc.id
499
+ }),
500
+ deleteDoc
501
+ )
478
502
 
479
503
  return yield* MessageStorage.makeEncoded({
480
504
  saveEnvelope: ({ deliverAt, envelope, primaryKey }) =>
@@ -487,7 +511,7 @@ export const makeMessageStorage = Effect.fnUntraced(function*(options?: {
487
511
  "SELECT * FROM c WHERE c.type = 'message' AND c.kind = 'AckChunk' AND c.processed = false AND c.requestId = @requestId",
488
512
  [{ name: "@requestId", value: envelope.requestId }]
489
513
  )
490
- yield* markAckMessagesProcessed(pendingAcks)
514
+ yield* patchDocs(pendingAcks, () => [{ op: "set", path: "/processed", value: true }])
491
515
  }
492
516
  return yield* Effect.tryPromise(() => container.items.create(doc)).pipe(
493
517
  Effect.tap(annotateItem),
@@ -525,17 +549,16 @@ export const makeMessageStorage = Effect.fnUntraced(function*(options?: {
525
549
  "SELECT * FROM c WHERE c.type = 'message' AND c.requestId = @requestId",
526
550
  [{ name: "@requestId", value: reply.requestId }]
527
551
  )
528
- yield* Effect.forEach(messages, (message) => {
529
- return updateMessage(message, (doc) => {
530
- if (reply._tag === "WithExit") {
531
- doc.processed = true
532
- } else if (doc.id !== reply.requestId && doc.kind !== "Request") {
533
- return false
534
- }
535
- doc.lastReplyId = reply.id
536
- return true
537
- })
538
- }, { discard: true })
552
+ const updatedMessages = reply._tag === "WithExit"
553
+ ? messages
554
+ : messages.filter((message) => message.id === reply.requestId || message.kind === "Request")
555
+ yield* patchDocs(updatedMessages, () =>
556
+ reply._tag === "WithExit"
557
+ ? [
558
+ { op: "set", path: "/processed", value: true },
559
+ { op: "set", path: "/lastReplyId", value: reply.id }
560
+ ]
561
+ : [{ op: "set", path: "/lastReplyId", value: reply.id }])
539
562
  })
540
563
  .pipe(annotate("saveReply"), refailPersistence, withTracerDisabled),
541
564
 
@@ -549,29 +572,20 @@ export const makeMessageStorage = Effect.fnUntraced(function*(options?: {
549
572
  { name: "@requestId", value: id }
550
573
  ]
551
574
  )
552
- yield* Effect.forEach(replies, (reply) =>
553
- Effect
554
- .tryPromise(() => container.item(reply.id, reply._partitionKey).delete())
555
- .pipe(
556
- Effect.tap(annotateItem),
557
- Effect.catchIf(isNotFound, () => Effect.void)
558
- ), { discard: true })
575
+ yield* deleteDocs(replies)
559
576
  const messages = yield* queryMessages(
560
577
  "SELECT * FROM c WHERE c.type = 'message' AND c.requestId = @requestId",
561
578
  [{ name: "@requestId", value: id }]
562
579
  )
563
- yield* Effect.forEach(messages, (message) => {
564
- if (message.kind === "Interrupt") {
565
- return Effect.tryPromise(() => container.item(message.id, message._partitionKey).delete()).pipe(
566
- Effect.tap(annotateItem),
567
- Effect.catchIf(isNotFound, () => Effect.void)
568
- )
569
- }
570
- message.processed = false
571
- message.lastReplyId = null
572
- message.lastRead = null
573
- return replaceMessage(message).pipe(Effect.asVoid)
574
- }, { discard: true })
580
+ yield* deleteDocs(messages.filter((message) => message.kind === "Interrupt"))
581
+ yield* patchDocs(
582
+ messages.filter((message) => message.kind !== "Interrupt"),
583
+ () => [
584
+ { op: "set", path: "/processed", value: false },
585
+ { op: "set", path: "/lastReplyId", value: null },
586
+ { op: "set", path: "/lastRead", value: null }
587
+ ]
588
+ )
575
589
  })
576
590
  .pipe(annotate("clearReplies"), refailPersistence, withTracerDisabled),
577
591
 
@@ -620,7 +634,7 @@ export const makeMessageStorage = Effect.fnUntraced(function*(options?: {
620
634
  ]
621
635
  )
622
636
  .pipe(
623
- Effect.flatMap((docs) => collectUnprocessed(docs, now, lastReply, replaceMessage, queryReplies)),
637
+ Effect.flatMap((docs) => collectUnprocessed(docs, now, claimMessageRead, queryReplies)),
624
638
  annotate("unprocessedMessages"),
625
639
  refailPersistence,
626
640
  withTracerDisabled
@@ -635,7 +649,7 @@ export const makeMessageStorage = Effect.fnUntraced(function*(options?: {
635
649
  ]
636
650
  )
637
651
  .pipe(
638
- Effect.flatMap((docs) => collectUnprocessed(docs, now, lastReply, replaceMessage, queryReplies)),
652
+ Effect.flatMap((docs) => collectUnprocessedById(docs, queryReplies)),
639
653
  annotate("unprocessedMessagesById"),
640
654
  refailPersistence,
641
655
  withTracerDisabled
@@ -651,12 +665,7 @@ export const makeMessageStorage = Effect.fnUntraced(function*(options?: {
651
665
  ]
652
666
  )
653
667
  .pipe(
654
- Effect.flatMap((docs) =>
655
- Effect.forEach(docs, (doc) => {
656
- doc.lastRead = null
657
- return replaceMessage(doc).pipe(Effect.asVoid)
658
- }, { discard: true })
659
- ),
668
+ Effect.flatMap((docs) => patchDocs(docs, () => [{ op: "set", path: "/lastRead", value: null }])),
660
669
  annotate("resetAddress"),
661
670
  refailPersistence,
662
671
  withTracerDisabled
@@ -671,37 +680,18 @@ export const makeMessageStorage = Effect.fnUntraced(function*(options?: {
671
680
  ]
672
681
  )
673
682
  .pipe(
674
- Effect.flatMap((messages) =>
675
- Effect.forEach(messages, (message) =>
676
- queryReplies("SELECT * FROM c WHERE c.type = 'reply' AND c.requestId = @requestId", [
677
- {
678
- name: "@requestId",
679
- value: message
680
- .requestId
681
- }
682
- ])
683
- .pipe(
684
- Effect
685
- .flatMap((replies) =>
686
- Effect
687
- .forEach(replies, (reply) =>
688
- Effect
689
- .tryPromise(() =>
690
- container.item(reply.id, reply._partitionKey).delete()
691
- )
692
- .pipe(
693
- Effect.tap(annotateItem),
694
- Effect.catchIf(isNotFound, () => Effect.void)
695
- ), { discard: true })
696
- ),
697
- Effect.andThen(
698
- Effect.tryPromise(() => container.item(message.id, message._partitionKey).delete()).pipe(
699
- Effect.tap(annotateItem),
700
- Effect.catchIf(isNotFound, () => Effect.void)
701
- )
702
- )
703
- ), { discard: true })
704
- ),
683
+ Effect.flatMap((messages) => {
684
+ if (!Arr.isArrayNonEmpty(messages)) return Effect.void
685
+ const requestIds = Array.from(new Set(messages.map((message) => message.requestId)))
686
+ return queryReplies(
687
+ "SELECT * FROM c WHERE c.type = 'reply' AND ARRAY_CONTAINS(@requestIds, c.requestId)",
688
+ [{ name: "@requestIds", value: requestIds }]
689
+ )
690
+ .pipe(
691
+ Effect.flatMap((replies) => deleteDocs(replies)),
692
+ Effect.andThen(deleteDocs(messages))
693
+ )
694
+ }),
705
695
  annotate("clearAddress"),
706
696
  refailPersistence,
707
697
  withTracerDisabled
@@ -713,12 +703,7 @@ export const makeMessageStorage = Effect.fnUntraced(function*(options?: {
713
703
  [{ name: "@shardIds", value: Array.from(shardIds) }]
714
704
  )
715
705
  .pipe(
716
- Effect.flatMap((docs) =>
717
- Effect.forEach(docs, (doc) => {
718
- doc.lastRead = null
719
- return replaceMessage(doc).pipe(Effect.asVoid)
720
- }, { discard: true })
721
- ),
706
+ Effect.flatMap((docs) => patchDocs(docs, () => [{ op: "set", path: "/lastRead", value: null }])),
722
707
  annotate("resetShards"),
723
708
  refailPersistence,
724
709
  withTracerDisabled
@@ -731,8 +716,7 @@ export const makeMessageStorage = Effect.fnUntraced(function*(options?: {
731
716
  const collectUnprocessed = <E>(
732
717
  docs: ReadonlyArray<MessageDoc>,
733
718
  now: number,
734
- lastReply: (replyId: string | null) => Effect.Effect<Option.Option<Reply.Encoded>, E>,
735
- replaceMessage: (doc: MessageDoc) => Effect.Effect<boolean, E>,
719
+ claimMessageRead: (doc: MessageDoc, now: number) => Effect.Effect<boolean, E>,
736
720
  queryReplies: (
737
721
  query: string,
738
722
  parameters: ReadonlyArray<CosmosParameter>
@@ -743,22 +727,77 @@ const collectUnprocessed = <E>(
743
727
  readonly envelope: Envelope.Encoded
744
728
  readonly lastSentReply: Option.Option<Reply.Encoded>
745
729
  }> = []
730
+ const activeRequestIds = yield* activeReplyRequestIds(docs, queryReplies)
731
+ const lastReplies = yield* lastRepliesById(docs, activeRequestIds, queryReplies)
746
732
  for (const doc of docs) {
747
- const replies = yield* queryReplies(
748
- "SELECT * FROM c WHERE c.type = 'reply' AND c.requestId = @requestId AND (c.kind = 'WithExit' OR (c.kind = 'Chunk' AND c.acked = false))",
749
- [{ name: "@requestId", value: doc.requestId }]
750
- )
751
- if (Arr.isArrayNonEmpty(replies)) continue
752
- const sentReply = yield* lastReply(doc.lastReplyId)
753
- doc.lastRead = now
754
- const replaced = yield* replaceMessage(doc)
755
- if (replaced) {
756
- messages.push(envelopeFromDoc(doc, sentReply))
733
+ if (activeRequestIds.has(doc.requestId)) continue
734
+ const sentReply = Option.fromNullishOr(doc.lastReplyId === null ? undefined : lastReplies.get(doc.lastReplyId))
735
+ const claimed = yield* claimMessageRead(doc, now)
736
+ if (claimed) {
737
+ messages.push(envelopeFromDoc({ ...doc, lastRead: now }, sentReply))
757
738
  }
758
739
  }
759
740
  return messages
760
741
  })
761
742
 
743
+ const collectUnprocessedById = <E>(
744
+ docs: ReadonlyArray<MessageDoc>,
745
+ queryReplies: (
746
+ query: string,
747
+ parameters: ReadonlyArray<CosmosParameter>
748
+ ) => Effect.Effect<Array<ReplyDoc>, E>
749
+ ) =>
750
+ Effect.gen(function*() {
751
+ const messages: Array<{
752
+ readonly envelope: Envelope.Encoded
753
+ readonly lastSentReply: Option.Option<Reply.Encoded>
754
+ }> = []
755
+ const activeRequestIds = yield* activeReplyRequestIds(docs, queryReplies)
756
+ const lastReplies = yield* lastRepliesById(docs, activeRequestIds, queryReplies)
757
+ for (const doc of docs) {
758
+ if (activeRequestIds.has(doc.requestId)) continue
759
+ const sentReply = Option.fromNullishOr(doc.lastReplyId === null ? undefined : lastReplies.get(doc.lastReplyId))
760
+ messages.push(envelopeFromDoc(doc, sentReply))
761
+ }
762
+ return messages
763
+ })
764
+
765
+ const activeReplyRequestIds = <E>(
766
+ docs: ReadonlyArray<MessageDoc>,
767
+ queryReplies: (
768
+ query: string,
769
+ parameters: ReadonlyArray<CosmosParameter>
770
+ ) => Effect.Effect<Array<ReplyDoc>, E>
771
+ ) => {
772
+ const requestIds = Array.from(new Set(docs.map((doc) => doc.requestId)))
773
+ if (!Arr.isArrayNonEmpty(requestIds)) return Effect.succeed(new Set<string>())
774
+ return queryReplies(
775
+ "SELECT * FROM c WHERE c.type = 'reply' AND ARRAY_CONTAINS(@requestIds, c.requestId) AND (c.kind = 'WithExit' OR (c.kind = 'Chunk' AND c.acked = false))",
776
+ [{ name: "@requestIds", value: requestIds }]
777
+ )
778
+ .pipe(Effect.map((replies) => new Set(replies.map((reply) => reply.requestId))))
779
+ }
780
+
781
+ const lastRepliesById = <E>(
782
+ docs: ReadonlyArray<MessageDoc>,
783
+ activeRequestIds: ReadonlySet<string>,
784
+ queryReplies: (
785
+ query: string,
786
+ parameters: ReadonlyArray<CosmosParameter>
787
+ ) => Effect.Effect<Array<ReplyDoc>, E>
788
+ ) => {
789
+ const replyIds = Array.from(
790
+ new Set(docs
791
+ .flatMap((doc) => activeRequestIds.has(doc.requestId) || doc.lastReplyId === null ? [] : [doc.lastReplyId]))
792
+ )
793
+ if (!Arr.isArrayNonEmpty(replyIds)) return Effect.succeed(new Map<string, Reply.Encoded>())
794
+ return queryReplies(
795
+ "SELECT * FROM c WHERE c.type = 'reply' AND ARRAY_CONTAINS(@replyIds, c.rowid)",
796
+ [{ name: "@replyIds", value: replyIds }]
797
+ )
798
+ .pipe(Effect.map((replies) => new Map(replies.map((reply) => [reply.rowid, replyFromDoc(reply)]))))
799
+ }
800
+
762
801
  export const makeRunnerStorage = Effect.fnUntraced(function*(options?: {
763
802
  readonly prefix?: string | undefined
764
803
  }) {
@@ -780,6 +819,52 @@ export const makeRunnerStorage = Effect.fnUntraced(function*(options?: {
780
819
  )
781
820
  .pipe(Effect.tap(annotateFeed), Effect.map((resp) => resp.resources))
782
821
 
822
+ const deleteRunner = (doc: RunnerDoc) =>
823
+ Effect
824
+ .tryPromise(() =>
825
+ container.item(doc.id, "runner").delete({
826
+ accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
827
+ })
828
+ )
829
+ .pipe(
830
+ Effect.tap(annotateItem),
831
+ Effect.asVoid,
832
+ Effect.catchIf(isNotFound, () => Effect.void),
833
+ Effect.catchIf(isPreconditionFailed, () => Effect.void)
834
+ )
835
+
836
+ const readRunner = (address: string) =>
837
+ Effect.tryPromise(() => container.item(runnerDocId(address), "runner").read<RunnerDoc>()).pipe(
838
+ Effect.tap(annotateItem),
839
+ Effect.map((resp) => Option.fromNullishOr(resp.resource)),
840
+ Effect.catchIf(isNotFound, () => Effect.succeed(Option.none()))
841
+ )
842
+
843
+ // Allocate a unique, monotonically increasing machine id via an atomic
844
+ // server-side `incr` on a single counter doc — mirrors SQL's auto-increment
845
+ // `machine_id` PK. A per-address string hash (the previous approach) could
846
+ // collide mod 1024 across distinct runners, yielding duplicate Snowflake ids.
847
+ // The counter doc is seeded at init below, so it always exists here.
848
+ const allocateMachineId = Effect
849
+ .tryPromise(() =>
850
+ container.item(machineIdCounterDocId, "runner").patch<MachineIdCounterDoc>([
851
+ { op: "incr", path: "/value", value: 1 }
852
+ ])
853
+ )
854
+ .pipe(Effect.tap(annotateItem), Effect.map((resp) => resp.resource!.value))
855
+
856
+ // Seed the counter once (create-if-missing); a lost create race is fine.
857
+ yield* Effect
858
+ .tryPromise(() =>
859
+ container.items.create<MachineIdCounterDoc>({
860
+ id: machineIdCounterDocId,
861
+ _partitionKey: "runner",
862
+ type: "machine-id-counter",
863
+ value: 0
864
+ })
865
+ )
866
+ .pipe(Effect.catchIf(isConflict, () => Effect.void), Effect.orDie)
867
+
783
868
  const readLock = (shardId: string) =>
784
869
  Effect.tryPromise(() => container.item(lockDocId(shardId), "lock").read<LockDoc>()).pipe(
785
870
  Effect.tap(annotateItem),
@@ -800,6 +885,20 @@ export const makeRunnerStorage = Effect.fnUntraced(function*(options?: {
800
885
  Effect.catchIf(isPreconditionFailed, () => Effect.succeed(false))
801
886
  )
802
887
 
888
+ const deleteLock = (doc: LockDoc) =>
889
+ Effect
890
+ .tryPromise(() =>
891
+ container.item(doc.id, "lock").delete({
892
+ accessCondition: { type: "IfMatch", condition: doc._etag ?? "" }
893
+ })
894
+ )
895
+ .pipe(
896
+ Effect.tap(annotateItem),
897
+ Effect.asVoid,
898
+ Effect.catchIf(isNotFound, () => Effect.void),
899
+ Effect.catchIf(isPreconditionFailed, () => Effect.void)
900
+ )
901
+
803
902
  const createLock = (address: string, shardId: string, now: number) =>
804
903
  Effect
805
904
  .tryPromise(() =>
@@ -850,9 +949,17 @@ export const makeRunnerStorage = Effect.fnUntraced(function*(options?: {
850
949
  ),
851
950
 
852
951
  register: (address, runner, healthy) =>
853
- Effect.sync(() => Date.now()).pipe(
854
- Effect.flatMap((now) =>
855
- Effect
952
+ Effect
953
+ .gen(function*() {
954
+ const now = Date.now()
955
+ // Reuse a previously-allocated machine id for this address (stable per
956
+ // address, like SQL's RETURNING on conflict); allocate a fresh unique
957
+ // one on first registration.
958
+ const existing = yield* readRunner(address)
959
+ const machineId = Option.isSome(existing) && existing.value.machineId !== undefined
960
+ ? existing.value.machineId
961
+ : yield* allocateMachineId
962
+ yield* Effect
856
963
  .tryPromise(() =>
857
964
  container.items.upsert<RunnerDoc>({
858
965
  id: runnerDocId(address),
@@ -861,40 +968,47 @@ export const makeRunnerStorage = Effect.fnUntraced(function*(options?: {
861
968
  address,
862
969
  runner,
863
970
  healthy,
864
- lastHeartbeat: now
971
+ lastHeartbeat: now,
972
+ machineId
865
973
  })
866
974
  )
867
975
  .pipe(Effect.tap(annotateItem))
868
- ),
869
- Effect.as(makeMachineId(address)),
870
- annotate("register"),
871
- refailPersistence,
872
- withTracerDisabled
873
- ),
976
+ return machineId
977
+ })
978
+ .pipe(annotate("register"), refailPersistence, withTracerDisabled),
874
979
 
875
980
  unregister: (address) =>
876
- Effect.tryPromise(() => container.item(runnerDocId(address), "runner").delete()).pipe(
877
- Effect.tap(annotateItem),
878
- Effect.catchIf(isNotFound, () => Effect.void),
981
+ Effect.sync(() => Date.now()).pipe(
982
+ Effect.flatMap((now) =>
983
+ queryRunners(
984
+ "SELECT * FROM c WHERE c.type = 'runner' AND (c.address = @address OR c.lastHeartbeat < @expiresAt)",
985
+ [
986
+ { name: "@address", value: address },
987
+ { name: "@expiresAt", value: now - expires }
988
+ ]
989
+ )
990
+ ),
991
+ Effect.flatMap((docs) => Effect.forEach(docs, deleteRunner, { discard: true })),
879
992
  annotate("unregister"),
880
993
  refailPersistence,
881
994
  withTracerDisabled
882
995
  ),
883
996
 
884
997
  setRunnerHealth: (address, healthy) =>
885
- Effect.tryPromise(() => container.item(runnerDocId(address), "runner").read<RunnerDoc>()).pipe(
886
- Effect.flatMap((resp) => {
887
- const doc = resp.resource
888
- if (!doc) return Effect.void
889
- doc.healthy = healthy
890
- return Effect.tryPromise(() => container.item(doc.id, "runner").replace(doc)).pipe(Effect.tap(annotateItem))
891
- }),
892
- Effect.asVoid,
893
- Effect.catchIf(isNotFound, () => Effect.void),
894
- annotate("setRunnerHealth"),
895
- refailPersistence,
896
- withTracerDisabled
897
- ),
998
+ Effect
999
+ .tryPromise(() =>
1000
+ container.item(runnerDocId(address), "runner").patch<RunnerDoc>([
1001
+ { op: "set", path: "/healthy", value: healthy }
1002
+ ])
1003
+ )
1004
+ .pipe(
1005
+ Effect.tap(annotateItem),
1006
+ Effect.asVoid,
1007
+ Effect.catchIf(isNotFound, () => Effect.void),
1008
+ annotate("setRunnerHealth"),
1009
+ refailPersistence,
1010
+ withTracerDisabled
1011
+ ),
898
1012
 
899
1013
  acquire: (address, shardIds) =>
900
1014
  Effect.sync(() => Date.now()).pipe(
@@ -909,17 +1023,17 @@ export const makeRunnerStorage = Effect.fnUntraced(function*(options?: {
909
1023
  Effect
910
1024
  .gen(function*() {
911
1025
  const now = Date.now()
912
- yield* Effect.tryPromise(() => container.item(runnerDocId(address), "runner").read<RunnerDoc>()).pipe(
913
- Effect.flatMap((resp) => {
914
- const doc = resp.resource
915
- if (!doc) return Effect.void
916
- doc.lastHeartbeat = now
917
- return Effect.tryPromise(() => container.item(doc.id, "runner").replace(doc)).pipe(
918
- Effect.tap(annotateItem)
919
- )
920
- }),
921
- Effect.catchIf(isNotFound, () => Effect.void)
922
- )
1026
+ yield* Effect
1027
+ .tryPromise(() =>
1028
+ container.item(runnerDocId(address), "runner").patch<RunnerDoc>([
1029
+ { op: "set", path: "/lastHeartbeat", value: now }
1030
+ ])
1031
+ )
1032
+ .pipe(
1033
+ Effect.tap(annotateItem),
1034
+ Effect.asVoid,
1035
+ Effect.catchIf(isNotFound, () => Effect.void)
1036
+ )
923
1037
  const refreshed = yield* Effect.forEach(shardIds, (shardId) =>
924
1038
  readLock(shardId).pipe(
925
1039
  Effect.flatMap((lock) =>
@@ -944,11 +1058,7 @@ export const makeRunnerStorage = Effect.fnUntraced(function*(options?: {
944
1058
  onNone: () => Effect.void,
945
1059
  onSome: (doc) =>
946
1060
  doc.address === address
947
- ? Effect.tryPromise(() => container.item(doc.id, "lock").delete()).pipe(
948
- Effect.tap(annotateItem),
949
- Effect.catchIf(isNotFound, () => Effect.void),
950
- Effect.asVoid
951
- )
1061
+ ? deleteLock(doc)
952
1062
  : Effect.void
953
1063
  })
954
1064
  ),
@@ -970,13 +1080,7 @@ export const makeRunnerStorage = Effect.fnUntraced(function*(options?: {
970
1080
  )
971
1081
  .pipe(
972
1082
  Effect.tap(annotateFeed),
973
- Effect.flatMap((resp) =>
974
- Effect.forEach(resp.resources, (doc) =>
975
- Effect.tryPromise(() => container.item(doc.id, "lock").delete()).pipe(
976
- Effect.tap(annotateItem),
977
- Effect.catchIf(isNotFound, () => Effect.void)
978
- ), { discard: true })
979
- ),
1083
+ Effect.flatMap((resp) => Effect.forEach(resp.resources, deleteLock, { discard: true })),
980
1084
  annotate("releaseAll"),
981
1085
  refailPersistence,
982
1086
  withTracerDisabled