@effect/cluster 0.37.2 → 0.38.1

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 (141) hide show
  1. package/ClusterCron/package.json +6 -0
  2. package/dist/cjs/ClusterCron.js +86 -0
  3. package/dist/cjs/ClusterCron.js.map +1 -0
  4. package/dist/cjs/ClusterSchema.js +9 -1
  5. package/dist/cjs/ClusterSchema.js.map +1 -1
  6. package/dist/cjs/ClusterWorkflowEngine.js +21 -6
  7. package/dist/cjs/ClusterWorkflowEngine.js.map +1 -1
  8. package/dist/cjs/Entity.js +6 -1
  9. package/dist/cjs/Entity.js.map +1 -1
  10. package/dist/cjs/EntityAddress.js +8 -1
  11. package/dist/cjs/EntityAddress.js.map +1 -1
  12. package/dist/cjs/MessageStorage.js +6 -4
  13. package/dist/cjs/MessageStorage.js.map +1 -1
  14. package/dist/cjs/Runner.js +15 -0
  15. package/dist/cjs/Runner.js.map +1 -1
  16. package/dist/cjs/RunnerAddress.js +8 -1
  17. package/dist/cjs/RunnerAddress.js.map +1 -1
  18. package/dist/cjs/Runners.js +5 -0
  19. package/dist/cjs/Runners.js.map +1 -1
  20. package/dist/cjs/ShardId.js +75 -7
  21. package/dist/cjs/ShardId.js.map +1 -1
  22. package/dist/cjs/ShardManager.js +63 -43
  23. package/dist/cjs/ShardManager.js.map +1 -1
  24. package/dist/cjs/ShardStorage.js +48 -35
  25. package/dist/cjs/ShardStorage.js.map +1 -1
  26. package/dist/cjs/Sharding.js +45 -37
  27. package/dist/cjs/Sharding.js.map +1 -1
  28. package/dist/cjs/ShardingConfig.js +9 -2
  29. package/dist/cjs/ShardingConfig.js.map +1 -1
  30. package/dist/cjs/Singleton.js +2 -2
  31. package/dist/cjs/Singleton.js.map +1 -1
  32. package/dist/cjs/SingletonAddress.js +2 -2
  33. package/dist/cjs/SingletonAddress.js.map +1 -1
  34. package/dist/cjs/SqlMessageStorage.js +32 -27
  35. package/dist/cjs/SqlMessageStorage.js.map +1 -1
  36. package/dist/cjs/SqlShardStorage.js +14 -14
  37. package/dist/cjs/SqlShardStorage.js.map +1 -1
  38. package/dist/cjs/index.js +3 -1
  39. package/dist/cjs/internal/entityManager.js +2 -1
  40. package/dist/cjs/internal/entityManager.js.map +1 -1
  41. package/dist/cjs/internal/shardManager.js +138 -37
  42. package/dist/cjs/internal/shardManager.js.map +1 -1
  43. package/dist/dts/ClusterCron.d.ts +37 -0
  44. package/dist/dts/ClusterCron.d.ts.map +1 -0
  45. package/dist/dts/ClusterSchema.d.ts +8 -0
  46. package/dist/dts/ClusterSchema.d.ts.map +1 -1
  47. package/dist/dts/ClusterWorkflowEngine.d.ts.map +1 -1
  48. package/dist/dts/Entity.d.ts +10 -0
  49. package/dist/dts/Entity.d.ts.map +1 -1
  50. package/dist/dts/EntityAddress.d.ts +9 -3
  51. package/dist/dts/EntityAddress.d.ts.map +1 -1
  52. package/dist/dts/MessageStorage.d.ts +3 -3
  53. package/dist/dts/MessageStorage.d.ts.map +1 -1
  54. package/dist/dts/Runner.d.ts +15 -0
  55. package/dist/dts/Runner.d.ts.map +1 -1
  56. package/dist/dts/RunnerAddress.d.ts +5 -0
  57. package/dist/dts/RunnerAddress.d.ts.map +1 -1
  58. package/dist/dts/Runners.d.ts.map +1 -1
  59. package/dist/dts/ShardId.d.ts +60 -6
  60. package/dist/dts/ShardId.d.ts.map +1 -1
  61. package/dist/dts/ShardManager.d.ts +13 -13
  62. package/dist/dts/ShardManager.d.ts.map +1 -1
  63. package/dist/dts/ShardStorage.d.ts +11 -14
  64. package/dist/dts/ShardStorage.d.ts.map +1 -1
  65. package/dist/dts/Sharding.d.ts +4 -2
  66. package/dist/dts/Sharding.d.ts.map +1 -1
  67. package/dist/dts/ShardingConfig.d.ts +32 -6
  68. package/dist/dts/ShardingConfig.d.ts.map +1 -1
  69. package/dist/dts/Singleton.d.ts +3 -1
  70. package/dist/dts/Singleton.d.ts.map +1 -1
  71. package/dist/dts/SingletonAddress.d.ts +4 -3
  72. package/dist/dts/SingletonAddress.d.ts.map +1 -1
  73. package/dist/dts/SqlMessageStorage.d.ts +3 -2
  74. package/dist/dts/SqlMessageStorage.d.ts.map +1 -1
  75. package/dist/dts/SqlShardStorage.d.ts +1 -1
  76. package/dist/dts/index.d.ts +4 -0
  77. package/dist/dts/index.d.ts.map +1 -1
  78. package/dist/esm/ClusterCron.js +77 -0
  79. package/dist/esm/ClusterCron.js.map +1 -0
  80. package/dist/esm/ClusterSchema.js +7 -0
  81. package/dist/esm/ClusterSchema.js.map +1 -1
  82. package/dist/esm/ClusterWorkflowEngine.js +21 -6
  83. package/dist/esm/ClusterWorkflowEngine.js.map +1 -1
  84. package/dist/esm/Entity.js +6 -1
  85. package/dist/esm/Entity.js.map +1 -1
  86. package/dist/esm/EntityAddress.js +8 -1
  87. package/dist/esm/EntityAddress.js.map +1 -1
  88. package/dist/esm/MessageStorage.js +6 -4
  89. package/dist/esm/MessageStorage.js.map +1 -1
  90. package/dist/esm/Runner.js +15 -0
  91. package/dist/esm/Runner.js.map +1 -1
  92. package/dist/esm/RunnerAddress.js +8 -1
  93. package/dist/esm/RunnerAddress.js.map +1 -1
  94. package/dist/esm/Runners.js +5 -0
  95. package/dist/esm/Runners.js.map +1 -1
  96. package/dist/esm/ShardId.js +73 -6
  97. package/dist/esm/ShardId.js.map +1 -1
  98. package/dist/esm/ShardManager.js +64 -45
  99. package/dist/esm/ShardManager.js.map +1 -1
  100. package/dist/esm/ShardStorage.js +47 -35
  101. package/dist/esm/ShardStorage.js.map +1 -1
  102. package/dist/esm/Sharding.js +45 -37
  103. package/dist/esm/Sharding.js.map +1 -1
  104. package/dist/esm/ShardingConfig.js +9 -2
  105. package/dist/esm/ShardingConfig.js.map +1 -1
  106. package/dist/esm/Singleton.js +2 -2
  107. package/dist/esm/Singleton.js.map +1 -1
  108. package/dist/esm/SingletonAddress.js +2 -2
  109. package/dist/esm/SingletonAddress.js.map +1 -1
  110. package/dist/esm/SqlMessageStorage.js +32 -27
  111. package/dist/esm/SqlMessageStorage.js.map +1 -1
  112. package/dist/esm/SqlShardStorage.js +14 -14
  113. package/dist/esm/SqlShardStorage.js.map +1 -1
  114. package/dist/esm/index.js +4 -0
  115. package/dist/esm/index.js.map +1 -1
  116. package/dist/esm/internal/entityManager.js +2 -1
  117. package/dist/esm/internal/entityManager.js.map +1 -1
  118. package/dist/esm/internal/shardManager.js +136 -36
  119. package/dist/esm/internal/shardManager.js.map +1 -1
  120. package/package.json +12 -4
  121. package/src/ClusterCron.ts +129 -0
  122. package/src/ClusterSchema.ts +9 -0
  123. package/src/ClusterWorkflowEngine.ts +37 -6
  124. package/src/Entity.ts +20 -1
  125. package/src/EntityAddress.ts +11 -1
  126. package/src/MessageStorage.ts +12 -7
  127. package/src/Runner.ts +18 -0
  128. package/src/RunnerAddress.ts +9 -1
  129. package/src/Runners.ts +5 -0
  130. package/src/ShardId.ts +81 -11
  131. package/src/ShardManager.ts +74 -45
  132. package/src/ShardStorage.ts +57 -49
  133. package/src/Sharding.ts +45 -39
  134. package/src/ShardingConfig.ts +36 -7
  135. package/src/Singleton.ts +5 -2
  136. package/src/SingletonAddress.ts +2 -2
  137. package/src/SqlMessageStorage.ts +36 -30
  138. package/src/SqlShardStorage.ts +15 -15
  139. package/src/index.ts +5 -0
  140. package/src/internal/entityManager.ts +2 -1
  141. package/src/internal/shardManager.ts +158 -52
