@effect/cluster 0.50.6 → 0.52.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.
Files changed (232) hide show
  1. package/RunnerStorage/package.json +6 -0
  2. package/SqlRunnerStorage/package.json +6 -0
  3. package/dist/cjs/ClusterError.js +2 -24
  4. package/dist/cjs/ClusterError.js.map +1 -1
  5. package/dist/cjs/ClusterMetrics.js +13 -15
  6. package/dist/cjs/ClusterMetrics.js.map +1 -1
  7. package/dist/cjs/ClusterSchema.js +17 -2
  8. package/dist/cjs/ClusterSchema.js.map +1 -1
  9. package/dist/cjs/ClusterWorkflowEngine.js +50 -83
  10. package/dist/cjs/ClusterWorkflowEngine.js.map +1 -1
  11. package/dist/cjs/Entity.js +1 -13
  12. package/dist/cjs/Entity.js.map +1 -1
  13. package/dist/cjs/EntityAddress.js +9 -1
  14. package/dist/cjs/EntityAddress.js.map +1 -1
  15. package/dist/cjs/EntityId.js +7 -1
  16. package/dist/cjs/EntityId.js.map +1 -1
  17. package/dist/cjs/EntityProxy.js +1 -1
  18. package/dist/cjs/EntityProxy.js.map +1 -1
  19. package/dist/cjs/HttpRunner.js +69 -43
  20. package/dist/cjs/HttpRunner.js.map +1 -1
  21. package/dist/cjs/MessageStorage.js +64 -16
  22. package/dist/cjs/MessageStorage.js.map +1 -1
  23. package/dist/cjs/Runner.js +3 -3
  24. package/dist/cjs/Runner.js.map +1 -1
  25. package/dist/cjs/RunnerAddress.js +7 -0
  26. package/dist/cjs/RunnerAddress.js.map +1 -1
  27. package/dist/cjs/RunnerHealth.js +91 -32
  28. package/dist/cjs/RunnerHealth.js.map +1 -1
  29. package/dist/cjs/RunnerServer.js +38 -24
  30. package/dist/cjs/RunnerServer.js.map +1 -1
  31. package/dist/cjs/RunnerStorage.js +100 -0
  32. package/dist/cjs/RunnerStorage.js.map +1 -0
  33. package/dist/cjs/Runners.js +18 -22
  34. package/dist/cjs/Runners.js.map +1 -1
  35. package/dist/cjs/ShardId.js +17 -7
  36. package/dist/cjs/ShardId.js.map +1 -1
  37. package/dist/cjs/Sharding.js +444 -320
  38. package/dist/cjs/Sharding.js.map +1 -1
  39. package/dist/cjs/ShardingConfig.js +10 -14
  40. package/dist/cjs/ShardingConfig.js.map +1 -1
  41. package/dist/cjs/Snowflake.js +1 -1
  42. package/dist/cjs/SocketRunner.js +1 -1
  43. package/dist/cjs/SocketRunner.js.map +1 -1
  44. package/dist/cjs/SqlMessageStorage.js +22 -28
  45. package/dist/cjs/SqlMessageStorage.js.map +1 -1
  46. package/dist/cjs/SqlRunnerStorage.js +375 -0
  47. package/dist/cjs/SqlRunnerStorage.js.map +1 -0
  48. package/dist/cjs/index.js +5 -15
  49. package/dist/cjs/internal/entityManager.js +42 -23
  50. package/dist/cjs/internal/entityManager.js.map +1 -1
  51. package/dist/dts/ClusterError.d.ts +0 -22
  52. package/dist/dts/ClusterError.d.ts.map +1 -1
  53. package/dist/dts/ClusterMetrics.d.ts +4 -14
  54. package/dist/dts/ClusterMetrics.d.ts.map +1 -1
  55. package/dist/dts/ClusterSchema.d.ts +9 -1
  56. package/dist/dts/ClusterSchema.d.ts.map +1 -1
  57. package/dist/dts/ClusterWorkflowEngine.d.ts.map +1 -1
  58. package/dist/dts/Entity.d.ts +3 -14
  59. package/dist/dts/Entity.d.ts.map +1 -1
  60. package/dist/dts/EntityAddress.d.ts +11 -0
  61. package/dist/dts/EntityAddress.d.ts.map +1 -1
  62. package/dist/dts/EntityId.d.ts +5 -0
  63. package/dist/dts/EntityId.d.ts.map +1 -1
  64. package/dist/dts/EntityProxy.d.ts +5 -6
  65. package/dist/dts/EntityProxy.d.ts.map +1 -1
  66. package/dist/dts/HttpRunner.d.ts +48 -25
  67. package/dist/dts/HttpRunner.d.ts.map +1 -1
  68. package/dist/dts/MessageStorage.d.ts +13 -5
  69. package/dist/dts/MessageStorage.d.ts.map +1 -1
  70. package/dist/dts/Runner.d.ts +4 -4
  71. package/dist/dts/Runner.d.ts.map +1 -1
  72. package/dist/dts/RunnerAddress.d.ts +5 -0
  73. package/dist/dts/RunnerAddress.d.ts.map +1 -1
  74. package/dist/dts/RunnerHealth.d.ts +24 -16
  75. package/dist/dts/RunnerHealth.d.ts.map +1 -1
  76. package/dist/dts/RunnerServer.d.ts +5 -4
  77. package/dist/dts/RunnerServer.d.ts.map +1 -1
  78. package/dist/dts/{ShardStorage.d.ts → RunnerStorage.d.ts} +41 -54
  79. package/dist/dts/RunnerStorage.d.ts.map +1 -0
  80. package/dist/dts/Runners.d.ts +15 -11
  81. package/dist/dts/Runners.d.ts.map +1 -1
  82. package/dist/dts/ShardId.d.ts +1 -1
  83. package/dist/dts/ShardId.d.ts.map +1 -1
  84. package/dist/dts/Sharding.d.ts +20 -10
  85. package/dist/dts/Sharding.d.ts.map +1 -1
  86. package/dist/dts/ShardingConfig.d.ts +40 -14
  87. package/dist/dts/ShardingConfig.d.ts.map +1 -1
  88. package/dist/dts/SocketRunner.d.ts +4 -3
  89. package/dist/dts/SocketRunner.d.ts.map +1 -1
  90. package/dist/dts/SqlMessageStorage.d.ts +2 -3
  91. package/dist/dts/SqlMessageStorage.d.ts.map +1 -1
  92. package/dist/dts/SqlRunnerStorage.d.ts +40 -0
  93. package/dist/dts/SqlRunnerStorage.d.ts.map +1 -0
  94. package/dist/dts/index.d.ts +4 -24
  95. package/dist/dts/index.d.ts.map +1 -1
  96. package/dist/esm/ClusterError.js +0 -21
  97. package/dist/esm/ClusterError.js.map +1 -1
  98. package/dist/esm/ClusterMetrics.js +12 -14
  99. package/dist/esm/ClusterMetrics.js.map +1 -1
  100. package/dist/esm/ClusterSchema.js +17 -2
  101. package/dist/esm/ClusterSchema.js.map +1 -1
  102. package/dist/esm/ClusterWorkflowEngine.js +50 -83
  103. package/dist/esm/ClusterWorkflowEngine.js.map +1 -1
  104. package/dist/esm/Entity.js +0 -12
  105. package/dist/esm/Entity.js.map +1 -1
  106. package/dist/esm/EntityAddress.js +7 -0
  107. package/dist/esm/EntityAddress.js.map +1 -1
  108. package/dist/esm/EntityId.js +5 -0
  109. package/dist/esm/EntityId.js.map +1 -1
  110. package/dist/esm/EntityProxy.js +2 -2
  111. package/dist/esm/EntityProxy.js.map +1 -1
  112. package/dist/esm/HttpRunner.js +62 -39
  113. package/dist/esm/HttpRunner.js.map +1 -1
  114. package/dist/esm/MessageStorage.js +65 -17
  115. package/dist/esm/MessageStorage.js.map +1 -1
  116. package/dist/esm/Runner.js +3 -3
  117. package/dist/esm/Runner.js.map +1 -1
  118. package/dist/esm/RunnerAddress.js +7 -0
  119. package/dist/esm/RunnerAddress.js.map +1 -1
  120. package/dist/esm/RunnerHealth.js +88 -30
  121. package/dist/esm/RunnerHealth.js.map +1 -1
  122. package/dist/esm/RunnerServer.js +38 -24
  123. package/dist/esm/RunnerServer.js.map +1 -1
  124. package/dist/esm/RunnerStorage.js +90 -0
  125. package/dist/esm/RunnerStorage.js.map +1 -0
  126. package/dist/esm/Runners.js +19 -23
  127. package/dist/esm/Runners.js.map +1 -1
  128. package/dist/esm/ShardId.js +16 -6
  129. package/dist/esm/ShardId.js.map +1 -1
  130. package/dist/esm/Sharding.js +447 -323
  131. package/dist/esm/Sharding.js.map +1 -1
  132. package/dist/esm/ShardingConfig.js +10 -14
  133. package/dist/esm/ShardingConfig.js.map +1 -1
  134. package/dist/esm/Snowflake.js +1 -1
  135. package/dist/esm/SocketRunner.js +1 -1
  136. package/dist/esm/SocketRunner.js.map +1 -1
  137. package/dist/esm/SqlMessageStorage.js +22 -28
  138. package/dist/esm/SqlMessageStorage.js.map +1 -1
  139. package/dist/esm/SqlRunnerStorage.js +366 -0
  140. package/dist/esm/SqlRunnerStorage.js.map +1 -0
  141. package/dist/esm/index.js +4 -24
  142. package/dist/esm/index.js.map +1 -1
  143. package/dist/esm/internal/entityManager.js +41 -22
  144. package/dist/esm/internal/entityManager.js.map +1 -1
  145. package/package.json +20 -60
  146. package/src/ClusterError.ts +0 -24
  147. package/src/ClusterMetrics.ts +12 -16
  148. package/src/ClusterSchema.ts +17 -2
  149. package/src/ClusterWorkflowEngine.ts +48 -80
  150. package/src/Entity.ts +3 -21
  151. package/src/EntityAddress.ts +10 -0
  152. package/src/EntityId.ts +6 -0
  153. package/src/EntityProxy.ts +10 -10
  154. package/src/HttpRunner.ts +132 -67
  155. package/src/MessageStorage.ts +89 -24
  156. package/src/Runner.ts +4 -4
  157. package/src/RunnerAddress.ts +8 -0
  158. package/src/RunnerHealth.ts +119 -56
  159. package/src/RunnerServer.ts +64 -47
  160. package/src/RunnerStorage.ts +218 -0
  161. package/src/Runners.ts +32 -45
  162. package/src/ShardId.ts +14 -3
  163. package/src/Sharding.ts +561 -417
  164. package/src/ShardingConfig.ts +39 -31
  165. package/src/Snowflake.ts +1 -1
  166. package/src/SocketRunner.ts +6 -4
  167. package/src/SqlMessageStorage.ts +28 -30
  168. package/src/SqlRunnerStorage.ts +537 -0
  169. package/src/index.ts +4 -29
  170. package/src/internal/entityManager.ts +45 -29
  171. package/HttpCommon/package.json +0 -6
  172. package/HttpShardManager/package.json +0 -6
  173. package/ShardManager/package.json +0 -6
  174. package/ShardStorage/package.json +0 -6
  175. package/SocketShardManager/package.json +0 -6
  176. package/SqlShardStorage/package.json +0 -6
  177. package/SynchronizedClock/package.json +0 -6
  178. package/dist/cjs/HttpCommon.js +0 -48
  179. package/dist/cjs/HttpCommon.js.map +0 -1
  180. package/dist/cjs/HttpShardManager.js +0 -139
  181. package/dist/cjs/HttpShardManager.js.map +0 -1
  182. package/dist/cjs/ShardManager.js +0 -549
  183. package/dist/cjs/ShardManager.js.map +0 -1
  184. package/dist/cjs/ShardStorage.js +0 -151
  185. package/dist/cjs/ShardStorage.js.map +0 -1
  186. package/dist/cjs/SocketShardManager.js +0 -32
  187. package/dist/cjs/SocketShardManager.js.map +0 -1
  188. package/dist/cjs/SqlShardStorage.js +0 -253
  189. package/dist/cjs/SqlShardStorage.js.map +0 -1
  190. package/dist/cjs/SynchronizedClock.js +0 -65
  191. package/dist/cjs/SynchronizedClock.js.map +0 -1
  192. package/dist/cjs/internal/shardManager.js +0 -353
  193. package/dist/cjs/internal/shardManager.js.map +0 -1
  194. package/dist/dts/HttpCommon.d.ts +0 -25
  195. package/dist/dts/HttpCommon.d.ts.map +0 -1
  196. package/dist/dts/HttpShardManager.d.ts +0 -119
  197. package/dist/dts/HttpShardManager.d.ts.map +0 -1
  198. package/dist/dts/ShardManager.d.ts +0 -459
  199. package/dist/dts/ShardManager.d.ts.map +0 -1
  200. package/dist/dts/ShardStorage.d.ts.map +0 -1
  201. package/dist/dts/SocketShardManager.d.ts +0 -17
  202. package/dist/dts/SocketShardManager.d.ts.map +0 -1
  203. package/dist/dts/SqlShardStorage.d.ts +0 -38
  204. package/dist/dts/SqlShardStorage.d.ts.map +0 -1
  205. package/dist/dts/SynchronizedClock.d.ts +0 -19
  206. package/dist/dts/SynchronizedClock.d.ts.map +0 -1
  207. package/dist/dts/internal/shardManager.d.ts +0 -2
  208. package/dist/dts/internal/shardManager.d.ts.map +0 -1
  209. package/dist/esm/HttpCommon.js +0 -38
  210. package/dist/esm/HttpCommon.js.map +0 -1
  211. package/dist/esm/HttpShardManager.js +0 -128
  212. package/dist/esm/HttpShardManager.js.map +0 -1
  213. package/dist/esm/ShardManager.js +0 -535
  214. package/dist/esm/ShardManager.js.map +0 -1
  215. package/dist/esm/ShardStorage.js +0 -141
  216. package/dist/esm/ShardStorage.js.map +0 -1
  217. package/dist/esm/SocketShardManager.js +0 -24
  218. package/dist/esm/SocketShardManager.js.map +0 -1
  219. package/dist/esm/SqlShardStorage.js +0 -244
  220. package/dist/esm/SqlShardStorage.js.map +0 -1
  221. package/dist/esm/SynchronizedClock.js +0 -57
  222. package/dist/esm/SynchronizedClock.js.map +0 -1
  223. package/dist/esm/internal/shardManager.js +0 -342
  224. package/dist/esm/internal/shardManager.js.map +0 -1
  225. package/src/HttpCommon.ts +0 -73
  226. package/src/HttpShardManager.ts +0 -273
  227. package/src/ShardManager.ts +0 -823
  228. package/src/ShardStorage.ts +0 -297
  229. package/src/SocketShardManager.ts +0 -48
  230. package/src/SqlShardStorage.ts +0 -329
  231. package/src/SynchronizedClock.ts +0 -82
  232. package/src/internal/shardManager.ts +0 -412
