@effect/cluster 0.36.3 → 0.37.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.
- package/ClusterWorkflowEngine/package.json +6 -0
- package/dist/cjs/ClusterSchema.js +9 -1
- package/dist/cjs/ClusterSchema.js.map +1 -1
- package/dist/cjs/ClusterWorkflowEngine.js +386 -0
- package/dist/cjs/ClusterWorkflowEngine.js.map +1 -0
- package/dist/cjs/Envelope.js +14 -4
- package/dist/cjs/Envelope.js.map +1 -1
- package/dist/cjs/Message.js +22 -2
- package/dist/cjs/Message.js.map +1 -1
- package/dist/cjs/MessageStorage.js +59 -21
- package/dist/cjs/MessageStorage.js.map +1 -1
- package/dist/cjs/Reply.js +15 -0
- package/dist/cjs/Reply.js.map +1 -1
- package/dist/cjs/Runners.js +2 -2
- package/dist/cjs/Runners.js.map +1 -1
- package/dist/cjs/Sharding.js +23 -8
- package/dist/cjs/Sharding.js.map +1 -1
- package/dist/cjs/Snowflake.js +2 -2
- package/dist/cjs/Snowflake.js.map +1 -1
- package/dist/cjs/SqlMessageStorage.js +36 -12
- package/dist/cjs/SqlMessageStorage.js.map +1 -1
- package/dist/cjs/SqlShardStorage.js +6 -6
- package/dist/cjs/SqlShardStorage.js.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/internal/entityManager.js +4 -4
- package/dist/cjs/internal/entityManager.js.map +1 -1
- package/dist/dts/ClusterSchema.d.ts +7 -0
- package/dist/dts/ClusterSchema.d.ts.map +1 -1
- package/dist/dts/ClusterWorkflowEngine.d.ts +48 -0
- package/dist/dts/ClusterWorkflowEngine.d.ts.map +1 -0
- package/dist/dts/Envelope.d.ts +9 -0
- package/dist/dts/Envelope.d.ts.map +1 -1
- package/dist/dts/Message.d.ts +11 -1
- package/dist/dts/Message.d.ts.map +1 -1
- package/dist/dts/MessageStorage.d.ts +56 -0
- package/dist/dts/MessageStorage.d.ts.map +1 -1
- package/dist/dts/Reply.d.ts +7 -0
- package/dist/dts/Reply.d.ts.map +1 -1
- package/dist/dts/ShardStorage.d.ts +1 -1
- package/dist/dts/Sharding.d.ts +9 -0
- package/dist/dts/Sharding.d.ts.map +1 -1
- package/dist/dts/SqlMessageStorage.d.ts +10 -1
- package/dist/dts/SqlMessageStorage.d.ts.map +1 -1
- package/dist/dts/SqlShardStorage.d.ts +1 -1
- package/dist/dts/index.d.ts +4 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/internal/resourceMap.d.ts +1 -1
- package/dist/dts/internal/resourceMap.d.ts.map +1 -1
- package/dist/dts/internal/resourceRef.d.ts +1 -1
- package/dist/dts/internal/resourceRef.d.ts.map +1 -1
- package/dist/esm/ClusterSchema.js +7 -0
- package/dist/esm/ClusterSchema.js.map +1 -1
- package/dist/esm/ClusterWorkflowEngine.js +378 -0
- package/dist/esm/ClusterWorkflowEngine.js.map +1 -0
- package/dist/esm/Envelope.js +12 -3
- package/dist/esm/Envelope.js.map +1 -1
- package/dist/esm/Message.js +20 -1
- package/dist/esm/Message.js.map +1 -1
- package/dist/esm/MessageStorage.js +59 -21
- package/dist/esm/MessageStorage.js.map +1 -1
- package/dist/esm/Reply.js +15 -0
- package/dist/esm/Reply.js.map +1 -1
- package/dist/esm/Runners.js +2 -2
- package/dist/esm/Runners.js.map +1 -1
- package/dist/esm/Sharding.js +24 -9
- package/dist/esm/Sharding.js.map +1 -1
- package/dist/esm/Snowflake.js +2 -2
- package/dist/esm/Snowflake.js.map +1 -1
- package/dist/esm/SqlMessageStorage.js +36 -12
- package/dist/esm/SqlMessageStorage.js.map +1 -1
- package/dist/esm/SqlShardStorage.js +6 -6
- package/dist/esm/SqlShardStorage.js.map +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal/entityManager.js +5 -5
- package/dist/esm/internal/entityManager.js.map +1 -1
- package/package.json +14 -5
- package/src/ClusterSchema.ts +10 -0
- package/src/ClusterWorkflowEngine.ts +475 -0
- package/src/Envelope.ts +17 -3
- package/src/Message.ts +24 -2
- package/src/MessageStorage.ts +122 -22
- package/src/Reply.ts +18 -0
- package/src/Runners.ts +2 -2
- package/src/Sharding.ts +45 -9
- package/src/Snowflake.ts +2 -2
- package/src/SqlMessageStorage.ts +74 -16
- package/src/SqlShardStorage.ts +6 -6
- package/src/index.ts +5 -0
- package/src/internal/entityManager.ts +6 -4
package/src/Message.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
/**
|
2
2
|
* @since 1.0.0
|
3
3
|
*/
|
4
|
-
import
|
4
|
+
import * as Rpc from "@effect/rpc/Rpc"
|
5
5
|
import type { Context } from "effect/Context"
|
6
6
|
import * as Data from "effect/Data"
|
7
7
|
import * as Effect from "effect/Effect"
|
@@ -10,8 +10,10 @@ import * as Option from "effect/Option"
|
|
10
10
|
import * as Schema from "effect/Schema"
|
11
11
|
import type { PersistenceError } from "./ClusterError.js"
|
12
12
|
import { MalformedMessage } from "./ClusterError.js"
|
13
|
+
import type { EntityAddress } from "./EntityAddress.js"
|
13
14
|
import * as Envelope from "./Envelope.js"
|
14
15
|
import type * as Reply from "./Reply.js"
|
16
|
+
import type { Snowflake } from "./Snowflake.js"
|
15
17
|
|
16
18
|
/**
|
17
19
|
* @since 1.0.0
|
@@ -99,7 +101,27 @@ export class OutgoingRequest<R extends Rpc.Any> extends Data.TaggedClass("Outgoi
|
|
99
101
|
export class OutgoingEnvelope extends Data.TaggedClass("OutgoingEnvelope")<{
|
100
102
|
readonly envelope: Envelope.AckChunk | Envelope.Interrupt
|
101
103
|
readonly rpc: Rpc.AnyWithProps
|
102
|
-
}> {
|
104
|
+
}> {
|
105
|
+
/**
|
106
|
+
* @since 1.0.0
|
107
|
+
*/
|
108
|
+
static interrupt(options: {
|
109
|
+
readonly address: EntityAddress
|
110
|
+
readonly id: Snowflake
|
111
|
+
readonly requestId: Snowflake
|
112
|
+
}): OutgoingEnvelope {
|
113
|
+
return new OutgoingEnvelope({
|
114
|
+
envelope: new Envelope.Interrupt(options),
|
115
|
+
rpc: neverRpc
|
116
|
+
})
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
120
|
+
const neverRpc = Rpc.make("Never", {
|
121
|
+
success: Schema.Never as any,
|
122
|
+
error: Schema.Never,
|
123
|
+
payload: {}
|
124
|
+
})
|
103
125
|
|
104
126
|
/**
|
105
127
|
* @since 1.0.0
|
package/src/MessageStorage.ts
CHANGED
@@ -52,6 +52,11 @@ export class MessageStorage extends Context.Tag("@effect/cluster/MessageStorage"
|
|
52
52
|
reply: Reply.ReplyWithContext<R>
|
53
53
|
) => Effect.Effect<void, PersistenceError | MalformedMessage>
|
54
54
|
|
55
|
+
/**
|
56
|
+
* Clear the `Reply`s for the given request id.
|
57
|
+
*/
|
58
|
+
readonly clearReplies: (requestId: Snowflake.Snowflake) => Effect.Effect<void, PersistenceError>
|
59
|
+
|
55
60
|
/**
|
56
61
|
* Retrieves the replies for the specified requests.
|
57
62
|
*
|
@@ -62,6 +67,24 @@ export class MessageStorage extends Context.Tag("@effect/cluster/MessageStorage"
|
|
62
67
|
requests: Iterable<Message.OutgoingRequest<R>>
|
63
68
|
) => Effect.Effect<Array<Reply.Reply<R>>, PersistenceError | MalformedMessage>
|
64
69
|
|
70
|
+
/**
|
71
|
+
* Retrieves the encoded replies for the specified request ids.
|
72
|
+
*/
|
73
|
+
readonly repliesForUnfiltered: <R extends Rpc.Any>(
|
74
|
+
requestIds: Iterable<Snowflake.Snowflake>
|
75
|
+
) => Effect.Effect<Array<Reply.ReplyEncoded<R>>, PersistenceError | MalformedMessage>
|
76
|
+
|
77
|
+
/**
|
78
|
+
* Retrieves the request id for the specified primary key.
|
79
|
+
*/
|
80
|
+
readonly requestIdForPrimaryKey: (
|
81
|
+
options: {
|
82
|
+
readonly address: EntityAddress
|
83
|
+
readonly tag: string
|
84
|
+
readonly id: string
|
85
|
+
}
|
86
|
+
) => Effect.Effect<Option.Option<Snowflake.Snowflake>, PersistenceError>
|
87
|
+
|
65
88
|
/**
|
66
89
|
* For locally sent messages, register a handler to process the replies.
|
67
90
|
*/
|
@@ -103,6 +126,13 @@ export class MessageStorage extends Context.Tag("@effect/cluster/MessageStorage"
|
|
103
126
|
readonly resetAddress: (
|
104
127
|
address: EntityAddress
|
105
128
|
) => Effect.Effect<void, PersistenceError>
|
129
|
+
|
130
|
+
/**
|
131
|
+
* Clear all messages and replies for the provided address.
|
132
|
+
*/
|
133
|
+
readonly clearAddress: (
|
134
|
+
address: EntityAddress
|
135
|
+
) => Effect.Effect<void, PersistenceError>
|
106
136
|
}>() {}
|
107
137
|
|
108
138
|
/**
|
@@ -194,6 +224,18 @@ export type Encoded = {
|
|
194
224
|
reply: Reply.ReplyEncoded<any>
|
195
225
|
) => Effect.Effect<void, PersistenceError>
|
196
226
|
|
227
|
+
/**
|
228
|
+
* Remove the replies for the specified request.
|
229
|
+
*/
|
230
|
+
readonly clearReplies: (requestId: Snowflake.Snowflake) => Effect.Effect<void, PersistenceError>
|
231
|
+
|
232
|
+
/**
|
233
|
+
* Retrieves the request id for the specified primary key.
|
234
|
+
*/
|
235
|
+
readonly requestIdForPrimaryKey: (
|
236
|
+
primaryKey: string
|
237
|
+
) => Effect.Effect<Option.Option<Snowflake.Snowflake>, PersistenceError>
|
238
|
+
|
197
239
|
/**
|
198
240
|
* Retrieves the replies for the specified requests.
|
199
241
|
*
|
@@ -205,6 +247,14 @@ export type Encoded = {
|
|
205
247
|
PersistenceError
|
206
248
|
>
|
207
249
|
|
250
|
+
/**
|
251
|
+
* Retrieves the replies for the specified request ids.
|
252
|
+
*/
|
253
|
+
readonly repliesForUnfiltered: (requestIds: Array<string>) => Effect.Effect<
|
254
|
+
Array<Reply.ReplyEncoded<any>>,
|
255
|
+
PersistenceError
|
256
|
+
>
|
257
|
+
|
208
258
|
/**
|
209
259
|
* Retrieves the unprocessed messages for the given shards.
|
210
260
|
*
|
@@ -247,6 +297,13 @@ export type Encoded = {
|
|
247
297
|
address: EntityAddress
|
248
298
|
) => Effect.Effect<void, PersistenceError>
|
249
299
|
|
300
|
+
/**
|
301
|
+
* Clear all messages and replies for the provided address.
|
302
|
+
*/
|
303
|
+
readonly clearAddress: (
|
304
|
+
address: EntityAddress
|
305
|
+
) => Effect.Effect<void, PersistenceError>
|
306
|
+
|
250
307
|
/**
|
251
308
|
* Reset the mailbox state for the provided shards.
|
252
309
|
*/
|
@@ -362,6 +419,7 @@ export const makeEncoded: (encoded: Encoded) => Effect.Effect<
|
|
362
419
|
Effect.asVoid
|
363
420
|
),
|
364
421
|
saveReply: (reply) => Effect.flatMap(Reply.serialize(reply), encoded.saveReply),
|
422
|
+
clearReplies: encoded.clearReplies,
|
365
423
|
repliesFor: Effect.fnUntraced(function*(messages) {
|
366
424
|
const requestIds = Arr.empty<string>()
|
367
425
|
const map = new Map<string, Message.OutgoingRequest<any>>()
|
@@ -374,6 +432,11 @@ export const makeEncoded: (encoded: Encoded) => Effect.Effect<
|
|
374
432
|
const encodedReplies = yield* encoded.repliesFor(requestIds)
|
375
433
|
return yield* decodeReplies(map, encodedReplies)
|
376
434
|
}),
|
435
|
+
repliesForUnfiltered: (ids) => encoded.repliesForUnfiltered(Array.from(ids, String)),
|
436
|
+
requestIdForPrimaryKey(options) {
|
437
|
+
const primaryKey = Envelope.primaryKeyByAddress(options)
|
438
|
+
return encoded.requestIdForPrimaryKey(primaryKey)
|
439
|
+
},
|
377
440
|
unprocessedMessages: (shardIds) => {
|
378
441
|
const shards = Array.from(shardIds)
|
379
442
|
if (shards.length === 0) return Effect.succeed([])
|
@@ -390,7 +453,8 @@ export const makeEncoded: (encoded: Encoded) => Effect.Effect<
|
|
390
453
|
decodeMessages
|
391
454
|
)
|
392
455
|
},
|
393
|
-
resetAddress:
|
456
|
+
resetAddress: encoded.resetAddress,
|
457
|
+
clearAddress: encoded.clearAddress,
|
394
458
|
resetShards: (shardIds) => encoded.resetShards(Array.from(shardIds))
|
395
459
|
})
|
396
460
|
|
@@ -506,10 +570,14 @@ export const noop: MessageStorage["Type"] = globalValue(
|
|
506
570
|
saveRequest: () => Effect.succeed(SaveResult.Success()),
|
507
571
|
saveEnvelope: () => Effect.void,
|
508
572
|
saveReply: () => Effect.void,
|
573
|
+
clearReplies: () => Effect.void,
|
509
574
|
repliesFor: () => Effect.succeed([]),
|
575
|
+
repliesForUnfiltered: () => Effect.succeed([]),
|
576
|
+
requestIdForPrimaryKey: () => Effect.succeedNone,
|
510
577
|
unprocessedMessages: () => Effect.succeed([]),
|
511
578
|
unprocessedMessagesById: () => Effect.succeed([]),
|
512
579
|
resetAddress: () => Effect.void,
|
580
|
+
clearAddress: () => Effect.void,
|
513
581
|
resetShards: () => Effect.void
|
514
582
|
}))
|
515
583
|
)
|
@@ -567,9 +635,31 @@ export class MemoryDriver extends Effect.Service<MemoryDriver>()("@effect/cluste
|
|
567
635
|
|
568
636
|
const replyLatch = yield* Effect.makeLatch()
|
569
637
|
|
638
|
+
function repliesFor(requestIds: Array<string>) {
|
639
|
+
const replies = Arr.empty<Reply.ReplyEncoded<any>>()
|
640
|
+
for (const requestId of requestIds) {
|
641
|
+
const request = requests.get(requestId)
|
642
|
+
if (!request) continue
|
643
|
+
else if (Option.isNone(request.lastReceivedChunk)) {
|
644
|
+
// eslint-disable-next-line no-restricted-syntax
|
645
|
+
replies.push(...request.replies)
|
646
|
+
continue
|
647
|
+
}
|
648
|
+
const sequence = request.lastReceivedChunk.value.sequence
|
649
|
+
for (const reply of request.replies) {
|
650
|
+
if (reply._tag === "Chunk" && reply.sequence <= sequence) {
|
651
|
+
continue
|
652
|
+
}
|
653
|
+
replies.push(reply)
|
654
|
+
}
|
655
|
+
}
|
656
|
+
return replies
|
657
|
+
}
|
658
|
+
|
570
659
|
const encoded: Encoded = {
|
571
|
-
saveEnvelope: ({ envelope, primaryKey }) =>
|
660
|
+
saveEnvelope: ({ envelope: envelope_, primaryKey }) =>
|
572
661
|
Effect.sync(() => {
|
662
|
+
const envelope = JSON.parse(JSON.stringify(envelope_)) as Envelope.Envelope.Encoded
|
573
663
|
const existing = primaryKey
|
574
664
|
? requestsByPrimaryKey.get(primaryKey)
|
575
665
|
: envelope._tag === "Request" && requests.get(envelope.requestId)
|
@@ -598,8 +688,9 @@ export class MemoryDriver extends Effect.Service<MemoryDriver>()("@effect/cluste
|
|
598
688
|
journal.push(envelope)
|
599
689
|
return SaveResultEncoded.Success()
|
600
690
|
}),
|
601
|
-
saveReply: (
|
691
|
+
saveReply: (reply_) =>
|
602
692
|
Effect.sync(() => {
|
693
|
+
const reply = JSON.parse(JSON.stringify(reply_)) as Reply.ReplyEncoded<any>
|
603
694
|
const entry = requests.get(reply.requestId)
|
604
695
|
if (!entry || replyIds.has(reply.id)) return
|
605
696
|
if (reply._tag === "WithExit") {
|
@@ -609,27 +700,22 @@ export class MemoryDriver extends Effect.Service<MemoryDriver>()("@effect/cluste
|
|
609
700
|
replyIds.add(reply.id)
|
610
701
|
replyLatch.unsafeOpen()
|
611
702
|
}),
|
612
|
-
|
703
|
+
clearReplies: (id) =>
|
613
704
|
Effect.sync(() => {
|
614
|
-
const
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
// eslint-disable-next-line no-restricted-syntax
|
620
|
-
replies.push(...request.replies)
|
621
|
-
continue
|
622
|
-
}
|
623
|
-
const sequence = request.lastReceivedChunk.value.sequence
|
624
|
-
for (const reply of request.replies) {
|
625
|
-
if (reply._tag === "Chunk" && reply.sequence <= sequence) {
|
626
|
-
continue
|
627
|
-
}
|
628
|
-
replies.push(reply)
|
629
|
-
}
|
630
|
-
}
|
631
|
-
return replies
|
705
|
+
const entry = requests.get(String(id))
|
706
|
+
if (!entry) return
|
707
|
+
entry.replies = []
|
708
|
+
entry.lastReceivedChunk = Option.none()
|
709
|
+
unprocessed.add(entry.envelope)
|
632
710
|
}),
|
711
|
+
requestIdForPrimaryKey: (primaryKey) =>
|
712
|
+
Effect.sync(() => {
|
713
|
+
const entry = requestsByPrimaryKey.get(primaryKey)
|
714
|
+
return Option.fromNullable(entry?.envelope.requestId).pipe(Option.map(Snowflake.Snowflake))
|
715
|
+
}),
|
716
|
+
repliesFor: (requestIds) => Effect.sync(() => repliesFor(requestIds)),
|
717
|
+
repliesForUnfiltered: (requestIds) =>
|
718
|
+
Effect.sync(() => requestIds.flatMap((id) => requests.get(String(id))?.replies ?? [])),
|
633
719
|
unprocessedMessages: (shardIds) =>
|
634
720
|
Effect.sync(() => {
|
635
721
|
if (unprocessed.size === 0) return []
|
@@ -667,6 +753,20 @@ export class MemoryDriver extends Effect.Service<MemoryDriver>()("@effect/cluste
|
|
667
753
|
return unprocessedWith((envelope) => envelopeIds.has(envelope.requestId))
|
668
754
|
}),
|
669
755
|
resetAddress: () => Effect.void,
|
756
|
+
clearAddress: (address) =>
|
757
|
+
Effect.sync(() => {
|
758
|
+
for (let i = journal.length - 1; i >= 0; i--) {
|
759
|
+
const envelope = journal[i]
|
760
|
+
const sameAddress = address.entityType === envelope.address.entityType &&
|
761
|
+
address.entityId === envelope.address.entityId
|
762
|
+
if (!sameAddress || envelope._tag !== "Request") {
|
763
|
+
continue
|
764
|
+
}
|
765
|
+
unprocessed.delete(envelope)
|
766
|
+
requests.delete(envelope.requestId)
|
767
|
+
journal.splice(i, 1)
|
768
|
+
}
|
769
|
+
}),
|
670
770
|
resetShards: () => Effect.void
|
671
771
|
}
|
672
772
|
|
package/src/Reply.ts
CHANGED
@@ -8,6 +8,7 @@ import * as Context from "effect/Context"
|
|
8
8
|
import * as Data from "effect/Data"
|
9
9
|
import * as Effect from "effect/Effect"
|
10
10
|
import * as Exit from "effect/Exit"
|
11
|
+
import * as FiberId from "effect/FiberId"
|
11
12
|
import * as FiberRef from "effect/FiberRef"
|
12
13
|
import { identity } from "effect/Function"
|
13
14
|
import type * as Option from "effect/Option"
|
@@ -68,6 +69,23 @@ export class ReplyWithContext<R extends Rpc.Any> extends Data.TaggedClass("Reply
|
|
68
69
|
rpc: neverRpc
|
69
70
|
})
|
70
71
|
}
|
72
|
+
/**
|
73
|
+
* @since 1.0.0
|
74
|
+
*/
|
75
|
+
static interrupt(options: {
|
76
|
+
readonly id: Snowflake
|
77
|
+
readonly requestId: Snowflake
|
78
|
+
}): ReplyWithContext<any> {
|
79
|
+
return new ReplyWithContext({
|
80
|
+
reply: new WithExit({
|
81
|
+
requestId: options.requestId,
|
82
|
+
id: options.id,
|
83
|
+
exit: Exit.interrupt(FiberId.none)
|
84
|
+
}),
|
85
|
+
context: Context.empty() as any,
|
86
|
+
rpc: neverRpc
|
87
|
+
})
|
88
|
+
}
|
71
89
|
}
|
72
90
|
|
73
91
|
const neverRpc = Rpc.make("Never", {
|
package/src/Runners.ts
CHANGED
@@ -312,7 +312,7 @@ export const make: (options: Omit<Runners["Type"], "sendLocal" | "notifyLocal">)
|
|
312
312
|
notify(options_) {
|
313
313
|
const { discard, message } = options_
|
314
314
|
return notifyWith(message, (message, duplicate) => {
|
315
|
-
if (message._tag === "OutgoingEnvelope") {
|
315
|
+
if (discard || message._tag === "OutgoingEnvelope") {
|
316
316
|
return options.notify(options_)
|
317
317
|
} else if (!duplicate && options_.address._tag === "Some") {
|
318
318
|
return Effect.catchAll(
|
@@ -328,7 +328,7 @@ export const make: (options: Omit<Runners["Type"], "sendLocal" | "notifyLocal">)
|
|
328
328
|
}
|
329
329
|
)
|
330
330
|
}
|
331
|
-
return
|
331
|
+
return options.notify(options_).pipe(
|
332
332
|
Effect.andThen(replyFromStorage(message))
|
333
333
|
)
|
334
334
|
})
|
package/src/Sharding.ts
CHANGED
@@ -35,7 +35,7 @@ import {
|
|
35
35
|
EntityNotManagedByRunner,
|
36
36
|
RunnerUnavailable
|
37
37
|
} from "./ClusterError.js"
|
38
|
-
import { Persisted } from "./ClusterSchema.js"
|
38
|
+
import { Persisted, Uninterruptible } from "./ClusterSchema.js"
|
39
39
|
import type { CurrentAddress, CurrentRunnerAddress, Entity, HandlersFrom } from "./Entity.js"
|
40
40
|
import { EntityAddress } from "./EntityAddress.js"
|
41
41
|
import { EntityId } from "./EntityId.js"
|
@@ -126,6 +126,17 @@ export class Sharding extends Context.Tag("@effect/cluster/Sharding")<Sharding,
|
|
126
126
|
EntityNotManagedByRunner | EntityNotAssignedToRunner | MailboxFull | AlreadyProcessingMessage
|
127
127
|
>
|
128
128
|
|
129
|
+
/**
|
130
|
+
* Sends an outgoing message
|
131
|
+
*/
|
132
|
+
readonly sendOutgoing: (
|
133
|
+
message: Message.Outgoing<any>,
|
134
|
+
discard: boolean
|
135
|
+
) => Effect.Effect<
|
136
|
+
void,
|
137
|
+
EntityNotManagedByRunner | MailboxFull | AlreadyProcessingMessage | PersistenceError
|
138
|
+
>
|
139
|
+
|
129
140
|
/**
|
130
141
|
* Notify sharding that a message has been persisted to storage.
|
131
142
|
*/
|
@@ -134,6 +145,11 @@ export class Sharding extends Context.Tag("@effect/cluster/Sharding")<Sharding,
|
|
134
145
|
EntityNotManagedByRunner | EntityNotAssignedToRunner | AlreadyProcessingMessage
|
135
146
|
>
|
136
147
|
|
148
|
+
/**
|
149
|
+
* Reset the state of a message
|
150
|
+
*/
|
151
|
+
readonly reset: (requestId: Snowflake.Snowflake) => Effect.Effect<boolean>
|
152
|
+
|
137
153
|
/**
|
138
154
|
* Retrieves the active entity count for the current runner.
|
139
155
|
*/
|
@@ -247,7 +263,7 @@ const make = Effect.gen(function*() {
|
|
247
263
|
Effect.forkIn(shardingScope)
|
248
264
|
)
|
249
265
|
|
250
|
-
// refresh the shard locks every
|
266
|
+
// refresh the shard locks every 4s
|
251
267
|
yield* Effect.suspend(() =>
|
252
268
|
shardStorage.refresh(selfAddress, [
|
253
269
|
...acquiredShards,
|
@@ -270,14 +286,14 @@ const make = Effect.gen(function*() {
|
|
270
286
|
}),
|
271
287
|
Effect.retry({
|
272
288
|
times: 5,
|
273
|
-
schedule: Schedule.spaced(
|
289
|
+
schedule: Schedule.spaced(50)
|
274
290
|
}),
|
275
291
|
Effect.catchAllCause((cause) =>
|
276
292
|
Effect.logError("Could not refresh shard locks", cause).pipe(
|
277
293
|
Effect.andThen(clearSelfShards)
|
278
294
|
)
|
279
295
|
),
|
280
|
-
Effect.delay("
|
296
|
+
Effect.delay("4 seconds"),
|
281
297
|
Effect.forever,
|
282
298
|
Effect.interruptible,
|
283
299
|
Effect.forkIn(shardingScope)
|
@@ -384,6 +400,11 @@ const make = Effect.gen(function*() {
|
|
384
400
|
|
385
401
|
let storageAlreadyProcessed = (_message: Message.IncomingRequest<any>) => true
|
386
402
|
|
403
|
+
// keep track of the last sent request ids to avoid duplicates
|
404
|
+
// we only keep the last 30 sets to avoid memory leaks
|
405
|
+
const sentRequestIds = new Set<Snowflake.Snowflake>()
|
406
|
+
const sentRequestIdSets = new Set<Set<Snowflake.Snowflake>>()
|
407
|
+
|
387
408
|
if (storageEnabled && Option.isSome(config.runnerAddress)) {
|
388
409
|
const selfAddress = config.runnerAddress.value
|
389
410
|
|
@@ -391,10 +412,8 @@ const make = Effect.gen(function*() {
|
|
391
412
|
yield* Effect.logDebug("Starting")
|
392
413
|
yield* Effect.addFinalizer(() => Effect.logDebug("Shutting down"))
|
393
414
|
|
394
|
-
|
395
|
-
|
396
|
-
const sentRequestIds = new Set<Snowflake.Snowflake>()
|
397
|
-
const sentRequestIdSets = new Set<Set<Snowflake.Snowflake>>()
|
415
|
+
sentRequestIds.clear()
|
416
|
+
sentRequestIdSets.clear()
|
398
417
|
|
399
418
|
storageAlreadyProcessed = (message: Message.IncomingRequest<any>) => {
|
400
419
|
if (!sentRequestIds.has(message.envelope.requestId)) {
|
@@ -755,6 +774,17 @@ const make = Effect.gen(function*() {
|
|
755
774
|
)
|
756
775
|
}
|
757
776
|
|
777
|
+
const reset: Sharding["Type"]["reset"] = Effect.fnUntraced(
|
778
|
+
function*(requestId) {
|
779
|
+
yield* storage.clearReplies(requestId)
|
780
|
+
sentRequestIds.delete(requestId)
|
781
|
+
},
|
782
|
+
Effect.matchCause({
|
783
|
+
onSuccess: () => true,
|
784
|
+
onFailure: () => false
|
785
|
+
})
|
786
|
+
)
|
787
|
+
|
758
788
|
// --- Shard Manager sync ---
|
759
789
|
|
760
790
|
const shardManagerTimeoutFiber = yield* FiberHandle.make().pipe(
|
@@ -903,6 +933,7 @@ const make = Effect.gen(function*() {
|
|
903
933
|
never
|
904
934
|
> = yield* ResourceMap.make(Effect.fnUntraced(function*(entity: Entity<any>) {
|
905
935
|
const client = yield* RpcClient.makeNoSerialization(entity.protocol, {
|
936
|
+
spanPrefix: `${entity.type}.client`,
|
906
937
|
supportsAck: true,
|
907
938
|
generateRequestId: () => RequestId(snowflakeGen.unsafeNext()),
|
908
939
|
onFromClient(options): Effect.Effect<
|
@@ -968,6 +999,9 @@ const make = Effect.gen(function*() {
|
|
968
999
|
const entry = clientRequests.get(requestId)!
|
969
1000
|
if (!entry) return Effect.void
|
970
1001
|
clientRequests.delete(requestId)
|
1002
|
+
if (Context.get(entry.rpc.annotations, Uninterruptible)) {
|
1003
|
+
return Effect.void
|
1004
|
+
}
|
971
1005
|
// for durable messages, we ignore interrupts on shutdown or as a
|
972
1006
|
// result of a shard being resassigned
|
973
1007
|
const isTransientInterrupt = MutableRef.get(isShutdown) ||
|
@@ -1148,8 +1182,10 @@ const make = Effect.gen(function*() {
|
|
1148
1182
|
registerSingleton,
|
1149
1183
|
makeClient,
|
1150
1184
|
send: sendLocal,
|
1185
|
+
sendOutgoing: (message, discard) => sendOutgoing(message, discard),
|
1151
1186
|
notify: (message) => notifyLocal(message, false),
|
1152
|
-
activeEntityCount
|
1187
|
+
activeEntityCount,
|
1188
|
+
reset
|
1153
1189
|
})
|
1154
1190
|
|
1155
1191
|
return sharding
|
package/src/Snowflake.ts
CHANGED
@@ -145,7 +145,7 @@ export const makeGenerator: Effect.Effect<Snowflake.Generator> = Effect.gen(func
|
|
145
145
|
const clock = yield* Effect.clock
|
146
146
|
|
147
147
|
let sequence = 0
|
148
|
-
let sequenceAt = clock.unsafeCurrentTimeMillis()
|
148
|
+
let sequenceAt = Math.floor(clock.unsafeCurrentTimeMillis())
|
149
149
|
|
150
150
|
return identity<Snowflake.Generator>({
|
151
151
|
setMachineId: (newMachineId) =>
|
@@ -153,7 +153,7 @@ export const makeGenerator: Effect.Effect<Snowflake.Generator> = Effect.gen(func
|
|
153
153
|
machineId = newMachineId
|
154
154
|
}),
|
155
155
|
unsafeNext() {
|
156
|
-
let now = clock.unsafeCurrentTimeMillis()
|
156
|
+
let now = Math.floor(clock.unsafeCurrentTimeMillis())
|
157
157
|
|
158
158
|
// account for clock drift, only allow time to move forward
|
159
159
|
if (now < sequenceAt) {
|
package/src/SqlMessageStorage.ts
CHANGED
@@ -663,6 +663,28 @@ export const make = Effect.fnUntraced(function*(options?: {
|
|
663
663
|
PersistenceError.refail
|
664
664
|
),
|
665
665
|
|
666
|
+
clearReplies: Effect.fnUntraced(
|
667
|
+
function*(requestId) {
|
668
|
+
yield* sql`DELETE FROM ${repliesTableSql} WHERE request_id = ${String(requestId)}`
|
669
|
+
yield* sql`UPDATE ${messagesTableSql} SET processed = ${sqlFalse}, last_reply_id = NULL, last_read = NULL WHERE request_id = ${
|
670
|
+
String(requestId)
|
671
|
+
}`
|
672
|
+
},
|
673
|
+
sql.withTransaction,
|
674
|
+
PersistenceError.refail
|
675
|
+
),
|
676
|
+
|
677
|
+
requestIdForPrimaryKey: (primaryKey) =>
|
678
|
+
sql<{ id: string | bigint }>`SELECT id FROM ${messagesTableSql} WHERE message_id = ${primaryKey}`.pipe(
|
679
|
+
Effect.map((rows) =>
|
680
|
+
Option.fromNullable(rows[0]?.id).pipe(
|
681
|
+
Option.map(Snowflake.Snowflake)
|
682
|
+
)
|
683
|
+
),
|
684
|
+
Effect.provideService(SqlClient.SafeIntegers, true),
|
685
|
+
PersistenceError.refail
|
686
|
+
),
|
687
|
+
|
666
688
|
repliesFor: (requestIds) =>
|
667
689
|
// replies where:
|
668
690
|
// - the request is in the list
|
@@ -682,22 +704,20 @@ export const make = Effect.fnUntraced(function*(options?: {
|
|
682
704
|
ORDER BY rowid ASC
|
683
705
|
`.unprepared.pipe(
|
684
706
|
Effect.provideService(SqlClient.SafeIntegers, true),
|
685
|
-
Effect.map(Arr.map(
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
}
|
700
|
-
)),
|
707
|
+
Effect.map(Arr.map(replyFromRow)),
|
708
|
+
PersistenceError.refail,
|
709
|
+
withTracerDisabled
|
710
|
+
),
|
711
|
+
|
712
|
+
repliesForUnfiltered: (requestIds) =>
|
713
|
+
sql<ReplyRow>`
|
714
|
+
SELECT id, kind, request_id, payload, sequence
|
715
|
+
FROM ${repliesTableSql}
|
716
|
+
WHERE request_id IN (${sql.literal(requestIds.join(","))})
|
717
|
+
ORDER BY rowid ASC
|
718
|
+
`.unprepared.pipe(
|
719
|
+
Effect.provideService(SqlClient.SafeIntegers, true),
|
720
|
+
Effect.map(Arr.map(replyFromRow)),
|
701
721
|
PersistenceError.refail,
|
702
722
|
withTracerDisabled
|
703
723
|
),
|
@@ -761,6 +781,28 @@ export const make = Effect.fnUntraced(function*(options?: {
|
|
761
781
|
withTracerDisabled
|
762
782
|
),
|
763
783
|
|
784
|
+
clearAddress: (address) =>
|
785
|
+
sql`
|
786
|
+
DELETE FROM ${repliesTableSql}
|
787
|
+
WHERE request_id IN (
|
788
|
+
SELECT id FROM ${messagesTableSql}
|
789
|
+
WHERE entity_type = ${address.entityType}
|
790
|
+
AND entity_id = ${address.entityId}
|
791
|
+
)
|
792
|
+
`.pipe(
|
793
|
+
Effect.andThen(
|
794
|
+
sql`
|
795
|
+
DELETE FROM ${messagesTableSql}
|
796
|
+
WHERE entity_type = ${address.entityType}
|
797
|
+
AND entity_id = ${address.entityId}
|
798
|
+
`
|
799
|
+
),
|
800
|
+
sql.withTransaction,
|
801
|
+
Effect.asVoid,
|
802
|
+
PersistenceError.refail,
|
803
|
+
withTracerDisabled
|
804
|
+
),
|
805
|
+
|
764
806
|
resetShards: (shardIds) =>
|
765
807
|
sql`
|
766
808
|
UPDATE ${messagesTableSql}
|
@@ -814,6 +856,22 @@ const replyKind = {
|
|
814
856
|
"Chunk": null
|
815
857
|
} as const satisfies Record<Reply.Reply<any>["_tag"], number | null>
|
816
858
|
|
859
|
+
const replyFromRow = (row: ReplyRow): Reply.ReplyEncoded<any> =>
|
860
|
+
Number(row.kind) === replyKind.WithExit ?
|
861
|
+
{
|
862
|
+
_tag: "WithExit",
|
863
|
+
id: String(row.id),
|
864
|
+
requestId: String(row.request_id),
|
865
|
+
exit: JSON.parse(row.payload)
|
866
|
+
} :
|
867
|
+
{
|
868
|
+
_tag: "Chunk",
|
869
|
+
id: String(row.id),
|
870
|
+
requestId: String(row.request_id),
|
871
|
+
values: JSON.parse(row.payload),
|
872
|
+
sequence: Number(row.sequence!)
|
873
|
+
}
|
874
|
+
|
817
875
|
type MessageRow = {
|
818
876
|
readonly id: string | bigint
|
819
877
|
readonly message_id: string | null
|
package/src/SqlShardStorage.ts
CHANGED
@@ -143,10 +143,10 @@ 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 '
|
147
|
-
mysql: () => sql`DATE_SUB(${sqlNow}, INTERVAL
|
148
|
-
mssql: () => sql`DATEADD(SECOND, -
|
149
|
-
orElse: () => sql`datetime(${sqlNow}, '-
|
146
|
+
pg: () => sql`${sqlNow} - INTERVAL '5 seconds'`,
|
147
|
+
mysql: () => sql`DATE_SUB(${sqlNow}, INTERVAL 5 SECOND)`,
|
148
|
+
mssql: () => sql`DATEADD(SECOND, -5, ${sqlNow})`,
|
149
|
+
orElse: () => sql`datetime(${sqlNow}, '-5 seconds')`
|
150
150
|
})
|
151
151
|
|
152
152
|
const acquireLock = sql.onDialectOrElse({
|
@@ -170,7 +170,7 @@ export const make = Effect.fnUntraced(function*(options?: {
|
|
170
170
|
MERGE ${locksTableSql} WITH (HOLDLOCK) AS target
|
171
171
|
USING (SELECT * FROM (VALUES ${sql.csv(values)})) AS source (shard_id, address, acquired_at)
|
172
172
|
ON target.shard_id = source.shard_id
|
173
|
-
WHEN MATCHED AND (target.address = source.address OR DATEDIFF(SECOND, target.acquired_at, ${sqlNow}) >
|
173
|
+
WHEN MATCHED AND (target.address = source.address OR DATEDIFF(SECOND, target.acquired_at, ${sqlNow}) > 5) THEN
|
174
174
|
UPDATE SET address = source.address, acquired_at = source.acquired_at
|
175
175
|
WHEN NOT MATCHED THEN
|
176
176
|
INSERT (shard_id, address, acquired_at)
|
@@ -187,7 +187,7 @@ export const make = Effect.fnUntraced(function*(options?: {
|
|
187
187
|
SELECT 1 FROM ${locksTableSql}
|
188
188
|
WHERE shard_id = source.shard_id
|
189
189
|
AND address != ${address}
|
190
|
-
AND (strftime('%s', ${sqlNow}) - strftime('%s', acquired_at)) <=
|
190
|
+
AND (strftime('%s', ${sqlNow}) - strftime('%s', acquired_at)) <= 5
|
191
191
|
)
|
192
192
|
ON CONFLICT(shard_id) DO UPDATE
|
193
193
|
SET address = ${address}, acquired_at = ${sqlNow}
|
package/src/index.ts
CHANGED
@@ -13,6 +13,11 @@ export * as ClusterMetrics from "./ClusterMetrics.js"
|
|
13
13
|
*/
|
14
14
|
export * as ClusterSchema from "./ClusterSchema.js"
|
15
15
|
|
16
|
+
/**
|
17
|
+
* @since 1.0.0
|
18
|
+
*/
|
19
|
+
export * as ClusterWorkflowEngine from "./ClusterWorkflowEngine.js"
|
20
|
+
|
16
21
|
/**
|
17
22
|
* @since 1.0.0
|
18
23
|
*/
|