package/src/Sharding.ts CHANGED
@@ -21,6 +21,7 @@ import * as HashMap from "effect/HashMap"
21
21
  import * as Iterable from "effect/Iterable"
22
22
  import * as Layer from "effect/Layer"
23
23
  import * as MutableHashMap from "effect/MutableHashMap"
24
+ import * as MutableHashSet from "effect/MutableHashSet"
24
25
  import * as MutableRef from "effect/MutableRef"
25
26
  import * as Option from "effect/Option"
26
27
  import * as Predicate from "effect/Predicate"
@@ -74,7 +75,7 @@ export class Sharding extends Context.Tag("@effect/cluster/Sharding")<Sharding,
74
75
  * Returns the `ShardId` of the shard to which the entity at the specified
75
76
  * `address` is assigned.
76
77
  */
77
- readonly getShardId: (entityId: EntityId) => ShardId
78
+ readonly getShardId: (entityId: EntityId, group: string) => ShardId
78
79
 
79
80
  /**
80
81
  * Returns `true` if sharding is shutting down, `false` otherwise.
@@ -115,7 +116,10 @@ export class Sharding extends Context.Tag("@effect/cluster/Sharding")<Sharding,
115
116
  */
116
117
  readonly registerSingleton: <E, R>(
117
118
  name: string,
118
- run: Effect.Effect<void, E, R>
119
+ run: Effect.Effect<void, E, R>,
120
+ options?: {
121
+ readonly shardGroup?: string | undefined
122
+ }
119
123
  ) => Effect.Effect<void, never, Exclude<R, Scope.Scope>>