@@ -1,15 +1,19 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
+ import * as FileSystem from "@effect/platform/FileSystem"
5
+ import * as HttpClient from "@effect/platform/HttpClient"
6
+ import * as HttpClientRequest from "@effect/platform/HttpClientRequest"
7
+ import * as HttpClientResponse from "@effect/platform/HttpClientResponse"
4
8
  import * as Context from "effect/Context"
5
9
  import * as Effect from "effect/Effect"
10
+ import { identity } from "effect/Function"
6
11
  import * as Layer from "effect/Layer"
7
- import * as RcMap from "effect/RcMap"
12
+ import * as Schedule from "effect/Schedule"
13
+ import * as Schema from "effect/Schema"
8
14
  import type * as Scope from "effect/Scope"
9
- import * as MessageStorage from "./MessageStorage.js"
10
15
  import type { RunnerAddress } from "./RunnerAddress.js"
11
16
  import * as Runners from "./Runners.js"
12
- import type { ShardingConfig } from "./ShardingConfig.js"
13
17
 
14
18
  /**
15
19
  * Represents the service used to check if a Runner is healthy.
@@ -24,46 +28,10 @@ import type { ShardingConfig } from "./ShardingConfig.js"
24
28
  export class RunnerHealth extends Context.Tag("@effect/cluster/RunnerHealth")<
25
29
  RunnerHealth,
26
30
  {
27
- /**
28
- * Used to indicate that a Runner is connected to this host and is healthy,
29
- * while the Scope is active.
30
- */
31
- readonly onConnection: (address: RunnerAddress) => Effect.Effect<void, never, Scope.Scope>
32
31
  readonly isAlive: (address: RunnerAddress) => Effect.Effect<boolean>
