@effect/cluster 0.52.2 → 0.52.4

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/src/Sharding.ts CHANGED
@@ -11,7 +11,9 @@ import type { DurationInput } from "effect/Duration"
11
11
  import * as Effect from "effect/Effect"
12
12
  import * as Either from "effect/Either"
13
13
  import * as Equal from "effect/Equal"
14
+ import type * as Exit from "effect/Exit"
14
15
  import * as Fiber from "effect/Fiber"
16
+ import * as FiberHandle from "effect/FiberHandle"
15
17
  import * as FiberMap from "effect/FiberMap"
16
18
  import * as FiberRef from "effect/FiberRef"
17
19
  import * as FiberSet from "effect/FiberSet"
@@ -280,8 +282,19 @@ const make = Effect.gen(function*() {
280
282
  continue
281
283
  }
282
284
 
283
- const acquired = yield* runnerStorage.acquire(selfAddress, unacquiredShards)
284
- yield* Effect.ignore(storage.resetShards(acquired))
285
+ const oacquired = yield* runnerStorage.acquire(selfAddress, unacquiredShards).pipe(
286
+ Effect.timeoutOption(config.shardLockRefreshInterval)
287
+ )
288
+ if (Option.isNone(oacquired)) {
289
+ activeShardsLatch.unsafeOpen()
290
+ continue
291
+ }
292
+
293
+ const acquired = oacquired.value
294
+ yield* storage.resetShards(acquired).pipe(
295
+ Effect.ignore,
296
+ Effect.timeoutOption(config.shardLockRefreshInterval)
297
+ )
285
298
  for (const shardId of acquired) {
286
299
  if (MutableHashSet.has(releasingShards, shardId) || !MutableHashSet.has(selfShards, shardId)) {
287
300
  continue
@@ -349,27 +362,28 @@ const make = Effect.gen(function*() {
349
362
  Effect.forkIn(shardingScope)
350
363
  )
351
364
 
352
- const releaseShardsLock = Effect.unsafeMakeSemaphore(1).withPermits(1)
353
- const releaseShards = releaseShardsLock(
354
- Effect.suspend(() =>
355
- Effect.forEach(
356
- releasingShards,
357
- (shardId) =>
358
- Effect.forEach(
359
- entityManagers.values(),
360
- (state) => state.manager.interruptShard(shardId),
361
- { concurrency: "unbounded", discard: true }
362
- ).pipe(
363
- Effect.andThen(runnerStorage.release(selfAddress, shardId)),
364
- Effect.annotateLogs({ runner: selfAddress }),
365
- Effect.flatMap(() => {
366
- MutableHashSet.remove(releasingShards, shardId)
367
- return storage.unregisterShardReplyHandlers(shardId)
368
- })
369
- ),
370
- { concurrency: "unbounded", discard: true }
371
- )
365
+ const releaseShardsHandle = yield* FiberHandle.make()
366
+ const releaseShards = Effect.suspend(() =>
367
+ Effect.forEach(
368
+ releasingShards,
369
+ (shardId) =>
370
+ Effect.forEach(
371
+ entityManagers.values(),
372
+ (state) => state.manager.interruptShard(shardId),
373
+ { concurrency: "unbounded", discard: true }
374
+ ).pipe(
375
+ Effect.andThen(runnerStorage.release(selfAddress, shardId)),
376
+ Effect.annotateLogs({ runner: selfAddress }),
377
+ Effect.flatMap(() => {
378
+ MutableHashSet.remove(releasingShards, shardId)
379
+ return storage.unregisterShardReplyHandlers(shardId)
380
+ })
381
+ ),
382
+ { concurrency: "unbounded", discard: true }
372
383
  )
384
+ ).pipe(
385
+ Effect.repeat({ until: () => MutableHashSet.size(releasingShards) === 0 }),
386
+ FiberHandle.run(releaseShardsHandle, { onlyIfMissing: true })
373
387
  )
374
388
 
375
389
  // open the shard latch every poll interval
@@ -1247,7 +1261,9 @@ const make = Effect.gen(function*() {
1247
1261
  Effect.fiberIdWith((id) => {
1248
1262
  state.status = "closing"
1249
1263
  internalInterruptors.add(id)
1250
- return Effect.void
1264
+ // if preemptive shutdown is enabled, we start shutting down Sharding
1265
+ // too
1266
+ return config.preemptiveShutdown ? shutdown() : Effect.void
1251
1267
  })
1252
1268
  )
1253
1269
 
@@ -1320,23 +1336,27 @@ const make = Effect.gen(function*() {
1320
1336
 
1321
1337
  // --- Finalization ---
1322
1338
 
1323
- yield* Scope.addFinalizerExit(
1324
- shardingScope,
1325
- Effect.fnUntraced(function*(exit) {
1326
- yield* Effect.logDebug("Shutting down", exit._tag === "Success" ? {} : exit.cause).pipe(
1339
+ const shutdown = Effect.fnUntraced(function*(exit?: Exit.Exit<unknown, unknown>) {
1340
+ if (exit) {
1341
+ yield* Effect.logDebug("Shutting down", exit._tag === "Failure" ? exit.cause : {}).pipe(
1327
1342
  Effect.annotateLogs({
1328
1343
  package: "@effect/cluster",
1329
1344
  module: "Sharding"
1330
1345
  })
1331
1346
  )
1332
- const fiberId = yield* Effect.fiberId
1333
- MutableRef.set(isShutdown, true)
1334
- internalInterruptors.add(fiberId)
1335
- if (selfRunner) {
1336
- yield* Effect.ignore(runnerStorage.unregister(selfRunner.address))
1337
- }
1338
- })
1339
- )
1347
+ }
1348
+
1349
+ const fiberId = yield* Effect.fiberId
1350
+ internalInterruptors.add(fiberId)
1351
+ if (isShutdown.current) return
1352
+
1353
+ MutableRef.set(isShutdown, true)
1354
+ if (selfRunner) {
1355
+ yield* Effect.ignore(runnerStorage.unregister(selfRunner.address))
1356
+ }
1357
+ })
1358
+
1359
+ yield* Scope.addFinalizerExit(shardingScope, shutdown)
1340
1360
 
1341
1361
  const activeEntityCount = Effect.gen(function*() {
1342
1362
  let count = 0
@@ -63,6 +63,12 @@ export class ShardingConfig extends Context.Tag("@effect/cluster/ShardingConfig"
63
63
  * Shard lock expiration duration.
64
64
  */
65
65
  readonly shardLockExpiration: DurationInput
66
+ /**
67
+ * Start shutting down as soon as an Entity has started shutting down.
68
+ *
69
+ * Defaults to `true`.
70
+ */
71
+ readonly preemptiveShutdown: boolean
66
72
  /**
67
73
  * The default capacity of the mailbox for entities.
68
74
  */
@@ -118,6 +124,7 @@ export const defaults: ShardingConfig["Type"] = {
118
124
  runnerShardWeight: 1,
119
125
  shardsPerGroup: 300,
120
126
  shardGroups: ["default"],
127
+ preemptiveShutdown: true,
121
128
  shardLockRefreshInterval: Duration.seconds(10),
122
129
  shardLockExpiration: Duration.seconds(35),
123
130
  entityMailboxCapacity: 4096,
@@ -179,6 +186,10 @@ export const config: Config.Config<ShardingConfig["Type"]> = Config.all({
179
186
  Config.withDefault(defaults.shardsPerGroup),
180
187
  Config.withDescription("The number of shards to allocate per shard group.")
181
188
  ),
189
+ preemptiveShutdown: Config.boolean("preemptiveShutdown").pipe(
190
+ Config.withDefault(defaults.preemptiveShutdown),
191
+ Config.withDescription("Start shutting down as soon as an Entity has started shutting down.")
192
+ ),
182
193
  shardLockRefreshInterval: Config.duration("shardLockRefreshInterval").pipe(
183
194
  Config.withDefault(defaults.shardLockRefreshInterval),
184
195
  Config.withDescription("Shard lock refresh interval.")
@@ -242,10 +242,8 @@ export const make = Effect.fnUntraced(function*(options: {
242
242
  if (toAcquire.size === 0) {
243
243
  return acquiredShardIds
244
244
  }
245
- const results = (yield* conn.executeUnprepared(`SELECT ${pgLocks(toAcquire)}`, [], undefined))[0] as Record<
246
- string,
247
- boolean
248
- >
245
+ const rows = yield* conn.executeUnprepared(`SELECT ${pgLocks(toAcquire)}`, [], undefined)
246
+ const results = rows[0] as Record<string, boolean>
249
247
  for (const shardId in results) {
250
248
  if (results[shardId]) {
251
249
  acquiredShardIds.push(shardId)
@@ -449,7 +447,9 @@ export const make = Effect.fnUntraced(function*(options: {
449
447
  function*(_address, shardId) {
450
448
  const lockNum = lockNumbers.get(shardId)!
451
449
  const conn = yield* Resource.get(lockConnRef!)
452
- const release = conn.executeRaw(`SELECT pg_advisory_unlock(${lockNum})`, [])
450
+ const release = conn.executeRaw(`SELECT pg_advisory_unlock(${lockNum})`, []).pipe(
451
+ Effect.timeoutOption(Duration.seconds(5))
452
+ )
453
453
  const check = conn.executeValues(
454
454
  `SELECT 1 FROM pg_locks WHERE locktype = 'advisory' AND granted = true AND pid = pg_backend_pid() AND objid = ${lockNum}`,
455
455
  []
@@ -460,6 +460,7 @@ export const make = Effect.fnUntraced(function*(options: {
460
460
  if (takenLocks.length === 0) return
461
461
  }
462
462
  },
463
+ Effect.timeout(config.shardLockExpiration),
463
464
  Effect.onError(() => Resource.refresh(lockConnRef!)),
464
465
  Effect.asVoid,
465
466
  PersistenceError.refail,
@@ -481,6 +482,7 @@ export const make = Effect.fnUntraced(function*(options: {
481
482
  if (takenLocks.length === 0 || takenLocks[0][0] !== 1) return
482
483
  }
483
484
  },
485
+ Effect.timeout(config.shardLockExpiration),
484
486
  Effect.onError(() => Resource.refresh(lockConnRef!)),
485
487
  Effect.asVoid,
486
488
  PersistenceError.refail,