120
124
 
121
125
  /**
@@ -182,10 +186,10 @@ const make = Effect.gen(function*() {
182
186
  const entityManagers = new Map<EntityType, EntityManagerState>()
183
187
 
184
188
  const shardAssignments = MutableHashMap.empty<ShardId, RunnerAddress>()
185
- const selfShards = new Set<ShardId>()
189
+ const selfShards = MutableHashSet.empty<ShardId>()
186
190
 
187
191
  // the active shards are the ones that we have acquired the lock for
188
- const acquiredShards = new Set<ShardId>()
192
+ const acquiredShards = MutableHashSet.empty<ShardId>()
189
193
  const activeShardsLatch = yield* Effect.makeLatch(false)
190
194
 
191
195
  const events = yield* PubSub.unbounded<ShardingRegistrationEvent>()
@@ -194,12 +198,13 @@ const make = Effect.gen(function*() {
194
198
  const isLocalRunner = (address: RunnerAddress) =>
195
199
  Option.isSome(config.runnerAddress) && Equal.equals(address, config.runnerAddress.value)
196
200
 
197
- function getShardId(entityId: EntityId): ShardId {
198
- return ShardId.make((Math.abs(hashString(entityId) % config.numberOfShards)) + 1)
201
+ function getShardId(entityId: EntityId, group: string): ShardId {
202
+ const id = Math.abs(hashString(entityId) % config.shardsPerGroup) + 1
203
+ return ShardId.make({ group, id }, { disableValidation: true })
199
204
  }
200
205
 
201
206
  function isEntityOnLocalShards(address: EntityAddress): boolean {
202
- return acquiredShards.has(address.shardId)
207
+ return MutableHashSet.has(acquiredShards, address.shardId)
203
208
  }
204
209
 
205
210
  // --- Shard acquisition ---
@@ -211,30 +216,30 @@ const make = Effect.gen(function*() {
211
216
  return Effect.ignore(shardStorage.releaseAll(selfAddress))
212
217
  })
213
218
 
214
- const releasingShards = new Set<ShardId>()
219
+ const releasingShards = MutableHashSet.empty<ShardId>()
215
220
  yield* Effect.gen(function*() {
216
221
  while (true) {
217
222
  yield* activeShardsLatch.await
218
223
 
219
224
  // if a shard is no longer assigned to this runner, we release it
220
225
  for (const shardId of acquiredShards) {
221
- if (selfShards.has(shardId)) continue
222
- acquiredShards.delete(shardId)
223
- releasingShards.add(shardId)
226
+ if (MutableHashSet.has(selfShards, shardId)) continue
227
+ MutableHashSet.remove(acquiredShards, shardId)
228
+ MutableHashSet.add(releasingShards, shardId)
224
229
  }
225
230
  // if a shard has been assigned to this runner, we acquire it
226
- const unacquiredShards = new Set<ShardId>()
231
+ const unacquiredShards = MutableHashSet.empty<ShardId>()
227
232
  for (const shardId of selfShards) {
228
- if (acquiredShards.has(shardId) || releasingShards.has(shardId)) continue
229
- unacquiredShards.add(shardId)
233
+ if (MutableHashSet.has(acquiredShards, shardId) || MutableHashSet.has(releasingShards, shardId)) continue
234
+ MutableHashSet.add(unacquiredShards, shardId)
230
235
  }
231
236
 
232
- if (releasingShards.size > 0) {
237
+ if (MutableHashSet.size(releasingShards) > 0) {
233
238
  yield* Effect.forkIn(syncSingletons, shardingScope)
234
239
  yield* releaseShards
235
240
  }
236
241
 
237
- if (unacquiredShards.size === 0) {
242
+ if (MutableHashSet.size(unacquiredShards) === 0) {
238
243
  yield* activeShardsLatch.close
239
244
  continue
240
245
  }
@@ -242,7 +247,7 @@ const make = Effect.gen(function*() {
242
247
  const acquired = yield* shardStorage.acquire(selfAddress, unacquiredShards)
243
248
  yield* Effect.ignore(storage.resetShards(acquired))
244
249
  for (const shardId of acquired) {
245
- acquiredShards.add(shardId)
250
+ MutableHashSet.add(acquiredShards, shardId)
246
251
  }
247
252
  if (acquired.length > 0) {
248
253
  yield* storageReadLatch.open
@@ -272,12 +277,12 @@ const make = Effect.gen(function*() {
272
277
  ).pipe(
273
278
  Effect.flatMap((acquired) => {
274
279
  for (const shardId of acquiredShards) {
275
- if (!acquired.includes(shardId)) {
276
- acquiredShards.delete(shardId)
277
- releasingShards.add(shardId)
280
+ if (!acquired.some((_) => _[Equal.symbol](shardId))) {
281
+ MutableHashSet.remove(acquiredShards, shardId)
282
+ MutableHashSet.add(releasingShards, shardId)
278
283
  }
279
284
  }
280
- return releasingShards.size > 0 ?
285
+ return MutableHashSet.size(releasingShards) > 0 ?
281
286
  Effect.andThen(
282
287
  Effect.forkIn(syncSingletons, shardingScope),
283
288
  releaseShards
@@ -315,7 +320,7 @@ const make = Effect.gen(function*() {
315
320
  runner: selfAddress
316
321
  }),
317
322
  Effect.andThen(() => {
318
- releasingShards.delete(shardId)
323
+ MutableHashSet.remove(releasingShards, shardId)
319
324
  })
320
325
  ),
321
326
  { concurrency: "unbounded", discard: true }
@@ -325,7 +330,7 @@ const make = Effect.gen(function*() {
325
330
  }
326
331
 
327
332
  const clearSelfShards = Effect.suspend(() => {
328
- selfShards.clear()
333
+ MutableHashSet.clear(selfShards)
329
334
  return activeShardsLatch.open
330
335
  })
331
336
 
@@ -336,9 +341,10 @@ const make = Effect.gen(function*() {
336
341
  const withSingletonLock = Effect.unsafeMakeSemaphore(1).withPermits(1)
337
342
 
338
343
  const registerSingleton: Sharding["Type"]["registerSingleton"] = Effect.fnUntraced(
339
- function*(name, run) {
344
+ function*(name, run, options) {
345
+ const shardGroup = options?.shardGroup ?? "default"
340
346
  const address = new SingletonAddress({
341
- shardId: getShardId(EntityId.make(name)),
347
+ shardId: getShardId(EntityId.make(name), shardGroup),
342
348
  name
343
349
  })
344
350
 
@@ -365,7 +371,7 @@ const make = Effect.gen(function*() {
365
371
  yield* PubSub.publish(events, SingletonRegistered({ address }))
366
372
 
367
373
  // start if we are on the right shard
368
- if (acquiredShards.has(address.shardId)) {
374
+ if (MutableHashSet.has(acquiredShards, address.shardId)) {
369
375
  yield* Effect.logDebug("Starting singleton", address)
370
376
  yield* FiberMap.run(singletonFibers, address, wrappedRun)
371
377
  }
@@ -377,7 +383,7 @@ const make = Effect.gen(function*() {
377
383
  for (const [shardId, map] of singletons) {
378
384
  for (const [address, run] of map) {
379
385
  const running = FiberMap.unsafeHas(singletonFibers, address)
380
- const shouldBeRunning = acquiredShards.has(shardId)
386
+ const shouldBeRunning = MutableHashSet.has(acquiredShards, shardId)
381
387
  if (running && !shouldBeRunning) {
382
388
  yield* Effect.logDebug("Stopping singleton", address)
383
389
  internalInterruptors.add(yield* Effect.fiberId)
@@ -452,7 +458,7 @@ const make = Effect.gen(function*() {
452
458
  currentSentRequestIds.add(message.envelope.requestId)
453
459
  }
454
460
  const address = message.envelope.address
455
- if (!acquiredShards.has(address.shardId)) {
461
+ if (!MutableHashSet.has(acquiredShards, address.shardId)) {
456
462
  return Effect.void
457
463
  }
458
464
  const state = entityManagers.get(address.entityType)
@@ -580,7 +586,7 @@ const make = Effect.gen(function*() {
580
586
 
581
587
  while (!done) {
582
588
  // if the shard is no longer assigned to this runner, we stop
583
- if (!acquiredShards.has(address.shardId)) {
589
+ if (!MutableHashSet.has(acquiredShards, address.shardId)) {
584
590
  return
585
591
  }
586
592
 
@@ -606,7 +612,7 @@ const make = Effect.gen(function*() {
606
612
  EntityNotManagedByRunner | EntityNotAssignedToRunner
607
613
  > = Effect.catchTags(
608
614
  Effect.suspend(() => {
609
- if (!acquiredShards.has(address.shardId)) {
615
+ if (!MutableHashSet.has(acquiredShards, address.shardId)) {
610
616
  return Effect.fail(new EntityNotAssignedToRunner({ address }))
611
617
  }
612
618
 
@@ -805,7 +811,7 @@ const make = Effect.gen(function*() {
805
811
  yield* Effect.gen(function*() {
806
812
  yield* Effect.logDebug("Registering with shard manager")
807
813
  if (Option.isSome(config.runnerAddress)) {
808
- const machineId = yield* shardManager.register(config.runnerAddress.value)
814
+ const machineId = yield* shardManager.register(config.runnerAddress.value, config.shardGroups)
809
815
  yield* snowflakeGen.setMachineId(machineId)
810
816
  }
811
817
 
@@ -832,8 +838,8 @@ const make = Effect.gen(function*() {
832
838
  }
833
839
  if (!MutableRef.get(isShutdown) && isLocalRunner(event.address)) {
834
840
  for (const shardId of event.shards) {
835
- if (selfShards.has(shardId)) continue
836
- selfShards.add(shardId)
841
+ if (MutableHashSet.has(selfShards, shardId)) continue
842
+ MutableHashSet.add(selfShards, shardId)
837
843
  }
838
844
  yield* activeShardsLatch.open
839
845
  }
@@ -845,7 +851,7 @@ const make = Effect.gen(function*() {
845
851
  }
846
852
  if (isLocalRunner(event.address)) {
847
853
  for (const shard of event.shards) {
848
- selfShards.delete(shard)
854
+ MutableHashSet.remove(selfShards, shard)
849
855
  }
850
856
  yield* activeShardsLatch.open
851
857
  }
@@ -896,20 +902,20 @@ const make = Effect.gen(function*() {
896
902
  for (const [shardId, runner] of assignments) {
897
903
  if (Option.isNone(runner)) {
898
904
  MutableHashMap.remove(shardAssignments, shardId)
899
- selfShards.delete(shardId)
905
+ MutableHashSet.remove(selfShards, shardId)
900
906
  continue
901
907
  }
902
908
 
903
909
  MutableHashMap.set(shardAssignments, shardId, runner.value)
904
910
 
905
911
  if (!isLocalRunner(runner.value)) {
906
- selfShards.delete(shardId)
912
+ MutableHashSet.remove(selfShards, shardId)
907
913
  continue
908
914
  }
909
- if (MutableRef.get(isShutdown) || selfShards.has(shardId)) {
915
+ if (MutableRef.get(isShutdown) || MutableHashSet.has(selfShards, shardId)) {
910
916
  continue
911
917
  }
912
- selfShards.add(shardId)
918
+ MutableHashSet.add(selfShards, shardId)
913
919
  }
914
920
 
915
921
  yield* activeShardsLatch.open
@@ -1054,7 +1060,7 @@ const make = Effect.gen(function*() {
1054
1060
  return {
1055
1061
  ...wrappedClient,
1056
1062
  [currentClientAddress]: ClientAddressTag.context(EntityAddress.make({
1057
- shardId: getShardId(id),
1063
+ shardId: getShardId(id, entity.getShardGroup(entityId as EntityId)),
1058
1064
  entityId: id,
1059
1065
  entityType: entity.type
1060
1066
  }))
@@ -20,22 +20,35 @@ import { RunnerAddress } from "./RunnerAddress.js"
20
20
  */