33
32
  }
34
33
  >() {}
35
34
 
36
- /**
37
- * @since 1.0.0
38
- * @category Constructors
39
- */
40
- export const make: (
41
- options: { readonly isAlive: (address: RunnerAddress) => Effect.Effect<boolean> }
42
- ) => Effect.Effect<
43
- RunnerHealth["Type"],
44
- never,
45
- Scope.Scope
46
- > = Effect.fnUntraced(function*(options: {
47
- readonly isAlive: (address: RunnerAddress) => Effect.Effect<boolean>
48
- }) {
49
- const connections = yield* RcMap.make({
50
- lookup: (_address: RunnerAddress) => Effect.void
51
- })
52
-
53
- const onConnection = (address: RunnerAddress) => RcMap.get(connections, address)
54
- const isAlive = Effect.fnUntraced(function*(address: RunnerAddress) {
55
- if (yield* RcMap.has(connections, address)) {
56
- return true
57
- }
58
- return yield* options.isAlive(address)
59
- })
60
-
61
- return RunnerHealth.of({
62
- onConnection,
63
- isAlive
64
- })
65
- })
66
-
67
35
  /**
68
36
  * A layer which will **always** consider a Runner healthy.
69
37
  *
@@ -72,12 +40,9 @@ export const make: (
72
40
  * @since 1.0.0
73
41
  * @category layers
74
42
  */
