@effect/cluster 0.53.5 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect/cluster",
3
- "version": "0.53.5",
3
+ "version": "0.54.0",
4
4
  "description": "Unified interfaces for common cluster-specific services",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,11 +14,11 @@
14
14
  "kubernetes-types": "^1.30.0"
15
15
  },
16
16
  "peerDependencies": {
17
- "@effect/workflow": "^0.13.0",
17
+ "@effect/platform": "^0.93.5",
18
+ "@effect/sql": "^0.48.5",
18
19
  "@effect/rpc": "^0.72.2",
19
- "@effect/sql": "^0.48.0",
20
- "effect": "^3.19.6",
21
- "@effect/platform": "^0.93.3"
20
+ "@effect/workflow": "^0.14.0",
21
+ "effect": "^3.19.8"
22
22
  },
23
23
  "publishConfig": {
24
24
  "provenance": true
@@ -183,6 +183,11 @@
183
183
  "import": "./dist/esm/ShardingRegistrationEvent.js",
184
184
  "default": "./dist/cjs/ShardingRegistrationEvent.js"
185
185
  },
186
+ "./SingleRunner": {
187
+ "types": "./dist/dts/SingleRunner.d.ts",
188
+ "import": "./dist/esm/SingleRunner.js",
189
+ "default": "./dist/cjs/SingleRunner.js"
190
+ },
186
191
  "./Singleton": {
187
192
  "types": "./dist/dts/Singleton.d.ts",
188
193
  "import": "./dist/esm/Singleton.js",
@@ -213,6 +218,11 @@
213
218
  "import": "./dist/esm/SqlRunnerStorage.js",
214
219
  "default": "./dist/cjs/SqlRunnerStorage.js"
215
220
  },