21
21
  export class ShardingConfig extends Context.Tag("@effect/cluster/ShardingConfig")<ShardingConfig, {
22
22
  /**
23
- * The address for the current runner.
23
+ * The address for the current runner that other runners can use to
24
+ * communicate with it.
24
25
  *
25
26
  * If `None`, the runner is not part of the cluster and will be in a client-only
26
27
  * mode.
27
28
  */
28
29
  readonly runnerAddress: Option.Option<RunnerAddress>
30
+ /**
31
+ * The listen address for the current runner.
32
+ *
33
+ * Defaults to the `runnerAddress`.
34
+ */
35
+ readonly runnerListenAddress: Option.Option<RunnerAddress>
29
36
  /**
30
37
  * The version of the current runner.
31
38
  */
32
39
  readonly serverVersion: number
33
40
  /**
34
- * The number of shards to allocate to a runner.
41
+ * The shard groups that are assigned to this runner.
42
+ *
43
+ * Defaults to `["default"]`.
44
+ */
45
+ readonly shardGroups: ReadonlyArray<string>
46
+ /**
47
+ * The number of shards to allocate per shard group.
35
48
  *
36
49
  * **Note**: this value should be consistent across all runners.
37
50
  */
38
- readonly numberOfShards: number
51
+ readonly shardsPerGroup: number
39
52
  /**
40
53
  * The address of the shard manager.
41
54
  */
@@ -89,10 +102,12 @@ const defaultRunnerAddress = RunnerAddress.make({ host: "localhost", port: 34431
89
102
  */
90
103
  export const defaults: ShardingConfig["Type"] = {
91
104
  runnerAddress: Option.some(defaultRunnerAddress),
105
+ runnerListenAddress: Option.none(),
92
106
  serverVersion: 1,
93
- numberOfShards: 300,
107
+ shardsPerGroup: 300,
94
108
  shardManagerAddress: RunnerAddress.make({ host: "localhost", port: 8080 }),
95
109
  shardManagerUnavailableTimeout: Duration.minutes(10),
110
+ shardGroups: ["default"],
96
111
  entityMailboxCapacity: 4096,
97
112
  entityMaxIdleTime: Duration.minutes(1),
98
113
  entityTerminationTimeout: Duration.seconds(15),
@@ -131,13 +146,27 @@ export const config: Config.Config<ShardingConfig["Type"]> = Config.all({
131
146
  Config.withDescription("The port used for inter-runner communication.")
132
147
  )
133
148
  }).pipe(Config.map((options) => RunnerAddress.make(options)), Config.option),
149
+ runnerListenAddress: Config.all({
150
+ host: Config.string("listenHost").pipe(
151
+ Config.withDefault(defaultRunnerAddress.host),
152
+ Config.withDescription("The host to listen on.")
153
+ ),
154
+ port: Config.integer("listenPort").pipe(
155
+ Config.withDefault(defaultRunnerAddress.port),
156
+ Config.withDescription("The port to listen on.")
157
+ )
158
+ }).pipe(Config.map((options) => RunnerAddress.make(options)), Config.option),
134
159
  serverVersion: Config.integer("serverVersion").pipe(
135
160
  Config.withDefault(defaults.serverVersion),
136
161
  Config.withDescription("The version of the current runner.")
137
162
  ),
138
- numberOfShards: Config.integer("numberOfShards").pipe(
139
- Config.withDefault(defaults.numberOfShards),
140
- Config.withDescription("The number of shards to allocate to a runner.")
163
+ shardGroups: Config.array(Config.string("shardGroups")).pipe(
164
+ Config.withDefault(["default"]),
165
+ Config.withDescription("The shard groups that are assigned to this runner.")
166
+ ),
167
+ shardsPerGroup: Config.integer("shardsPerGroup").pipe(
168
+ Config.withDefault(defaults.shardsPerGroup),
169
+ Config.withDescription("The number of shards to allocate per shard group.")
141
170
  ),
142
171
  shardManagerAddress: Config.all({
143
172
  host: Config.string("shardManagerHost").pipe(
package/src/Singleton.ts CHANGED
@@ -12,9 +12,12 @@ import { Sharding } from "./Sharding.js"
12
12
  */
13
13
  export const make = <E, R>(
14
14
  name: string,
15
- run: Effect.Effect<void, E, R>
15
+ run: Effect.Effect<void, E, R>,
16
+ options?: {
17
+ readonly shardGroup?: string | undefined
18
+ }
16
19
  ): Layer.Layer<never, never, Sharding | Exclude<R, Scope>> =>
17
20
  Layer.effectDiscard(Effect.gen(function*() {
18
21
  const sharding = yield* Sharding
19
- yield* sharding.registerSingleton(name, run)
22
+ yield* sharding.registerSingleton(name, run, options)
20
23
  }))
@@ -36,12 +36,12 @@ export class SingletonAddress extends Schema.Class<SingletonAddress>("@effect/cl
36
36
  * @since 1.0.0
37
37
  */
38
38
  [Hash.symbol]() {
39
- return Hash.cached(this)(Hash.string(`${this.shardId}:${this.name}`))
39
+ return Hash.cached(this)(Hash.string(`${this.name}:${this.shardId.toString()}`))
40
40
  }
41
41
  /**
42
42
  * @since 1.0.0
43
43
  */
44
44
  [Equal.symbol](that: SingletonAddress): boolean {
45
- return this.shardId === that.shardId && this.name === that.name
45
+ return this.name === that.name && this.shardId[Equal.symbol](that.shardId)
46
46
  }
47
47
  }
@@ -15,6 +15,7 @@ import type * as Envelope from "./Envelope.js"
15
15
  import * as MessageStorage from "./MessageStorage.js"
16
16
  import { SaveResultEncoded } from "./MessageStorage.js"
17
17
  import type * as Reply from "./Reply.js"
18
+ import { ShardId } from "./ShardId.js"
18
19
  import type { ShardingConfig } from "./ShardingConfig.js"
19
20
  import * as Snowflake from "./Snowflake.js"
20
21
 
@@ -48,15 +49,15 @@ export const make = Effect.fnUntraced(function*(options?: {
48
49
  id BIGINT PRIMARY KEY,
49
50
  rowid BIGINT IDENTITY(1,1),
50
51
  message_id VARCHAR(255),
51
- shard_id INT NOT NULL,
52
- entity_type VARCHAR(255) NOT NULL,
52
+ shard_id VARCHAR(50) NOT NULL,
53
+ entity_type VARCHAR(50) NOT NULL,
53
54
  entity_id VARCHAR(255) NOT NULL,
54
55
  kind INT NOT NULL,
55
- tag VARCHAR(255),
56
+ tag VARCHAR(50),
56
57
  payload TEXT,
57
58
  headers TEXT,
58
- trace_id VARCHAR(255),
59
- span_id VARCHAR(255),
59
+ trace_id VARCHAR(32),
60
+ span_id VARCHAR(16),
60
61
  sampled BIT,
61
62
  processed BIT NOT NULL DEFAULT 0,
62
63
  request_id BIGINT NOT NULL,
@@ -74,15 +75,15 @@ export const make = Effect.fnUntraced(function*(options?: {
74
75
  id BIGINT NOT NULL,
75
76
  rowid BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
76
77
  message_id VARCHAR(255),
77
- shard_id INT NOT NULL,
78
- entity_type VARCHAR(255) NOT NULL,
78
+ shard_id VARCHAR(50) NOT NULL,
79
+ entity_type VARCHAR(50) NOT NULL,
79
80
  entity_id VARCHAR(255) NOT NULL,
80
81
  kind INT NOT NULL,
81
- tag VARCHAR(255),
82
+ tag VARCHAR(50),
82
83
  payload TEXT,
83
84
  headers TEXT,
84
- trace_id VARCHAR(255),
85
- span_id VARCHAR(255),
85
+ trace_id VARCHAR(32),
86
+ span_id VARCHAR(16),
86
87
  sampled BOOLEAN,
87
88
  processed BOOLEAN NOT NULL DEFAULT FALSE,
88
89
  request_id BIGINT NOT NULL,
@@ -101,15 +102,15 @@ export const make = Effect.fnUntraced(function*(options?: {
101
102
  id BIGINT PRIMARY KEY,
102
103
  rowid BIGSERIAL,
103
104
  message_id VARCHAR(255),
104
- shard_id INT NOT NULL,
105
- entity_type VARCHAR(255) NOT NULL,
105
+ shard_id VARCHAR(50) NOT NULL,
106
+ entity_type VARCHAR(50) NOT NULL,
106
107
  entity_id VARCHAR(255) NOT NULL,
107
108
  kind INT NOT NULL,
108
- tag VARCHAR(255),
109
+ tag VARCHAR(50),
109
110
  payload TEXT,
110
111
  headers TEXT,
111
- trace_id VARCHAR(255),
112
- span_id VARCHAR(255),
112
+ trace_id VARCHAR(32),
113
+ span_id VARCHAR(16),
113
114
  sampled BOOLEAN,
114
115
  processed BOOLEAN NOT NULL DEFAULT FALSE,
115
116
  request_id BIGINT NOT NULL,
@@ -127,7 +128,7 @@ export const make = Effect.fnUntraced(function*(options?: {
127
128
  CREATE TABLE IF NOT EXISTS ${messagesTableSql} (
128
129
  id INTEGER PRIMARY KEY,
129
130
  message_id TEXT,
130
- shard_id INTEGER NOT NULL,
131
+ shard_id TEXT NOT NULL,
131
132
  entity_type TEXT NOT NULL,
132
133
  entity_id TEXT NOT NULL,
133
134
  kind INTEGER NOT NULL,
@@ -315,7 +316,7 @@ export const make = Effect.fnUntraced(function*(options?: {
315
316
  return {
316
317
  id: envelope.requestId,
317
318
  message_id,
318
- shard_id: envelope.address.shardId,
319
+ shard_id: ShardId.toString(envelope.address.shardId),
319
320
  entity_type: envelope.address.entityType,
320
321
  entity_id: envelope.address.entityId,
321
322
  kind: messageKind.Request,
@@ -333,7 +334,7 @@ export const make = Effect.fnUntraced(function*(options?: {
333
334
  return {
334
335
  id: envelope.id,
335
336
  message_id,
336
- shard_id: envelope.address.shardId,
337
+ shard_id: ShardId.toString(envelope.address.shardId),
337
338
  entity_type: envelope.address.entityType,
338
339
  entity_id: envelope.address.entityId,
339
340
  kind: messageKind.AckChunk,
@@ -351,7 +352,7 @@ export const make = Effect.fnUntraced(function*(options?: {
351
352
  return {
352
353
  id: envelope.id,
353
354
  message_id,
354
- shard_id: envelope.address.shardId,
355
+ shard_id: ShardId.toString(envelope.address.shardId),
355
356
  entity_type: envelope.address.entityType,
356
357
  entity_id: envelope.address.entityId,
357
358
  kind: messageKind.Interrupt,
@@ -393,7 +394,7 @@ export const make = Effect.fnUntraced(function*(options?: {
393
394
  _tag: "Request",
394
395
  requestId: String(row.id),
395
396
  address: {
396
- shardId: Number(row.shard_id),
397
+ shardId: ShardId.fromStringEncoded(row.shard_id),
397
398
  entityType: row.entity_type,
398
399
  entityId: row.entity_id
399
400
  },
@@ -422,7 +423,7 @@ export const make = Effect.fnUntraced(function*(options?: {
422
423
  requestId: String(row.request_id!),
423
424
  replyId: String(row.reply_id!),
424
425
  address: {
425
- shardId: Number(row.shard_id),
426
+ shardId: ShardId.fromStringEncoded(row.shard_id),
426
427
  entityType: row.entity_type,
427
428
  entityId: row.entity_id
428
429
  }
@@ -436,7 +437,7 @@ export const make = Effect.fnUntraced(function*(options?: {
436
437
  id: String(row.id),
437
438
  requestId: String(row.request_id!),
438
439
  address: {
439
- shardId: Number(row.shard_id),
440
+ shardId: ShardId.fromStringEncoded(row.shard_id),
440
441
  entityType: row.entity_type,
441
442
  entityId: row.entity_id
442
443
  }
@@ -549,15 +550,20 @@ export const make = Effect.fnUntraced(function*(options?: {
549
550
  })
550
551
  const sqlNow = sql.literal(sqlNowString)
551
552
 
553
+ const wrapString = sql.onDialectOrElse({
554
+ mssql: () => (s: string) => `N'${s}'`,
555
+ orElse: () => (s: string) => `'${s}'`
556
+ })
557
+
552
558
  const getUnprocessedMessages = sql.onDialectOrElse({
553
- pg: () => (shardIds: ReadonlyArray<number>, now: number) =>
559
+ pg: () => (shardIds: ReadonlyArray<string>, now: number) =>
554
560
  sql<MessageJoinRow>`
555
561
  UPDATE ${messagesTableSql} m
556
562
  SET last_read = ${sqlNow}
557
563
  FROM (
558
564
  SELECT m.*
559
565
  FROM ${messagesTableSql} m
560
- WHERE m.shard_id IN (${sql.literal(shardIds.map(String).join(","))})
566
+ WHERE m.shard_id IN (${sql.literal(shardIds.map(wrapString).join(","))})
561
567
  AND NOT EXISTS (
562
568
  SELECT 1 FROM ${repliesTableSql}
563
569
  WHERE request_id = m.request_id
@@ -573,12 +579,12 @@ export const make = Effect.fnUntraced(function*(options?: {
573
579
  WHERE m.id = ids.id
574
580
  RETURNING ids.*, r.id as reply_reply_id, r.kind as reply_kind, r.payload as reply_payload, r.sequence as reply_sequence
575
581
  `,
576
- orElse: () => (shardIds: ReadonlyArray<number>, now: number) =>
582
+ orElse: () => (shardIds: ReadonlyArray<string>, now: number) =>
577
583
  sql<MessageJoinRow>`
578
584
  SELECT m.*, r.id as reply_reply_id, r.kind as reply_kind, r.payload as reply_payload, r.sequence as reply_sequence
579
585
  FROM ${messagesTableSql} m
580
586
  LEFT JOIN ${repliesTableSql} r ON r.id = m.last_reply_id
581
- WHERE m.shard_id IN (${sql.literal(shardIds.map(String).join(","))})
587
+ WHERE m.shard_id IN (${sql.literal(shardIds.map(wrapString).join(","))})
582
588
  AND NOT EXISTS (
583
589
  SELECT 1 FROM ${repliesTableSql}
584
590
  WHERE request_id = m.request_id
@@ -653,7 +659,7 @@ export const make = Effect.fnUntraced(function*(options?: {
653
659
  const row = replyToRow(reply)
654
660
  const update = reply._tag === "Chunk" ?
655
661
  sql`UPDATE ${messagesTableSql} SET last_reply_id = ${reply.id} WHERE id = ${reply.requestId}` :
656
- sql`UPDATE ${messagesTableSql} SET processed = ${sqlTrue}, last_reply_id = NULL WHERE request_id = ${reply.requestId}`
662
+ sql`UPDATE ${messagesTableSql} SET processed = ${sqlTrue}, last_reply_id = ${reply.id} WHERE request_id = ${reply.requestId}`
657
663
  return update.unprepared.pipe(
658
664
  Effect.andThen(sql`INSERT INTO ${repliesTableSql} ${sql.insert(row)}`),
659
665
  sql.withTransaction
@@ -772,7 +778,7 @@ export const make = Effect.fnUntraced(function*(options?: {
772
778
  UPDATE ${messagesTableSql}
773
779
  SET last_read = NULL
774
780
  WHERE processed = ${sqlFalse}
775
- AND shard_id = ${address.shardId}
781
+ AND shard_id = ${address.shardId.toString()}
776
782
  AND entity_type = ${address.entityType}
777
783
  AND entity_id = ${address.entityId}
778
784
  `.pipe(
@@ -808,7 +814,7 @@ export const make = Effect.fnUntraced(function*(options?: {
808
814
  UPDATE ${messagesTableSql}
809
815
  SET last_read = NULL
810
816
  WHERE processed = ${sqlFalse}
811
- AND shard_id IN (${sql.literal(shardIds.join(","))})
817
+ AND shard_id IN (${sql.literal(shardIds.map(wrapString).join(","))})
812
818
  `.pipe(
813
819
  Effect.asVoid,
814
820
  PersistenceError.refail,
@@ -875,7 +881,7 @@ const replyFromRow = (row: ReplyRow): Reply.ReplyEncoded<any> =>
875
881
  type MessageRow = {
876
882
  readonly id: string | bigint
877
883
  readonly message_id: string | null
878
- readonly shard_id: number | bigint
884
+ readonly shard_id: string
879
885
  readonly entity_type: string
880
886
  readonly entity_id: string
881
887
  readonly kind: 0 | 1 | 2 | 0n | 1n | 2n