75
- export const layerNoop = Layer.scoped(
76
- RunnerHealth,
77
- make({
78
- isAlive: () => Effect.succeed(true)
79
- })
80
- )
43
+ export const layerNoop = Layer.succeed(RunnerHealth, {
44
+ isAlive: () => Effect.succeed(true)
45
+ })
81
46
 
82
47
  /**
83
48
  * @since 1.0.0
@@ -89,16 +54,17 @@ export const makePing: Effect.Effect<
89
54
  Runners.Runners | Scope.Scope
90
55
  > = Effect.gen(function*() {
91
56
  const runners = yield* Runners.Runners
57
+ const schedule = Schedule.spaced(500)
92
58
 
93
59
  function isAlive(address: RunnerAddress): Effect.Effect<boolean> {
94
60
  return runners.ping(address).pipe(
95
- Effect.timeout(3000),
96
- Effect.retry({ times: 3 }),
61
+ Effect.timeout(10_000),
62
+ Effect.retry({ times: 5, schedule }),
97
63
  Effect.isSuccess
98
64
  )
99
65
  }
100
66
 
101
- return yield* make({ isAlive })
67
+ return RunnerHealth.of({ isAlive })
102
68
  })
103
69
 
104
70
  /**
@@ -107,23 +73,120 @@ export const makePing: Effect.Effect<
107
73
  * @since 1.0.0
108
74
  * @category layers
109
75
  */
110
- export const layer: Layer.Layer<
76
+ export const layerPing: Layer.Layer<
111
77
  RunnerHealth,
112
78
  never,
113
79
  Runners.Runners
114
80
  > = Layer.scoped(RunnerHealth, makePing)
115
81
 
