@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
@@ -66,21 +66,21 @@ export const make = Effect.fnUntraced(function*(options?: {
66
66
  sql`
67
67
  IF OBJECT_ID(N'${shardsTableSql}', N'U') IS NULL
68
68
  CREATE TABLE ${shardsTableSql} (
69
- shard_id INT PRIMARY KEY,
69
+ shard_id VARCHAR(50) PRIMARY KEY,
70
70
  address VARCHAR(255)
71
71
  )
72
72
  `,
73
73
  mysql: () =>
74
74
  sql`
75
75
  CREATE TABLE IF NOT EXISTS ${shardsTableSql} (
76
- shard_id INT PRIMARY KEY,
76
+ shard_id VARCHAR(50) PRIMARY KEY,
77
77
  address VARCHAR(255)
78
78
  )
79
79
  `,
80
80
  pg: () =>
81
81
  sql`
82
82
  CREATE TABLE IF NOT EXISTS ${shardsTableSql} (
83
- shard_id INT PRIMARY KEY,
83
+ shard_id VARCHAR(50) PRIMARY KEY,
84
84
  address VARCHAR(255)
85
85
  )
86
86
  `,
@@ -88,7 +88,7 @@ export const make = Effect.fnUntraced(function*(options?: {
88
88
  // sqlite
89
89
  sql`
90
90
  CREATE TABLE IF NOT EXISTS ${shardsTableSql} (
91
- shard_id INTEGER PRIMARY KEY,
91
+ shard_id TEXT PRIMARY KEY,
92
92
  address TEXT
93
93
  )
94
94
  `
@@ -102,7 +102,7 @@ export const make = Effect.fnUntraced(function*(options?: {
102
102
  sql`
103
103
  IF OBJECT_ID(N'${locksTableSql}', N'U') IS NULL
104
104
  CREATE TABLE ${locksTableSql} (
105
- shard_id INT PRIMARY KEY,
105
+ shard_id VARCHAR(50) PRIMARY KEY,
106
106
  address VARCHAR(255) NOT NULL,
107
107
  acquired_at DATETIME NOT NULL
108
108
  )
@@ -110,7 +110,7 @@ export const make = Effect.fnUntraced(function*(options?: {
110
110
  mysql: () =>
111
111
  sql`
112
112
  CREATE TABLE IF NOT EXISTS ${locksTableSql} (
113
- shard_id INT PRIMARY KEY,
113
+ shard_id VARCHAR(50) PRIMARY KEY,
114
114
  address VARCHAR(255) NOT NULL,
115
115
  acquired_at DATETIME NOT NULL
116
116
  )
@@ -118,7 +118,7 @@ export const make = Effect.fnUntraced(function*(options?: {
118
118
  pg: () =>
119
119
  sql`
120
120
  CREATE TABLE IF NOT EXISTS ${locksTableSql} (
121
- shard_id INT PRIMARY KEY,
121
+ shard_id VARCHAR(50) PRIMARY KEY,
122
122
  address VARCHAR(255) NOT NULL,
123
123
  acquired_at TIMESTAMP NOT NULL
124
124
  )
@@ -127,7 +127,7 @@ export const make = Effect.fnUntraced(function*(options?: {
127
127
  // sqlite
128
128
  sql`
129
129
  CREATE TABLE IF NOT EXISTS ${locksTableSql} (
130
- shard_id INTEGER PRIMARY KEY,
130
+ shard_id TEXT PRIMARY KEY,
131
131
  address TEXT NOT NULL,
132
132
  acquired_at DATETIME NOT NULL
133
133
  )
@@ -143,7 +143,7 @@ export const make = Effect.fnUntraced(function*(options?: {
143
143
  const sqlNow = sql.literal(sqlNowString)
144
144
 
145
145
  const lockExpiresAt = sql.onDialectOrElse({
146
- pg: () => sql`${sqlNow} - INTERVAL '5 seconds'`,
146
+ pg: () => sql`${sqlNow} - INTERVAL '10 seconds'`,
147
147
  mysql: () => sql`DATE_SUB(${sqlNow}, INTERVAL 5 SECOND)`,
148
148
  mssql: () => sql`DATEADD(SECOND, -5, ${sqlNow})`,
149
149
  orElse: () => sql`datetime(${sqlNow}, '-5 seconds')`
@@ -199,7 +199,7 @@ export const make = Effect.fnUntraced(function*(options?: {
199
199
  orElse: () => sql.literal("FOR UPDATE")
200
200
  })
201
201
 
202
- return yield* ShardStorage.makeEncoded({
202
+ return ShardStorage.makeEncoded({
203
203
  getAssignments: sql`SELECT shard_id, address FROM ${shardsTableSql} ORDER BY shard_id`.values.pipe(
204
204
  PersistenceError.refail,
205
205
  withTracerDisabled
@@ -244,12 +244,12 @@ export const make = Effect.fnUntraced(function*(options?: {
244
244
  function*(address, shardIds) {
245
245
  const values = shardIds.map((shardId) => sql`(${shardId}, ${address}, ${sqlNow})`)
246
246
  yield* acquireLock(address, values)
247
- const currentLocks = yield* sql<{ shard_id: number }>`
247
+ const currentLocks = yield* sql<{ shard_id: string }>`
248
248
  SELECT shard_id FROM ${sql(locksTable)}
249
- WHERE address = ${address} AND ${sql.in("shard_id", shardIds)}
249
+ WHERE address = ${address} AND acquired_at >= ${lockExpiresAt}
250
250
  ${forUpdate}
251
- `
252
- return currentLocks.map((row) => row.shard_id)
251
+ `.values
252
+ return currentLocks.map((row) => row[0] as string)
253
253
  },
254
254
  sql.withTransaction,
255
255
  PersistenceError.refail,
@@ -264,7 +264,7 @@ export const make = Effect.fnUntraced(function*(options?: {
264
264
  sql`SELECT shard_id FROM ${locksTableSql} WHERE address = ${address} AND acquired_at >= ${lockExpiresAt} ${forUpdate}`
265
265
  .values
266
266
  ),
267
- Effect.map((rows) => rows.map((row) => Number(row[0]))),
267
+ Effect.map((rows) => rows.map((row) => row[0] as string)),
268
268
  PersistenceError.refail,
269
269
  withTracerDisabled
270
270
  ),
package/src/index.ts CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ export * as ClusterCron from "./ClusterCron.js"
5
+
1
6
  /**
2
7
  * @since 1.0.0
3
8
  */
@@ -7,6 +7,7 @@ import * as Context from "effect/Context"
7
7
  import * as Duration from "effect/Duration"
8
8
  import type { DurationInput } from "effect/Duration"
9
9
  import * as Effect from "effect/Effect"
10
+ import * as Equal from "effect/Equal"
10
11
  import * as Exit from "effect/Exit"
11
12
  import * as FiberRef from "effect/FiberRef"
12
13
  import { identity } from "effect/Function"
@@ -400,7 +401,7 @@ export const make = Effect.fnUntraced(function*<
400
401
  Effect.suspend(function loop(): Effect.Effect<void> {
401
402
  const toInterrupt = new Set<EntityState>()
402
403
  for (const state of activeServers.values()) {
403
- if (shardId === state.address.shardId) {
404
+ if (shardId[Equal.symbol](state.address.shardId)) {
404
405
  toInterrupt.add(state)
405
406
  }
406
407
  }
@@ -14,7 +14,9 @@ import { ShardStorage } from "../ShardStorage.js"
14
14
 
15
15
  /** @internal */
16
16
  export class State {
17
- static fromStorage = Effect.fnUntraced(function*(numberOfShards: number) {
17
+ static fromStorage = Effect.fnUntraced(function*(
18
+ shardsPerGroup: number
19
+ ) {
18
20
  const storage = yield* ShardStorage
19
21
  const runnerHealth = yield* RunnerHealth
20
22
 
@@ -38,11 +40,11 @@ export class State {
38
40
  }
39
41
 
40
42
  // Determine which shards remain unassigned to a runner
41
- const assignedShards = new Map<ShardId, RunnerAddress>()
43
+ const assignedShards = MutableHashMap.empty<ShardId, RunnerAddress>()
42
44
  const invalidAssignments = Arr.empty<[ShardId, RunnerAddress]>()
43
45
  for (const [shard, address] of storedAssignments) {
44
46
  if (Option.isSome(address) && MutableHashMap.has(aliveRunners, address.value)) {
45
- assignedShards.set(shard, address.value)
47
+ MutableHashMap.set(assignedShards, shard, address.value)
46
48
  } else if (Option.isSome(address)) {
47
49
  invalidAssignments.push([shard, address.value])
48
50
  }
@@ -56,29 +58,96 @@ export class State {
56
58
 
57
59
  // Construct the initial state
58
60
  const now = yield* Clock.currentTimeMillis
59
- const runnerState = MutableHashMap.empty<RunnerAddress, RunnerWithMetadata>()
61
+ const allRunners = MutableHashMap.empty<RunnerAddress, RunnerWithMetadata>()
62
+ const runnerState = new Map<string, MutableHashMap.MutableHashMap<RunnerAddress, RunnerWithMetadata>>()
63
+ // for (const group of groups) {
64
+ // runnerState.set(group, MutableHashMap.empty<RunnerAddress, RunnerWithMetadata>())
65
+ // }
60
66
  for (const [address, runner] of aliveRunners) {
61
- MutableHashMap.set(runnerState, address, RunnerWithMetadata({ runner, registeredAt: now }))
67
+ const withMetadata = RunnerWithMetadata({ runner, registeredAt: now })
68
+ MutableHashMap.set(allRunners, address, withMetadata)
69
+ for (const group of runner.groups) {
70
+ let groupMap = runnerState.get(group)
71
+ if (!groupMap) {
72
+ groupMap = MutableHashMap.empty<RunnerAddress, RunnerWithMetadata>()
73
+ runnerState.set(group, groupMap)
74
+ }
75
+ MutableHashMap.set(groupMap, address, withMetadata)
76
+ }
62
77
  }
63
78
 
64
- const shardState = new Map<ShardId, Option.Option<RunnerAddress>>()
65
- for (let n = 1; n <= numberOfShards; n++) {
66
- const shardId = ShardId.make(n)
67
- shardState.set(shardId, Option.fromNullable(assignedShards.get(shardId)))
79
+ const shardState = new Map<string, Map<number, Option.Option<RunnerAddress>>>()
80
+ for (const group of runnerState.keys()) {
81
+ const groupMap = new Map<number, Option.Option<RunnerAddress>>()
82
+ shardState.set(group, groupMap)
83
+ for (let n = 1; n <= shardsPerGroup; n++) {
84
+ const shardId = new ShardId({ group, id: n })
85
+ groupMap.set(n, MutableHashMap.get(assignedShards, shardId))
86
+ }
68
87
  }
69
88
 
70
- return new State(runnerState, shardState)
89
+ return new State(allRunners, runnerState, shardState, shardsPerGroup)
71
90
  })
72
91
 
73
92
  constructor(
74
- readonly runners: MutableHashMap.MutableHashMap<RunnerAddress, RunnerWithMetadata>,
75
- readonly shards: Map<ShardId, Option.Option<RunnerAddress>>
76
- ) {}
93
+ readonly allRunners: MutableHashMap.MutableHashMap<RunnerAddress, RunnerWithMetadata>,
94
+ readonly runners: Map<string, MutableHashMap.MutableHashMap<RunnerAddress, RunnerWithMetadata>>,
95
+ readonly shards: Map<string, Map<number, Option.Option<RunnerAddress>>>,
96
+ readonly shardsPerGroup: number
97
+ ) {
98
+ this.assignments = MutableHashMap.empty<ShardId, Option.Option<RunnerAddress>>()
99
+ for (const [group, groupMap] of this.shards) {
100
+ for (const [id, address] of groupMap) {
101
+ MutableHashMap.set(this.assignments, new ShardId({ group, id }), address)
102
+ }
103
+ }
104
+ }
105
+
106
+ readonly assignments: MutableHashMap.MutableHashMap<ShardId, Option.Option<RunnerAddress>>
107
+
108
+ addGroup(group: string): void {
109
+ this.runners.set(group, MutableHashMap.empty<RunnerAddress, RunnerWithMetadata>())
110
+ const shardMap = new Map<number, Option.Option<RunnerAddress>>()
111
+ for (let n = 1; n <= this.shardsPerGroup; n++) {
112
+ shardMap.set(n, Option.none())
113
+ MutableHashMap.set(this.assignments, new ShardId({ group, id: n }), Option.none())
114
+ }
115
+ this.shards.set(group, shardMap)
116
+ }
117
+
118
+ addAssignments(
119
+ shards: Iterable<ShardId>,
120
+ address: Option.Option<RunnerAddress>
121
+ ) {
122
+ for (const shardId of shards) {
123
+ MutableHashMap.set(this.assignments, shardId, address)
124
+ this.shards.get(shardId.group)?.set(shardId.id, address)
125
+ }
126
+ }
127
+
128
+ addRunner(runner: Runner, registeredAt: number): void {
129
+ const withMetadata = RunnerWithMetadata({ runner, registeredAt })
130
+ MutableHashMap.set(this.allRunners, runner.address, withMetadata)
131
+ for (const group of runner.groups) {
132
+ if (!this.runners.has(group)) {
133
+ this.addGroup(group)
134
+ }
135
+ const groupMap = this.runners.get(group)!
136
+ MutableHashMap.set(groupMap, runner.address, withMetadata)
137
+ }
138
+ }
139
+
140
+ removeRunner(address: RunnerAddress): void {
141
+ MutableHashMap.remove(this.allRunners, address)
142
+ for (const groupMap of this.runners.values()) {
143
+ MutableHashMap.remove(groupMap, address)
144
+ }
145
+ }
77
146
 
78
147
  get maxVersion(): Option.Option<number> {
79
- if (MutableHashMap.size(this.runners) === 0) return Option.none()
148
+ if (MutableHashMap.size(this.allRunners) === 0) return Option.none()
80
149
  let version: number | undefined = undefined
81
- for (const [, meta] of this.runners) {
150
+ for (const [, meta] of this.allRunners) {
82
151
  if (version === undefined || meta.runner.version > version) {
83
152
  version = meta.runner.version
84
153
  }
@@ -93,31 +162,45 @@ export class State {
93
162
  )
94
163
  }
95
164
 
96
- get shardsPerRunner(): MutableHashMap.MutableHashMap<RunnerAddress, Set<ShardId>> {
97
- const shards = MutableHashMap.empty<RunnerAddress, Set<ShardId>>()
165
+ shardsPerRunner(group: string): MutableHashMap.MutableHashMap<RunnerAddress, Set<number>> {
166
+ const groupRunners = this.runners.get(group)
167
+ const shards = MutableHashMap.empty<RunnerAddress, Set<number>>()
98
168
 
99
- if (MutableHashMap.isEmpty(this.runners)) return shards
100
- MutableHashMap.forEach(this.runners, (_, address) => {
169
+ if (!groupRunners || MutableHashMap.isEmpty(groupRunners)) return shards
170
+ MutableHashMap.forEach(groupRunners, (_, address) => {
101
171
  MutableHashMap.set(shards, address, new Set())
102
172
  })
103
173
 
104
- for (const [shard, address] of this.shards) {
174
+ const assignments = this.shards.get(group)!
175
+ for (const [id, address] of assignments) {
105
176
  if (Option.isNone(address)) continue
106
177
  const shardIds = Option.getOrUndefined(MutableHashMap.get(shards, address.value))!
107
- shardIds.add(shard)
178
+ shardIds.add(id)
108
179
  }
109
180
 
110
181
  return shards
111
182
  }
112
183
 
113
- get averageShardsPerRunner(): number {
114
- const runnerCount = MutableHashMap.size(this.runners)
115
- return runnerCount > 0 ? this.shards.size / runnerCount : 0
184
+ averageShardsPerRunner(group: string): number {
185
+ const runnerCount = MutableHashMap.size(this.runners.get(group) ?? MutableHashMap.empty())
186
+ const shardGroup = this.shards.get(group) ?? new Map()
187
+ return runnerCount > 0 ? shardGroup.size / runnerCount : 0
188
+ }
189
+
190
+ get allUnassignedShards(): Array<ShardId> {
191
+ const unassigned: Array<ShardId> = []
192
+ for (const [shardId, address] of this.assignments) {
193
+ if (Option.isNone(address)) {
194
+ unassigned.push(shardId)
195
+ }
196
+ }
197
+ return unassigned
116
198
  }
117
199
 
118
- get unassignedShards(): Array<ShardId> {
119
- const shardIds: Array<ShardId> = []
120
- for (const [shard, address] of this.shards) {
200
+ unassignedShards(group: string): Array<number> {
201
+ const shardIds: Array<number> = []
202
+ const assignments = this.shards.get(group)!
203
+ for (const [shard, address] of assignments) {
121
204
  if (Option.isNone(address)) {
122
205
  shardIds.push(shard)
123
206
  }
@@ -127,7 +210,7 @@ export class State {
127
210
 
128
211
  private get runnerVersions(): Array<number> {
129
212
  const runnerVersions: Array<number> = []
130
- for (const [, meta] of this.runners) {
213
+ for (const [, meta] of this.allRunners) {
131
214
  runnerVersions.push(meta.runner.version)
132
215
  }
133
216
  return runnerVersions
@@ -143,36 +226,39 @@ export interface RunnerWithMetadata {
143
226
  export const RunnerWithMetadata = (runner: RunnerWithMetadata): RunnerWithMetadata => runner
144
227
 
145
228
  /** @internal */
146
- export function decideAssignmentsForUnassignedShards(state: State): readonly [
147
- assignments: MutableHashMap.MutableHashMap<RunnerAddress, Set<ShardId>>,
148
- unassignments: MutableHashMap.MutableHashMap<RunnerAddress, Set<ShardId>>,
229
+ export function decideAssignmentsForUnassignedShards(state: State, group: string): readonly [
230
+ assignments: MutableHashMap.MutableHashMap<RunnerAddress, Set<number>>,
231
+ unassignments: MutableHashMap.MutableHashMap<RunnerAddress, Set<number>>,
149
232
  changes: MutableHashSet.MutableHashSet<RunnerAddress>
150
233
  ] {
151
- return pickNewRunners(state.unassignedShards, state, true, 1)
234
+ return pickNewRunners(state.unassignedShards(group), state, group, true, 1)
152
235
  }
153
236
 
154
- const allocationOrder: Order.Order<[ShardId, number, number]> = Order.combine(
237
+ const allocationOrder: Order.Order<[number, number, number]> = Order.combine(
155
238
  Order.mapInput(Order.number, ([, shards]) => shards),
156
239
  Order.mapInput(Order.number, ([, , registeredAt]) => registeredAt)
157
240
  )
158
241
 
159
242
  /** @internal */
160
- export function decideAssignmentsForUnbalancedShards(state: State, rate: number): readonly [
161
- assignments: MutableHashMap.MutableHashMap<RunnerAddress, Set<ShardId>>,
162
- unassignments: MutableHashMap.MutableHashMap<RunnerAddress, Set<ShardId>>,
243
+ export function decideAssignmentsForUnbalancedShards(state: State, group: string, rate: number): readonly [
244
+ assignments: MutableHashMap.MutableHashMap<RunnerAddress, Set<number>>,
245
+ unassignments: MutableHashMap.MutableHashMap<RunnerAddress, Set<number>>,
163
246
  changes: MutableHashSet.MutableHashSet<RunnerAddress>
164
247
  ] {
165
- const shardsPerRunner = state.shardsPerRunner
248
+ const shardsPerRunner = state.shardsPerRunner(group)
166
249
  const maxVersion = state.maxVersion
167
- const extraShardsToAllocate = Arr.empty<[ShardId, shardsInverse: number, registeredAt: number]>()
250
+ const extraShardsToAllocate = Arr.empty<[number, shardsInverse: number, registeredAt: number]>()
251
+
252
+ const runnerGroup = state.runners.get(group)!
253
+ const shardsGroup = state.shards.get(group)!
168
254
 
169
255
  if (state.allRunnersHaveVersion(maxVersion)) {
170
- const averageShardsPerRunner = state.averageShardsPerRunner
256
+ const averageShardsPerRunner = state.averageShardsPerRunner(group)
171
257
  MutableHashMap.forEach(shardsPerRunner, (shards) => {
172
258
  // Count how many extra shards there are compared to the average
173
259
  const extraShards = Math.max(0, shards.size - averageShardsPerRunner)
174
260
  for (const shard of takeRandom(shards, extraShards)) {
175
- const maybeAddress = state.shards.get(shard) ?? Option.none()
261
+ const maybeAddress = shardsGroup.get(shard) ?? Option.none()
176
262
  if (Option.isNone(maybeAddress)) {
177
263
  extraShardsToAllocate.push([shard, Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER])
178
264
  continue
@@ -184,7 +270,7 @@ export function decideAssignmentsForUnbalancedShards(state: State, rate: number)
184
270
  onNone: () => Number.MIN_SAFE_INTEGER,
185
271
  onSome: (shards) => -shards.size
186
272
  }),
187
- Option.match(MutableHashMap.get(state.runners, address), {
273
+ Option.match(MutableHashMap.get(runnerGroup, address), {
188
274
  onNone: () => Number.MIN_SAFE_INTEGER,
189
275
  onSome: (meta) => meta.registeredAt
190
276
  })
@@ -195,23 +281,24 @@ export function decideAssignmentsForUnbalancedShards(state: State, rate: number)
195
281
 
196
282
  const sortedShardsToRebalance = extraShardsToAllocate.sort(allocationOrder).map(([shard]) => shard)
197
283
 
198
- return pickNewRunners(sortedShardsToRebalance, state, false, rate, shardsPerRunner, maxVersion)
284
+ return pickNewRunners(sortedShardsToRebalance, state, group, false, rate, shardsPerRunner, maxVersion)
199
285
  }
200
286
 
201
287
  function pickNewRunners(
202
- shardsToRebalance: ReadonlyArray<ShardId>,
288
+ shardsToRebalance: ReadonlyArray<number>,
203
289
  state: State,
290
+ group: string,
204
291
  immediate: boolean,
205
292
  rate: number,
206
- shardsPerRunner = state.shardsPerRunner,
293
+ shardsPerRunner = state.shardsPerRunner(group),
207
294
  maybeMaxVersion = state.maxVersion
208
295
  ): readonly [
209
- assignments: MutableHashMap.MutableHashMap<RunnerAddress, Set<ShardId>>,
210
- unassignments: MutableHashMap.MutableHashMap<RunnerAddress, Set<ShardId>>,
296
+ assignments: MutableHashMap.MutableHashMap<RunnerAddress, Set<number>>,
297
+ unassignments: MutableHashMap.MutableHashMap<RunnerAddress, Set<number>>,
211
298
  changes: MutableHashSet.MutableHashSet<RunnerAddress>
212
299
  ] {
213
- const addressAssignments = MutableHashMap.empty<RunnerAddress, Set<ShardId>>()
214
- const unassignments = MutableHashMap.empty<RunnerAddress, Set<ShardId>>()
300
+ const addressAssignments = MutableHashMap.empty<RunnerAddress, Set<number>>()
301
+ const unassignments = MutableHashMap.empty<RunnerAddress, Set<number>>()
215
302
  const changes = MutableHashSet.empty<RunnerAddress>()
216
303
 
217
304
  if (Option.isNone(maybeMaxVersion)) {
@@ -219,14 +306,17 @@ function pickNewRunners(
219
306
  }
220
307
  const maxVersion = maybeMaxVersion.value
221
308
 
309
+ const runnerGroup = state.runners.get(group)!
310
+ const shardsGroup = state.shards.get(group)!
311
+
222
312
  for (const shardId of shardsToRebalance) {
223
313
  // Find the runner with the fewest assigned shards
224
314
  let candidate: RunnerAddress | undefined
225
- let candidateShards: Set<ShardId> | undefined
315
+ let candidateShards: Set<number> | undefined
226
316
 
227
317
  for (const [address, shards] of shardsPerRunner) {
228
318
  // Keep only runners with the maximum version
229
- const maybeRunnerMeta = MutableHashMap.get(state.runners, address)
319
+ const maybeRunnerMeta = MutableHashMap.get(runnerGroup, address)
230
320
  if (Option.isNone(maybeRunnerMeta)) continue
231
321
  const runnerMeta = maybeRunnerMeta.value
232
322
  if (runnerMeta.runner.version !== maxVersion) continue
@@ -238,7 +328,7 @@ function pickNewRunners(
238
328
  // occur immediately
239
329
  if (!immediate) {
240
330
  const assignmentCount = Option.getOrUndefined(MutableHashMap.get(addressAssignments, address))?.size ?? 0
241
- if (assignmentCount >= state.shards.size * rate) continue
331
+ if (assignmentCount >= shardsGroup.size * rate) continue
242
332
  }
243
333
 
244
334
  if (candidate === undefined || shards.size < candidateShards!.size) {
@@ -249,7 +339,7 @@ function pickNewRunners(
249
339
  if (!candidate || !candidateShards) break
250
340
 
251
341
  // If the old runner is the same as the new runner, do nothing
252
- const oldRunner = Option.getOrUndefined(state.shards.get(shardId) ?? Option.none())
342
+ const oldRunner = Option.getOrUndefined(shardsGroup.get(shardId) ?? Option.none())
253
343
  if (oldRunner && oldRunner.toString() === candidate.toString()) {
254
344
  continue
255
345
  }
@@ -316,3 +406,19 @@ function swap<A>(array: Array<A>, i: number, j: number): ReadonlyArray<A> {
316
406
  array[j] = tmp
317
407
  return array
318
408
  }
409
+
410
+ /** @internal */
411
+ export const addAllNested = <K, V>(
412
+ self: MutableHashMap.MutableHashMap<K, MutableHashSet.MutableHashSet<V>>,
413
+ key: K,
414
+ values: Iterable<V>
415
+ ) => {
416
+ const oset = MutableHashMap.get(self, key)
417
+ if (Option.isSome(oset)) {
418
+ for (const value of values) {
419
+ MutableHashSet.add(oset.value, value)
420
+ }
421
+ } else {
422
+ MutableHashMap.set(self, key, MutableHashSet.fromIterable(values))
423
+ }
424
+ }