@effect/cluster 0.52.11 → 0.53.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.
@@ -15,6 +15,7 @@ import { identity } from "effect/Function"
15
15
  import * as HashMap from "effect/HashMap"
16
16
  import * as Metric from "effect/Metric"
17
17
  import * as Option from "effect/Option"
18
+ import * as ParseResult from "effect/ParseResult"
18
19
  import * as Runtime from "effect/Runtime"
19
20
  import * as Schedule from "effect/Schedule"
20
21
  import * as Schema from "effect/Schema"
@@ -23,10 +24,10 @@ import { AlreadyProcessingMessage, EntityNotAssignedToRunner, MailboxFull, Malfo
23
24
  import * as ClusterMetrics from "../ClusterMetrics.js"
24
25
  import { Persisted, Uninterruptible } from "../ClusterSchema.js"
25
26
  import type { Entity, HandlersFrom } from "../Entity.js"
26
- import { CurrentAddress, CurrentRunnerAddress, Request } from "../Entity.js"
27
+ import { CurrentAddress, CurrentRunnerAddress, KeepAliveLatch, KeepAliveRpc, Request } from "../Entity.js"
27
28
  import type { EntityAddress } from "../EntityAddress.js"
28
29
  import type { EntityId } from "../EntityId.js"
29
- import * as Envelope from "../Envelope.js"
30
+ import type * as Envelope from "../Envelope.js"
30
31
  import * as Message from "../Message.js"
31
32
  import * as MessageStorage from "../MessageStorage.js"
32
33
  import * as Reply from "../Reply.js"
@@ -65,6 +66,7 @@ export interface EntityManager {
65
66
  /** @internal */
66
67
  export type EntityState = {
67
68
  readonly address: EntityAddress
69
+ readonly scope: Scope.Scope
68
70
  readonly activeRequests: Map<bigint, {
69
71
  readonly rpc: Rpc.AnyWithProps
70
72
  readonly message: Message.IncomingRequestLocal<any>
@@ -74,6 +76,8 @@ export type EntityState = {
74
76
  }>
75
77
  lastActiveCheck: number
76
78
  write: RpcServer.RpcServer<any>["write"]
79
+ readonly keepAliveLatch: Effect.Latch
80
+ keepAliveEnabled: boolean
77
81
  }
78
82
 
79
83
  /** @internal */
@@ -107,6 +111,10 @@ export const make = Effect.fnUntraced(function*<
107
111
  const retryDriver = yield* Schedule.driver(
108
112
  options.defectRetryPolicy ? Schedule.andThen(options.defectRetryPolicy, defaultRetryPolicy) : defaultRetryPolicy
109
113
  )
114
+ const entityRpcs = new Map(entity.protocol.requests)
115
+
116
+ // add internal rpcs
117
+ entityRpcs.set(KeepAliveRpc._tag, KeepAliveRpc as any)
110
118
 
111
119
  const activeServers = new Map<EntityId, EntityState>()
112
120
  const serverCloseLatches = new Map<EntityAddress, Effect.Latch>()
@@ -122,7 +130,8 @@ export const make = Effect.fnUntraced(function*<
122
130
  }
123
131
 
124
132
  const scope = yield* Effect.scope
125
- const endLatch = yield* Effect.makeLatch()
133
+ const endLatch = Effect.unsafeMakeLatch()
134
+ const keepAliveLatch = Effect.unsafeMakeLatch()
126
135
 
127
136
  // on shutdown, reset the storage for the entity
128
137
  yield* Scope.addFinalizerExit(
@@ -149,6 +158,7 @@ export const make = Effect.fnUntraced(function*<
149
158
  Effect.provide(context.pipe(
150
159
  Context.add(CurrentAddress, address),
151
160
  Context.add(CurrentRunnerAddress, options.runnerAddress),
161
+ Context.add(KeepAliveLatch, keepAliveLatch),
152
162
  Context.add(Scope.Scope, scope)
153
163
  )),
154
164
  Effect.locally(FiberRef.currentLogAnnotations, HashMap.empty())
@@ -306,6 +316,7 @@ export const make = Effect.fnUntraced(function*<
306
316
  }
307
317
 
308
318
  const state: EntityState = {
319
+ scope,
309
320
  address,
310
321
  write(clientId, message) {
311
322
  if (writeRef.state.current._tag !== "Acquired") {
@@ -314,7 +325,9 @@ export const make = Effect.fnUntraced(function*<
314
325
  return writeRef.state.current.value(clientId, message)
315
326
  },
316
327
  activeRequests,
317
- lastActiveCheck: clock.unsafeCurrentTimeMillis()
328
+ lastActiveCheck: clock.unsafeCurrentTimeMillis(),
329
+ keepAliveLatch,
330
+ keepAliveEnabled: false
318
331
  }
319
332
 
320
333
  // During shutdown, signal that no more messages will be processed
@@ -380,13 +393,43 @@ export const make = Effect.fnUntraced(function*<
380
393
  )
381
394
  }
382
395
 
383
- const rpc = entity.protocol.requests.get(message.envelope.tag)! as any as Rpc.AnyWithProps
396
+ const rpc = entityRpcs.get(message.envelope.tag)! as any as Rpc.AnyWithProps
384
397
  if (!storageEnabled && Context.get(rpc.annotations, Persisted)) {
385
398
  return Effect.dieMessage(
386
399
  "EntityManager.sendLocal: Cannot process a persisted message without MessageStorage"
387
400
  )
388
401
  }
389
402
 
403
+ // Cluster internal RPCs
404
+
405
+ // keep-alive RPC
406
+ if (rpc._tag === KeepAliveRpc._tag) {
407
+ const msg = message as unknown as Message.IncomingRequestLocal<typeof KeepAliveRpc>
408
+ const reply = Effect.suspend(() =>
409
+ Effect.orDie(retryRespond(
410
+ 4,
411
+ msg.respond(
412
+ new Reply.WithExit<typeof KeepAliveRpc>({
413
+ requestId: message.envelope.requestId,
414
+ id: snowflakeGen.unsafeNext(),
415
+ exit: Exit.void
416
+ })
417
+ )
418
+ ))
419
+ )
420
+
421
+ if (server.keepAliveEnabled) return reply
422
+ server.keepAliveEnabled = true
423
+ server.keepAliveLatch.unsafeClose()
424
+ return server.keepAliveLatch.whenOpen(Effect.suspend(() => {
425
+ server.keepAliveEnabled = false
426
+ return reply
427
+ })).pipe(
428
+ Effect.forkIn(server.scope),
429
+ Effect.asVoid
430
+ )
431
+ }
432
+
390
433
  if (mailboxCapacity !== "unbounded" && server.activeRequests.size >= mailboxCapacity) {
391
434
  return Effect.fail(new MailboxFull({ address: message.envelope.address }))
392
435
  }
@@ -437,7 +480,7 @@ export const make = Effect.fnUntraced(function*<
437
480
  )
438
481
  }
439
482
 
440
- const decodeMessage = Schema.decode(makeMessageSchema(entity))
483
+ const decodeMessage = makeMessageDecode(entity, entityRpcs)
441
484
 
442
485
  const runFork = Runtime.runFork(
443
486
  yield* Effect.runtime<never>().pipe(
@@ -533,49 +576,65 @@ const defaultRetryPolicy = Schedule.exponential(500, 1.5).pipe(
533
576
  Schedule.union(Schedule.spaced("10 seconds"))
534
577
  )
535
578
 
536
- const makeMessageSchema = <Type extends string, Rpcs extends Rpc.Any>(entity: Entity<Type, Rpcs>): Schema.Schema<
537
- {
538
- readonly _tag: "IncomingRequest"
539
- readonly envelope: Envelope.Request.Any
540
- readonly lastSentReply: Option.Option<Reply.Reply<Rpcs>>
541
- } | {
542
- readonly _tag: "IncomingEnvelope"
543
- readonly envelope: Envelope.AckChunk | Envelope.Interrupt
544
- },
545
- Message.Incoming<Rpcs>,
546
- Rpc.Context<Rpcs>
547
- > => {
548
- const requests = Arr.empty<Schema.Schema.Any>()
549
-
550
- for (const rpc of entity.protocol.requests.values()) {
551
- requests.push(
552
- Schema.TaggedStruct("IncomingRequest", {
553
- envelope: Schema.transform(
554
- Schema.Struct({
555
- ...Envelope.PartialEncodedRequestFromSelf.fields,
556
- tag: Schema.Literal(rpc._tag),
557
- payload: (rpc as any as Rpc.AnyWithProps).payloadSchema
558
- }),
559
- Envelope.RequestFromSelf,
560
- {
561
- decode: (encoded) => Envelope.makeRequest(encoded),
562
- encode: identity
563
- }
564
- ),
565
- lastSentReply: Schema.OptionFromSelf(Reply.Reply(rpc))
566
- })
567
- )
579
+ const makeMessageDecode = <Type extends string, Rpcs extends Rpc.Any>(
580
+ entity: Entity<Type, Rpcs>,
581
+ entityRpcs: Map<string, Rpcs>
582
+ ) => {
583
+ const decodeRequest = (
584
+ message: Message.IncomingRequest<Rpcs>,
585
+ rpc: Rpc.AnyWithProps
586
+ ) => {
587
+ const payload = Schema.decode(rpc.payloadSchema)(message.envelope.payload)
588
+ const lastSentReply = Option.isSome(message.lastSentReply)
589
+ ? Effect.asSome(Schema.decode(Reply.Reply(rpc as any))(message.lastSentReply.value))
590
+ : Effect.succeedNone
591
+ return Effect.flatMap(payload, (payload) =>
592
+ Effect.map(lastSentReply, (lastSentReply) => ({
593
+ _tag: "IncomingRequest" as const,
594
+ envelope: {
595
+ ...message.envelope,
596
+ payload
597
+ } as Envelope.Request.Any,
598
+ lastSentReply
599
+ })))
568
600
  }
569
601
 
570
- return Schema.Union(
571
- ...requests,
572
- Schema.TaggedStruct("IncomingEnvelope", {
573
- envelope: Schema.Union(
574
- Schema.typeSchema(Envelope.AckChunk),
575
- Schema.typeSchema(Envelope.Interrupt)
602
+ return (message: Message.Incoming<Rpcs>): Effect.Effect<
603
+ {
604
+ readonly _tag: "IncomingRequest"
605
+ readonly envelope: Envelope.Request.Any
606
+ readonly lastSentReply: Option.Option<Reply.Reply<Rpcs>>
607
+ } | {
608
+ readonly _tag: "IncomingEnvelope"
609
+ readonly envelope: Envelope.AckChunk | Envelope.Interrupt
610
+ },
611
+ ParseResult.ParseError,
612
+ Rpc.Context<Rpcs>
613
+ > => {
614
+ if (message._tag === "IncomingEnvelope") {
615
+ return Effect.succeed(message)
616
+ }
617
+ const rpc = entityRpcs.get(message.envelope.tag) as any as Rpc.AnyWithProps
618
+ if (!rpc) {
619
+ return Effect.fail(
620
+ new ParseResult.ParseError({
621
+ issue: new ParseResult.Unexpected(
622
+ message,
623
+ `Unknown tag ${message.envelope.tag} for entity type ${entity.type}`
624
+ )
625
+ })
576
626
  )
577
- })
578
- ) as any
627
+ }
628
+ return decodeRequest(message, rpc) as Effect.Effect<
629
+ {
630
+ readonly _tag: "IncomingRequest"
631
+ readonly envelope: Envelope.Request.Any
632
+ readonly lastSentReply: Option.Option<Reply.Reply<Rpcs>>
633
+ },
634
+ ParseResult.ParseError,
635
+ Rpc.Context<Rpcs>
636
+ >
637
+ }
579
638
  }
580
639
 
581
640
  const retryRespond = <A, E, R>(times: number, effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>