116
82
  /**
117
- * A layer which will ping a Runner directly to check if it is healthy.
83
+ * @since 1.0.0
84
+ * @category Constructors
85
+ */
86
+ export const makeK8s = Effect.fnUntraced(function*(options?: {
87
+ readonly namespace?: string | undefined
88
+ readonly labelSelector?: string | undefined
89
+ }) {
90
+ const fs = yield* FileSystem.FileSystem
91
+ const token = yield* fs.readFileString("/var/run/secrets/kubernetes.io/serviceaccount/token").pipe(
92
+ Effect.option
93
+ )
94
+ const client = (yield* HttpClient.HttpClient).pipe(
95
+ HttpClient.filterStatusOk
96
+ )
97
+ const baseRequest = HttpClientRequest.get("https://kubernetes.default.svc/api").pipe(
98
+ token._tag === "Some" ? HttpClientRequest.bearerToken(token.value.trim()) : identity
99
+ )
100
+ const getPods = baseRequest.pipe(
101
+ HttpClientRequest.appendUrl(options?.namespace ? `/v1/namespaces/${options.namespace}/pods` : "/v1/pods"),
102
+ HttpClientRequest.setUrlParam("fieldSelector", "status.phase=Running"),
103
+ options?.labelSelector ? HttpClientRequest.setUrlParam("labelSelector", options.labelSelector) : identity
104
+ )
105
+ const allPods = yield* client.execute(getPods).pipe(
106
+ Effect.flatMap(HttpClientResponse.schemaBodyJson(PodList)),
107
+ Effect.map((list) => {
108
+ const pods = new Map<string, Pod>()
109
+ for (let i = 0; i < list.items.length; i++) {
110
+ const pod = list.items[i]
111
+ pods.set(pod.status.podIP, pod)
112
+ }
113
+ return pods
114
+ }),
115
+ Effect.tapErrorCause((cause) => Effect.logWarning("Failed to fetch pods from Kubernetes API", cause)),
116
+ Effect.cachedWithTTL("10 seconds")
117
+ )
118
+
119
+ return RunnerHealth.of({
120
+ isAlive: (address) =>
121
+ allPods.pipe(
122
+ Effect.map((pods) => pods.get(address.host)?.isReady ?? false),
123
+ Effect.catchAllCause(() => Effect.succeed(true))
124
+ )
125
+ })
126
+ })
127
+
128
+ class Pod extends Schema.Class<Pod>("effect/cluster/RunnerHealth/Pod")({
129
+ status: Schema.Struct({
130
+ phase: Schema.String,
131
+ conditions: Schema.Array(Schema.Struct({
132
+ type: Schema.String,
133
+ status: Schema.String,
134
+ lastTransitionTime: Schema.String
135
+ })),
136
+ podIP: Schema.String
137
+ })
138
+ }) {
139
+ get isReady(): boolean {
140
+ let initializedAt: string | undefined
141
+ let readyAt: string | undefined
142
+ for (let i = 0; i < this.status.conditions.length; i++) {
143
+ const condition = this.status.conditions[i]
144
+ switch (condition.type) {
145
+ case "Initialized": {
146
+ if (condition.status !== "True") {
147
+ return true
148
+ }
149
+ initializedAt = condition.lastTransitionTime
150
+ break
151
+ }
152
+ case "Ready": {
153
+ if (condition.status === "True") {
154
+ return true
155
+ }
156
+ readyAt = condition.lastTransitionTime
157
+ break
158
+ }
159
+ }
160
+ }
161
+ // if the pod is still booting up, consider it ready as it would have
162
+ // already registered itself with RunnerStorage by now
163
+ return initializedAt === readyAt
164
+ }
165
+ }
166
+
167
+ const PodList = Schema.Struct({
168
+ items: Schema.Array(Pod)
169
+ })
170
+
171
+ /**
172
+ * A layer which will check the Kubernetes API to see if a Runner is healthy.
173
+ *
174
+ * The provided HttpClient will need to add the pod's CA certificate to its
175
+ * trusted root certificates in order to communicate with the Kubernetes API.
176
+ *
177
+ * The pod service account will also need to have permissions to list pods in
178
+ * order to use this layer.
118
179
  *
119
180
  * @since 1.0.0
120
181
  * @category layers
121
182
  */
122
- export const layerRpc: Layer.Layer<
183
+ export const layerK8s = (
184
+ options?: {
185
+ readonly namespace?: string | undefined
186
+ readonly labelSelector?: string | undefined
187
+ } | undefined
188
+ ): Layer.Layer<
123
189
  RunnerHealth,
124
190
  never,
125
- Runners.RpcClientProtocol | ShardingConfig
126
- > = layer.pipe(
127
- Layer.provide(Runners.layerRpc),
128
- Layer.provide(MessageStorage.layerNoop)
129
- )
191
+ HttpClient.HttpClient | FileSystem.FileSystem
192
+ > => Layer.effect(RunnerHealth, makeK8s(options))
@@ -3,20 +3,22 @@
3
3
  */
4
4
  import * as RpcServer from "@effect/rpc/RpcServer"
5
5
  import * as Effect from "effect/Effect"
6
+ import type * as Exit from "effect/Exit"
7
+ import * as Fiber from "effect/Fiber"
6
8
  import { constant } from "effect/Function"
7
9
  import * as Layer from "effect/Layer"
8
10
  import * as Mailbox from "effect/Mailbox"
9
11
  import * as Option from "effect/Option"
10
- import * as ClusterError from "./ClusterError.js"
12
+ import * as Runtime from "effect/Runtime"
13
+ import type * as ClusterError from "./ClusterError.js"
11
14
  import * as Message from "./Message.js"
12
15
  import * as MessageStorage from "./MessageStorage.js"
13
16
  import * as Reply from "./Reply.js"
17
+ import * as RunnerHealth from "./RunnerHealth.js"
14
18
  import * as Runners from "./Runners.js"
19
+ import type * as RunnerStorage from "./RunnerStorage.js"
15
20
  import * as Sharding from "./Sharding.js"