221
+ "./TestRunner": {
222
+ "types": "./dist/dts/TestRunner.d.ts",
223
+ "import": "./dist/esm/TestRunner.js",
224
+ "default": "./dist/cjs/TestRunner.js"
225
+ },
216
226
  "./index": {
217
227
  "types": "./dist/dts/index.d.ts",
218
228
  "import": "./dist/esm/index.js",
@@ -311,6 +321,9 @@
311
321
  "ShardingRegistrationEvent": [
312
322
  "./dist/dts/ShardingRegistrationEvent.d.ts"
313
323
  ],
324
+ "SingleRunner": [
325
+ "./dist/dts/SingleRunner.d.ts"
326
+ ],
314
327
  "Singleton": [
315
328
  "./dist/dts/Singleton.d.ts"
316
329
  ],
@@ -329,6 +342,9 @@
329
342
  "SqlRunnerStorage": [
330
343
  "./dist/dts/SqlRunnerStorage.d.ts"
331
344
  ],
345
+ "TestRunner": [
346
+ "./dist/dts/TestRunner.d.ts"
347
+ ],
332
348
  "index": [
333
349
  "./dist/dts/index.d.ts"
334
350
  ]
@@ -63,6 +63,10 @@ export class ShardingConfig extends Context.Tag("@effect/cluster/ShardingConfig"
63
63
  * Shard lock expiration duration.
64
64
  */
65
65
  readonly shardLockExpiration: DurationInput
66
+ /**
67
+ * Disable the use of advisory locks for shard locking.
68
+ */
69
+ readonly shardLockDisableAdvisory: boolean
66
70
  /**
67
71
  * Start shutting down as soon as an Entity has started shutting down.
68
72
  *
@@ -134,6 +138,7 @@ export const defaults: ShardingConfig["Type"] = {
134
138
  preemptiveShutdown: true,
135
139
  shardLockRefreshInterval: Duration.seconds(10),
136
140
  shardLockExpiration: Duration.seconds(35),
141
+ shardLockDisableAdvisory: false,
137
142
  entityMailboxCapacity: 4096,
138
143
  entityMaxIdleTime: Duration.minutes(1),
139
144
  entityRegistrationTimeout: Duration.minutes(1),
@@ -206,6 +211,10 @@ export const config: Config.Config<ShardingConfig["Type"]> = Config.all({
206
211
  Config.withDefault(defaults.shardLockExpiration),
207
212
  Config.withDescription("Shard lock expiration duration.")
208
213
  ),
214
+ shardLockDisableAdvisory: Config.boolean("shardLockDisableAdvisory").pipe(
215
+ Config.withDefault(defaults.shardLockDisableAdvisory),
216
+ Config.withDescription("Disable the use of advisory locks for shard locking.")
217
+ ),
209
218
  entityMailboxCapacity: Config.integer("entityMailboxCapacity").pipe(
210
219
  Config.withDefault(defaults.entityMailboxCapacity),
211
220
  Config.withDescription("The default capacity of the mailbox for entities.")
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import type * as SqlClient from "@effect/sql/SqlClient"
5
+ import type * as ConfigError from "effect/ConfigError"
6
+ import * as Layer from "effect/Layer"
7
+ import type * as MessageStorage from "./MessageStorage.js"
8
+ import * as RunnerHealth from "./RunnerHealth.js"
9
+ import * as Runners from "./Runners.js"
10
+ import * as RunnerStorage from "./RunnerStorage.js"
11
+ import * as Sharding from "./Sharding.js"
12
+ import * as ShardingConfig from "./ShardingConfig.js"
13
+ import * as SqlMessageStorage from "./SqlMessageStorage.js"
14
+ import * as SqlRunnerStorage from "./SqlRunnerStorage.js"
15
+
16
+ /**
17
+ * A sql backed single-node cluster, that can be used for running durable
18
+ * entities and workflows.
19
+ *
20
+ * @since 1.0.0
21
+ * @category Layers
22
+ */
23
+ export const layer = (options?: {
24
+ readonly shardingConfig?: Partial<ShardingConfig.ShardingConfig["Type"]> | undefined
25
+ readonly runnerStorage?: "memory" | "sql" | undefined
26
+ }): Layer.Layer<
27
+ | Sharding.Sharding
28
+ | Runners.Runners
29
+ | MessageStorage.MessageStorage,
30
+ ConfigError.ConfigError,
31
+ SqlClient.SqlClient
32
+ > =>
33
+ Sharding.layer.pipe(
34
+ Layer.provideMerge(Runners.layerNoop),
35
+ Layer.provideMerge(SqlMessageStorage.layer),
36
+ Layer.provide([
37
+ options?.runnerStorage === "memory" ? RunnerStorage.layerMemory : Layer.orDie(SqlRunnerStorage.layer),
38
+ RunnerHealth.layerNoop
39
+ ]),
40
+ Layer.provide(ShardingConfig.layerFromEnv(options?.shardingConfig))
41
+ )
@@ -25,6 +25,7 @@ export const make = Effect.fnUntraced(function*(options: {
25
25
  readonly prefix?: string | undefined
26
26
  }) {
27
27
  const config = yield* ShardingConfig.ShardingConfig
28
+ const disableAdvisoryLocks = config.shardLockDisableAdvisory
28
29
  const sql = (yield* SqlClient.SqlClient).withoutTransforms()
29
30
  const prefix = options?.prefix ?? "cluster"
30
31
  const table = (name: string) => `${prefix}_${name}`
@@ -138,8 +139,22 @@ export const make = Effect.fnUntraced(function*(options: {
138
139
  acquired_at DATETIME NOT NULL
139
140
  )
140
141
  `,
141
- mysql: () => Effect.void,
142
- pg: () => Effect.void,
142
+ mysql: () =>
143
+ sql`
144
+ CREATE TABLE IF NOT EXISTS ${locksTableSql} (
145
+ shard_id VARCHAR(50) PRIMARY KEY,
146
+ address VARCHAR(255) NOT NULL,
147
+ acquired_at DATETIME NOT NULL
148
+ )
149
+ `,
150
+ pg: () =>
151
+ sql`
152
+ CREATE TABLE IF NOT EXISTS ${locksTableSql} (
153
+ shard_id VARCHAR(50) PRIMARY KEY,
154
+ address VARCHAR(255) NOT NULL,
155
+ acquired_at TIMESTAMP NOT NULL
156
+ )
157
+ `,
143
158
  orElse: () =>
144
159
  // sqlite
145
160
  sql`
@@ -232,6 +247,16 @@ export const make = Effect.fnUntraced(function*(options: {
232
247
  Effect.onError(() => lockConn.unsafeRebuild())
233
248
  )
234
249
  }
250
+ const execWithLockConnUnprepared = <A>(
251
+ effect: Statement.Statement<A>
252
+ ): Effect.Effect<ReadonlyArray<ReadonlyArray<any>>, SqlError> => {
253
+ if (!lockConn) return effect.values
254
+ const [query, params] = effect.compile()
255
+ return lockConn.await.pipe(
256
+ Effect.flatMap(([conn]) => conn.executeUnprepared(query, params, undefined)),
257
+ Effect.onError(() => lockConn.unsafeRebuild())
258
+ )
259
+ }
235
260
  const execWithLockConnValues = <A>(
236
261
  effect: Statement.Statement<A>
237
262
  ): Effect.Effect<ReadonlyArray<ReadonlyArray<any>>, SqlError> => {
@@ -244,8 +269,24 @@ export const make = Effect.fnUntraced(function*(options: {
244
269
  }
245
270
 
246
271
  const acquireLock = sql.onDialectOrElse({
247
- pg: () =>
248
- Effect.fnUntraced(function*(_address: string, shardIds: ReadonlyArray<string>) {
272
+ pg: () => {
273
+ if (disableAdvisoryLocks) {
274
+ return (address: string, shardIds: ReadonlyArray<string>) => {
275
+ const values = shardIds.map((shardId) =>
276
+ sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})`
277
+ )
278
+ return sql`
279
+ INSERT INTO ${locksTableSql} (shard_id, address, acquired_at) VALUES ${sql.csv(values)}
280
+ ON CONFLICT (shard_id) DO UPDATE
281
+ SET address = ${address}, acquired_at = ${sqlNow}
282
+ WHERE ${locksTableSql}.address = ${address}
283
+ OR ${locksTableSql}.acquired_at < ${lockExpiresAt}
284
+ `.pipe(
285
+ Effect.andThen(acquiredLocks(address, shardIds))
286
+ )
287
+ }
288
+ }
289
+ return Effect.fnUntraced(function*(_address: string, shardIds: ReadonlyArray<string>) {
249
290
  const [conn, pid] = yield* lockConn!.await
250
291
  const acquiredShardIds: Array<string> = []
251
292
  const toAcquire = new Map(shardIds.map((shardId) => [lockNumbers.get(shardId)!, shardId]))
@@ -269,10 +310,26 @@ export const make = Effect.fnUntraced(function*(options: {
269
310
  }
270
311
  }
271
312
  return acquiredShardIds
272
- }, Effect.onError(() => lockConn!.unsafeRebuild())),
313
+ }, Effect.onError(() => lockConn!.unsafeRebuild()))
314
+ },
273
315
 
274
- mysql: () =>
275
- Effect.fnUntraced(function*(_address: string, shardIds: ReadonlyArray<string>) {
316
+ mysql: () => {
317
+ if (disableAdvisoryLocks) {
318
+ return (address: string, shardIds: ReadonlyArray<string>) => {
319
+ const values = shardIds.map((shardId) =>
320
+ sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})`
321
+ )
322
+ return sql`
323
+ INSERT INTO ${locksTableSql} (shard_id, address, acquired_at) VALUES ${sql.csv(values)}
324
+ ON DUPLICATE KEY UPDATE
325
+ address = IF(address = VALUES(address) OR acquired_at < ${lockExpiresAt}, VALUES(address), address),
326
+ acquired_at = IF(address = VALUES(address) OR acquired_at < ${lockExpiresAt}, VALUES(acquired_at), acquired_at)
327
+ `.unprepared.pipe(
328
+ Effect.andThen(acquiredLocks(address, shardIds))
329
+ )
330
+ }
331
+ }
332
+ return Effect.fnUntraced(function*(_address: string, shardIds: ReadonlyArray<string>) {
276
333
  const [conn, pid] = yield* lockConn!.await
277
334
  const takenLocks = (yield* conn.executeValues(`SELECT ${allMySqlTakenLocks}`, []))[0] as Array<number | null>
278
335
  const acquiredShardIds: Array<string> = []
@@ -296,7 +353,8 @@ export const make = Effect.fnUntraced(function*(options: {
296
353
  }
297
354
  }
298
355
  return acquiredShardIds
299
- }, Effect.onError(() => lockConn!.unsafeRebuild())),
356
+ }, Effect.onError(() => lockConn!.unsafeRebuild()))
357
+ },
300
358
 
301
359
  mssql: () => (address: string, shardIds: ReadonlyArray<string>) => {
302
360
  const values = shardIds.map((shardId) => sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})`)
@@ -399,8 +457,34 @@ export const make = Effect.fnUntraced(function*(options: {
399
457
  const stringLiteralArr = (arr: ReadonlyArray<string>) => sql.literal(`(${arr.map(wrapString).join(",")})`)
400
458
 
401
459
  const refreshShards = sql.onDialectOrElse({
402
- pg: () => acquireLock,
403
- mysql: () => acquireLock,
460
+ pg: () => {
461
+ if (!disableAdvisoryLocks) return acquireLock
462
+ return (address: string, shardIds: ReadonlyArray<string>) =>
463
+ sql`
464
+ UPDATE ${locksTableSql}
465
+ SET acquired_at = ${sqlNow}
466
+ WHERE address = ${address} AND shard_id IN ${stringLiteralArr(shardIds)}
467
+ RETURNING shard_id
468
+ `.pipe(
469
+ execWithLockConnValues,
470
+ Effect.map((rows) => rows.map((row) => row[0] as string))
471
+ )
472
+ },
473
+ mysql: () => {
474
+ if (!disableAdvisoryLocks) return acquireLock
475
+ return (address: string, shardIds: ReadonlyArray<string>) => {
476
+ const shardIdsStr = stringLiteralArr(shardIds)
477
+ return sql<Array<{ shard_id: string }>>`
478
+ UPDATE ${locksTableSql}
479
+ SET acquired_at = ${sqlNow}
480
+ WHERE address = ${address} AND shard_id IN ${shardIdsStr};
481
+ SELECT shard_id FROM ${locksTableSql} WHERE address = ${address} AND shard_id IN ${shardIdsStr}
482
+ `.pipe(
483
+ execWithLockConnUnprepared,
484
+ Effect.map((rows) => rows[1].map((row) => row.shard_id))
485
+ )
486
+ }
487
+ },
404
488
  mssql: () => (address: string, shardIds: ReadonlyArray<string>) =>
405
489
  sql`
406
490
  UPDATE ${locksTableSql}
@@ -462,8 +546,14 @@ export const make = Effect.fnUntraced(function*(options: {
462
546
  ),
463
547
 
464
548
  release: sql.onDialectOrElse({
465
- pg: () =>
466
- Effect.fnUntraced(
549
+ pg: () => {
550
+ if (disableAdvisoryLocks) {
551
+ return (address: string, shardId: string) =>
552
+ sql`DELETE FROM ${locksTableSql} WHERE address = ${address} AND shard_id = ${shardId}`.pipe(
553
+ PersistenceError.refail
554
+ )
555
+ }
556
+ return Effect.fnUntraced(
467
557
  function*(_address, shardId) {
468
558
  const lockNum = lockNumbers.get(shardId)!
469
559
  for (let i = 0; i < 5; i++) {
@@ -481,9 +571,16 @@ export const make = Effect.fnUntraced(function*(options: {
481
571
  Effect.onError(() => lockConn!.unsafeRebuild()),
482
572
  Effect.asVoid,
483
573
  PersistenceError.refail
484
- ),
485
- mysql: () =>
486
- Effect.fnUntraced(
574
+ )
575
+ },
576
+ mysql: () => {
577
+ if (disableAdvisoryLocks) {
578
+ return (address: string, shardId: string) =>
579
+ sql`DELETE FROM ${locksTableSql} WHERE address = ${address} AND shard_id = ${shardId}`.pipe(
580
+ PersistenceError.refail
581
+ )
582
+ }
583
+ return Effect.fnUntraced(
487
584
  function*(_address, shardId) {
488
585
  const lockName = lockNames.get(shardId)!
489
586
  while (true) {
@@ -499,7 +596,8 @@ export const make = Effect.fnUntraced(function*(options: {
499
596
  Effect.onError(() => lockConn!.unsafeRebuild()),
500
597
  Effect.asVoid,
501
598
  PersistenceError.refail
502
- ),
599
+ )
600
+ },
503
601
  orElse: () => (address, shardId) =>
504
602
  sql`DELETE FROM ${locksTableSql} WHERE address = ${address} AND shard_id = ${shardId}`.pipe(
505
603
  PersistenceError.refail
@@ -507,20 +605,34 @@ export const make = Effect.fnUntraced(function*(options: {
507
605
  }),
508
606
 
509
607
  releaseAll: sql.onDialectOrElse({
510
- pg: () => (_address) =>
511
- sql`SELECT pg_advisory_unlock_all()`.pipe(
608
+ pg: () => (address) => {
609
+ if (disableAdvisoryLocks) {
610
+ return sql`DELETE FROM ${locksTableSql} WHERE address = ${address}`.pipe(
611
+ PersistenceError.refail,
612
+ withTracerDisabled
613
+ )
614
+ }
615
+ return sql`SELECT pg_advisory_unlock_all()`.pipe(
512
616
  execWithLockConn,
513
617
  Effect.asVoid,
514
618
  PersistenceError.refail,
515
619
  withTracerDisabled
516
- ),
517
- mysql: () => (_address) =>
518
- sql`SELECT RELEASE_ALL_LOCKS()`.pipe(
620
+ )
621
+ },
622
+ mysql: () => (address) => {
623
+ if (disableAdvisoryLocks) {
624
+ return sql`DELETE FROM ${locksTableSql} WHERE address = ${address}`.pipe(
625
+ PersistenceError.refail,
626
+ withTracerDisabled
627
+ )
628
+ }
629
+ return sql`SELECT RELEASE_ALL_LOCKS()`.pipe(
519
630
  execWithLockConn,
520
631
  Effect.asVoid,
521
632
  PersistenceError.refail,
522
633
  withTracerDisabled
523
- ),
634
+ )
635
+ },
524
636
  orElse: () => (address) =>
525
637
  sql`DELETE FROM ${locksTableSql} WHERE address = ${address}`.pipe(
526
638
  PersistenceError.refail,
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import * as Layer from "effect/Layer"
5
+ import * as MessageStorage from "./MessageStorage.js"
6
+ import * as RunnerHealth from "./RunnerHealth.js"
7
+ import * as Runners from "./Runners.js"
8
+ import * as RunnerStorage from "./RunnerStorage.js"
9
+ import * as Sharding from "./Sharding.js"
10
+ import * as ShardingConfig from "./ShardingConfig.js"
11
+
12
+ /**
13
+ * An in-memory cluster that can be used for testing purposes.
14
+ *
15
+ * MessageStorage is backed by an in-memory driver, and RunnerStorage is backed
16
+ * by an in-memory driver.
17
+ *
18
+ * @since 1.0.0
19
+ * @category Layers
20
+ */
21
+ export const layer: Layer.Layer<
22
+ Sharding.Sharding | Runners.Runners | MessageStorage.MessageStorage | MessageStorage.MemoryDriver
23
+ > = Sharding.layer.pipe(
24
+ Layer.provideMerge(Runners.layerNoop),
25
+ Layer.provideMerge(MessageStorage.layerMemory),
26
+ Layer.provide([RunnerStorage.layerMemory, RunnerHealth.layerNoop]),
27
+ Layer.provide(ShardingConfig.layer())
28
+ )
package/src/index.ts CHANGED
@@ -148,6 +148,11 @@ export * as ShardingConfig from "./ShardingConfig.js"
148
148
  */
149
149
  export * as ShardingRegistrationEvent from "./ShardingRegistrationEvent.js"
150
150
 
151
+ /**
152
+ * @since 1.0.0
153
+ */
154
+ export * as SingleRunner from "./SingleRunner.js"
155
+
151
156
  /**
152
157
  * @since 1.0.0
153
158
  */
@@ -177,3 +182,8 @@ export * as SqlMessageStorage from "./SqlMessageStorage.js"
177
182
  * @since 1.0.0
178
183
  */
179
184
  export * as SqlRunnerStorage from "./SqlRunnerStorage.js"
185
+
186
+ /**
187
+ * @since 1.0.0
188
+ */
189
+ export * as TestRunner from "./TestRunner.js"