16
21
  import { ShardingConfig } from "./ShardingConfig.js"
17
- import * as ShardManager from "./ShardManager.js"
18
- import * as ShardStorage from "./ShardStorage.js"
19
- import * as SynchronizedClock from "./SynchronizedClock.js"
20
22
 
21
23
  const constVoid = constant(Effect.void)
22
24
 
@@ -41,41 +43,60 @@ export const layerHandlers = Runners.Rpcs.toLayer(Effect.gen(function*() {
41
43
  : new Message.IncomingEnvelope({ envelope })
42
44
  ),
43
45
  Effect: ({ persisted, request }) => {
44
- let resume: (reply: Effect.Effect<Reply.ReplyEncoded<any>, ClusterError.EntityNotAssignedToRunner>) => void
45
- let replyEncoded: Reply.ReplyEncoded<any> | undefined
46
+ let replyEncoded:
47
+ | Effect.Effect<
48
+ Reply.ReplyEncoded<any>,
49
+ ClusterError.EntityNotAssignedToRunner
50
+ >
51
+ | undefined = undefined
52
+ let resume = (reply: Effect.Effect<Reply.ReplyEncoded<any>, ClusterError.EntityNotAssignedToRunner>) => {
53
+ replyEncoded = reply
54
+ }
46
55
  const message = new Message.IncomingRequest({
47
56
  envelope: request,
48
57
  lastSentReply: Option.none(),
49
58
  respond(reply) {
50
- return Effect.flatMap(Reply.serialize(reply), (reply) => {
51
- if (resume) {
52
- resume(Effect.succeed(reply))
53
- } else {
54
- replyEncoded = reply
55
- }
56
- return Effect.void
57
- })
59
+ resume(Effect.orDie(Reply.serialize(reply)))
60
+ return Effect.void
58
61
  }
59
62
  })
63
+ if (persisted) {
64
+ return Effect.async<
65
+ Reply.ReplyEncoded<any>,
66
+ ClusterError.EntityNotAssignedToRunner
67
+ >((resume_) => {
68
+ resume = resume_
69
+ const parent = Option.getOrThrow(Fiber.getCurrentFiber())
70
+ const runtime = Runtime.make({
71
+ context: parent.currentContext,
72
+ runtimeFlags: Runtime.defaultRuntimeFlags,
73
+ fiberRefs: parent.getFiberRefs()
74
+ })
75
+ const onExit = (
76
+ exit: Exit.Exit<
77
+ any,
78
+ ClusterError.EntityNotAssignedToRunner
79
+ >
80
+ ) => {
81
+ if (exit._tag === "Failure") {
82
+ resume(exit as any)
83
+ }
84
+ }
85
+ const fiber = Runtime.runFork(runtime)(storage.registerReplyHandler(message))
86
+ fiber.addObserver(onExit)
87
+ Runtime.runFork(runtime)(Effect.catchTag(
88
+ sharding.notify(message, constWaitUntilRead),
89
+ "AlreadyProcessingMessage",
90
+ () => Effect.void
91
+ )).addObserver(onExit)
92
+ return Fiber.interrupt(fiber)
93
+ })
94
+ }
60
95
  return Effect.zipRight(
61
- persisted ?
62
- Effect.zipRight(
63
- storage.registerReplyHandler(
64
- message,
65
- Effect.sync(() =>
66
- resume(Effect.fail(
67
- new ClusterError.EntityNotAssignedToRunner({
68
- address: request.address
69
- })
70
- ))
71
- )
72
- ),
73
- sharding.notify(message)
74
- ) :
75
- sharding.send(message),
96
+ sharding.send(message),
76
97
  Effect.async<Reply.ReplyEncoded<any>, ClusterError.EntityNotAssignedToRunner>((resume_) => {
77
98
  if (replyEncoded) {
78
- resume_(Effect.succeed(replyEncoded))
99
+ resume_(replyEncoded)
79
100
  } else {
80
101
  resume = resume_
81
102
  }
@@ -99,17 +120,12 @@ export const layerHandlers = Runners.Rpcs.toLayer(Effect.gen(function*() {
99
120
  return Effect.as(
100
121
  persisted ?
101
122
  Effect.zipRight(
102
- storage.registerReplyHandler(
103
- message,
104
- Effect.suspend(() =>
105
- mailbox.fail(
106
- new ClusterError.EntityNotAssignedToRunner({
107
- address: request.address
108
- })
109
- )
110
- )
123
+ storage.registerReplyHandler(message).pipe(
124
+ Effect.onError((cause) => mailbox.failCause(cause)),
125
+ Effect.forkScoped,
126
+ Effect.interruptible
111
127
  ),
112
- sharding.notify(message)
128
+ sharding.notify(message, constWaitUntilRead)
113
129
  ) :
114
130
  sharding.send(message),
115
131
  mailbox
@@ -120,6 +136,8 @@ export const layerHandlers = Runners.Rpcs.toLayer(Effect.gen(function*() {
120
136
  }
121
137
  }))
122
138
 
139
+ const constWaitUntilRead = { waitUntilRead: true } as const
140
+
123
141
  /**
124
142
  * The `RunnerServer` recieves messages from other Runners and forwards them to the
125
143
  * `Sharding` layer.
@@ -151,18 +169,17 @@ export const layerWithClients: Layer.Layer<
151
169
  | ShardingConfig
152
170
  | Runners.RpcClientProtocol
153
171
  | MessageStorage.MessageStorage
154
- | ShardStorage.ShardStorage
172
+ | RunnerStorage.RunnerStorage
173
+ | RunnerHealth.RunnerHealth
155
174
  > = layer.pipe(
156
175
  Layer.provideMerge(Sharding.layer),
157
- Layer.provideMerge(Runners.layerRpc),
158
- Layer.provideMerge(SynchronizedClock.layer),
159
- Layer.provide(ShardManager.layerClientRpc)
176
+ Layer.provideMerge(Runners.layerRpc)
160
177
  )
161
178
 
162
179
  /**
163
180
  * A `Runners` layer that is client only.
164
181
  *
165
- * It will not register with the ShardManager and recieve shard assignments,
182
+ * It will not register with RunnerStorage and recieve shard assignments,
166
183
  * so this layer can be used to embed a cluster client inside another effect
167
184
  * application.
168
185
  *
@@ -175,10 +192,10 @@ export const layerClientOnly: Layer.Layer<
175
192
  | ShardingConfig
176
193
  | Runners.RpcClientProtocol
177
194
  | MessageStorage.MessageStorage
195
+ | RunnerStorage.RunnerStorage
178
196
  > = Sharding.layer.pipe(
179
197
  Layer.provideMerge(Runners.layerRpc),
180
- Layer.provide(ShardManager.layerClientRpc),
181
- Layer.provide(ShardStorage.layerNoop),
198
+ Layer.provide(RunnerHealth.layerNoop),
182
199
  Layer.updateService(ShardingConfig, (config) => ({
183
200
  ...config,
184
201
  runnerAddress: Option.none()
@@ -0,0 +1,218 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import { isNonEmptyArray, type NonEmptyArray } from "effect/Array"
5
+ import * as Context from "effect/Context"
6
+ import * as Effect from "effect/Effect"
7
+ import * as Layer from "effect/Layer"
8
+ import * as MutableHashMap from "effect/MutableHashMap"
9
+ import type { PersistenceError } from "./ClusterError.js"
10
+ import * as MachineId from "./MachineId.js"
11
+ import { Runner } from "./Runner.js"
12
+ import type { RunnerAddress } from "./RunnerAddress.js"
13
+ import { ShardId } from "./ShardId.js"
14
+
15
+ /**
16
+ * Represents a generic interface to the persistent storage required by the
17
+ * cluster.
18
+ *
19
+ * @since 1.0.0
20
+ * @category models
21
+ */
22
+ export class RunnerStorage extends Context.Tag("@effect/cluster/RunnerStorage")<RunnerStorage, {
23
+ /**
24
+ * Register a new runner with the cluster.
25
+ */
26
+ readonly register: (runner: Runner, healthy: boolean) => Effect.Effect<MachineId.MachineId, PersistenceError>
27
+
28
+ /**
29
+ * Unregister the runner with the given address.
30
+ */
31
+ readonly unregister: (address: RunnerAddress) => Effect.Effect<void, PersistenceError>
32
+
33
+ /**
34
+ * Get all runners registered with the cluster.
35
+ */
36
+ readonly getRunners: Effect.Effect<Array<readonly [runner: Runner, healthy: boolean]>, PersistenceError>
37
+
38
+ /**
39
+ * Set the health status of the given runner.
40
+ */
41
+ readonly setRunnerHealth: (address: RunnerAddress, healthy: boolean) => Effect.Effect<void, PersistenceError>
42
+
43
+ /**
44
+ * Try to acquire the given shard ids for processing.
45
+ *
46
+ * It returns an array of shards it was able to acquire.
47
+ */
48
+ readonly acquire: (
49
+ address: RunnerAddress,
50
+ shardIds: Iterable<ShardId>
51
+ ) => Effect.Effect<Array<ShardId>, PersistenceError>
52
+
53
+ /**
54
+ * Refresh the locks owned by the given runner.
55
+ */
56
+ readonly refresh: (
57
+ address: RunnerAddress,
58
+ shardIds: Iterable<ShardId>
59
+ ) => Effect.Effect<Array<ShardId>, PersistenceError>
60
+
61
+ /**
62
+ * Release the given shard ids.
63
+ */
64
+ readonly release: (
65
+ address: RunnerAddress,
66
+ shardId: ShardId
67
+ ) => Effect.Effect<void, PersistenceError>
68
+
69
+ /**
70
+ * Release all the shards assigned to the given runner.
71
+ */
72
+ readonly releaseAll: (address: RunnerAddress) => Effect.Effect<void, PersistenceError>
73
+ }>() {}
74
+
75
+ /**
76
+ * @since 1.0.0
77
+ * @category Encoded
78
+ */
79
+ export interface Encoded {
80
+ /**
81
+ * Get all runners registered with the cluster.
82
+ */
83
+ readonly getRunners: Effect.Effect<Array<readonly [runner: string, healthy: boolean]>, PersistenceError>
84
+
85
+ /**
86
+ * Register a new runner with the cluster.
87
+ */
88
+ readonly register: (address: string, runner: string, healthy: boolean) => Effect.Effect<number, PersistenceError>
89
+
90
+ /**
91
+ * Unregister the runner with the given address.
92
+ */
93
+ readonly unregister: (address: string) => Effect.Effect<void, PersistenceError>
94
+
95
+ /**
96
+ * Set the health status of the given runner.
97
+ */
98
+ readonly setRunnerHealth: (address: string, healthy: boolean) => Effect.Effect<void, PersistenceError>
99
+
100
+ /**
101
+ * Acquire the lock on the given shards, returning the shards that were
102
+ * successfully locked.
103
+ */
104
+ readonly acquire: (
105
+ address: string,
106
+ shardIds: NonEmptyArray<string>
107
+ ) => Effect.Effect<Array<string>, PersistenceError>
108
+
109
+ /**
110
+ * Refresh the lock on the given shards, returning the shards that were
111
+ * successfully locked.
112
+ */
113
+ readonly refresh: (
114
+ address: string,
115
+ shardIds: Array<string>
116
+ ) => Effect.Effect<ReadonlyArray<string>, PersistenceError>
117
+
118
+ /**
119
+ * Release the lock on the given shard.
120
+ */
121
+ readonly release: (
122
+ address: string,
123
+ shardId: string
124
+ ) => Effect.Effect<void, PersistenceError>
125
+
126
+ /**
127
+ * Release the lock on all shards for the given runner.
128
+ */
129
+ readonly releaseAll: (address: string) => Effect.Effect<void, PersistenceError>
130
+ }
131
+
132
+ /**
133
+ * @since 1.0.0
134
+ * @category layers
135
+ */
136
+ export const makeEncoded = (encoded: Encoded) =>
137
+ RunnerStorage.of({
138
+ getRunners: Effect.gen(function*() {
139
+ const runners = yield* encoded.getRunners
140
+ const results: Array<[Runner, boolean]> = []
141
+ for (let i = 0; i < runners.length; i++) {
142
+ const [runner, healthy] = runners[i]
143
+ try {
144
+ results.push([Runner.decodeSync(runner), healthy])
145
+ } catch {
146
+ //
147
+ }
148
+ }
149
+ return results
150
+ }),
151
+ register: (runner, healthy) =>
152
+ Effect.map(
153
+ encoded.register(encodeRunnerAddress(runner.address), Runner.encodeSync(runner), healthy),
154
+ MachineId.make
155
+ ),
156
+ unregister: (address) => encoded.unregister(encodeRunnerAddress(address)),
157
+ setRunnerHealth: (address, healthy) => encoded.setRunnerHealth(encodeRunnerAddress(address), healthy),
158
+ acquire: (address, shardIds) => {
159
+ const arr = Array.from(shardIds, (id) => id.toString())
160
+ if (!isNonEmptyArray(arr)) return Effect.succeed([])
161
+ return encoded.acquire(encodeRunnerAddress(address), arr).pipe(
162
+ Effect.map((shards) => shards.map(ShardId.fromString))
163
+ )
164
+ },
165
+ refresh: (address, shardIds) =>
166
+ encoded.refresh(encodeRunnerAddress(address), Array.from(shardIds, (id) => id.toString())).pipe(
167
+ Effect.map((shards) => shards.map(ShardId.fromString))
168
+ ),
169
+ release(address, shardId) {
170
+ return encoded.release(encodeRunnerAddress(address), shardId.toString())
171
+ },
172
+ releaseAll(address) {
173
+ return encoded.releaseAll(encodeRunnerAddress(address))
174
+ }
175
+ })
176
+
177
+ /**
178
+ * @since 1.0.0
179
+ * @category constructors
180
+ */
181
+ export const makeMemory = Effect.gen(function*() {
182
+ const runners = MutableHashMap.empty<RunnerAddress, Runner>()
183
+ let acquired: Array<ShardId> = []
184
+ let id = 0
185
+
186
+ return RunnerStorage.of({
187
+ getRunners: Effect.sync(() => Array.from(MutableHashMap.values(runners), (runner) => [runner, true])),
188
+ register: (runner) =>
189
+ Effect.sync(() => {
190
+ MutableHashMap.set(runners, runner.address, runner)
191
+ return MachineId.make(id++)
192
+ }),
193
+ unregister: (address) =>
194
+ Effect.sync(() => {
195
+ MutableHashMap.remove(runners, address)
196
+ }),
197
+ setRunnerHealth: () => Effect.void,
198
+ acquire: (_address, shardIds) => {
199
+ acquired = Array.from(shardIds)
200
+ return Effect.succeed(Array.from(shardIds))
201
+ },
202
+ refresh: () => Effect.sync(() => acquired),
203
+ release: () => Effect.void,
204
+ releaseAll: () => Effect.void
205
+ })
206
+ })
207
+
208
+ /**
209
+ * @since 1.0.0
210
+ * @category layers
211
+ */
212
+ export const layerMemory: Layer.Layer<RunnerStorage> = Layer.effect(RunnerStorage)(makeMemory)
213
+
214
+ // -------------------------------------------------------------------------------------
215
+ // internal
216
+ // -------------------------------------------------------------------------------------
217
+
218
+ const encodeRunnerAddress = (runnerAddress: RunnerAddress) => `${runnerAddress.host}:${runnerAddress.port}`