Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-cluster-reply-defect-isolation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Scope cluster reply serialization failures and peer-delivered defects to their own request instead of the whole runner connection
6 changes: 4 additions & 2 deletions packages/effect/src/unstable/cluster/HttpRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ export const toHttpEffect: Effect.Effect<
const handlers = yield* Layer.build(RunnerServer.layerHandlers)
return yield* RpcServer.toHttpEffect(Runners.Rpcs, {
spanPrefix: "RunnerServer",
disableTracing: true
disableTracing: true,
disableFatalDefects: true

@marbemac marbemac Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Least sure about this one — the other two changes fix the issue without it, this just stops other handler defect classes from failing every in-flight request on the shared connection. Happy to drop it if connection-fatal defects are intentional on this channel (same with the other 2 flips of disableFatalDefects to true).

}).pipe(Effect.provideContext(handlers))
})

Expand All @@ -173,7 +174,8 @@ export const toHttpEffectWebsocket: Effect.Effect<
const handlers = yield* Layer.build(RunnerServer.layerHandlers)
return yield* RpcServer.toHttpEffectWebsocket(Runners.Rpcs, {
spanPrefix: "RunnerServer",
disableTracing: true
disableTracing: true,
disableFatalDefects: true
}).pipe(Effect.provideContext(handlers))
})

Expand Down
49 changes: 41 additions & 8 deletions packages/effect/src/unstable/cluster/RunnerServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
*
* @since 4.0.0
*/
import type * as Cause from "../../Cause.ts"
import * as Effect from "../../Effect.ts"
import type * as Exit from "../../Exit.ts"
import * as Fiber from "../../Fiber.ts"
import { constant } from "../../Function.ts"
import * as Layer from "../../Layer.ts"
import * as Option from "../../Option.ts"
import * as Queue from "../../Queue.ts"
import type * as Rpc from "../rpc/Rpc.ts"
import * as RpcServer from "../rpc/RpcServer.ts"
import type * as ClusterError from "./ClusterError.ts"
import * as Message from "./Message.ts"
Expand All @@ -30,6 +32,25 @@ import { ShardingConfig } from "./ShardingConfig.ts"

const constVoid = constant(Effect.void)

const serializeDefectReply = <R extends Rpc.Any>(
reply: Reply.ReplyWithContext<R>,
defect: unknown
): Effect.Effect<Reply.Encoded> =>
Effect.orDie(Reply.serialize(Reply.ReplyWithContext.fromDefect({
id: reply.reply.id,
requestId: reply.reply.requestId,
defect
})))

const serializeReply = <R extends Rpc.Any>(
reply: Reply.ReplyWithContext<R>
): Effect.Effect<Reply.Encoded> =>
Effect.catchTag(
Reply.serialize(reply),
"MalformedMessage",
(error) => serializeDefectReply(reply, error)
)

/**
* Layer that handles runner protocol RPCs by forwarding requests to `Sharding`
* and `MessageStorage`.
Expand Down Expand Up @@ -63,7 +84,7 @@ export const layerHandlers = Runners.Rpcs.toLayer(Effect.gen(function*() {
envelope: request,
lastSentReply: Option.none(),
respond(reply) {
resume(Effect.orDie(Reply.serialize(reply)))
resume(serializeReply(reply))
return Effect.void
}
})
Expand Down Expand Up @@ -108,23 +129,34 @@ export const layerHandlers = Runners.Rpcs.toLayer(Effect.gen(function*() {
},
Stream: ({ persisted, request }) =>
Effect.flatMap(
Queue.make<Reply.Encoded, ClusterError.EntityNotAssignedToRunner>(),
Queue.make<Reply.Encoded, ClusterError.EntityNotAssignedToRunner | Cause.Done>(),
(queue) => {
const message = new Message.IncomingRequest({
envelope: request,
lastSentReply: Option.none(),
respond(reply) {
return Effect.flatMap(Reply.serialize(reply), (reply) => {
Queue.offerUnsafe(queue, reply)
return Effect.void
})
return Reply.serialize(reply).pipe(
Effect.flatMap((reply) => {
Queue.offerUnsafe(queue, reply)
return Effect.void
}),
Effect.catchTag("MalformedMessage", (error) =>
Effect.flatMap(serializeDefectReply(reply, error), (reply) => {
// the fallback defect reply is terminal, so end the stream
Queue.offerUnsafe(queue, reply)
Queue.endUnsafe(queue)
return Effect.void
}))
)
}
})
return Effect.as(
persisted ?
Effect.andThen(
storage.registerReplyHandler(message).pipe(
Effect.onError((cause) => Queue.failCause(queue, cause)),
Effect.onError((cause) =>
Queue.failCause(queue, cause)
),
Effect.forkScoped
),
sharding.notify(message, constWaitUntilRead)
Expand Down Expand Up @@ -168,7 +200,8 @@ export const layer: Layer.Layer<
RpcServer.Protocol | Sharding.Sharding | MessageStorage.MessageStorage
> = RpcServer.layer(Runners.Rpcs, {
spanPrefix: "RunnerServer",
disableTracing: true
disableTracing: true,
disableFatalDefects: true
Comment thread
marbemac marked this conversation as resolved.
}).pipe(Layer.provide(layerHandlers))

/**
Expand Down
23 changes: 17 additions & 6 deletions packages/effect/src/unstable/cluster/Runners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,11 +574,22 @@ export const makeRpc: Effect.Effect<
persisted: isPersisted
})
),
Effect.catchTag("RpcClientError", Effect.die),
Effect.scoped,
Effect.catchDefect(() => Effect.fail(new RunnerUnavailable({ address })))
Effect.catchTag("RpcClientError", () => Effect.fail(new RunnerUnavailable({ address })))
)
}
// Persisted requests can recover their reply from storage via the
// `RunnerUnavailable` path, volatile requests receive the defect as their reply.
const respondDefect = (defect: unknown) =>
isPersisted
? Effect.fail(new RunnerUnavailable({ address }))
: message.respond(
new Reply.WithExit({
id: snowflakeGen.nextUnsafe(),
requestId: message.envelope.requestId,
exit: Exit.die(defect)
})
)
const isStream = RpcSchema.isStreamSchema(rpc.successSchema)
if (!isStream) {
return Effect.matchEffect(Message.serializeRequest(message), {
Expand All @@ -590,7 +601,6 @@ export const makeRpc: Effect.Effect<
persisted: isPersisted
})
),
Effect.catchTag("RpcClientError", Effect.die),
Effect.flatMap((reply) =>
Schema.decodeEffect(Reply.Reply(message.rpc))(reply).pipe(
Effect.provideContext(message.context),
Expand All @@ -599,7 +609,8 @@ export const makeRpc: Effect.Effect<
),
Effect.flatMap(message.respond),
Effect.scoped,
Effect.catchDefect(() => Effect.fail(new RunnerUnavailable({ address })))
Effect.catchTag("RpcClientError", () => Effect.fail(new RunnerUnavailable({ address }))),
Effect.catchDefect(respondDefect)
),
onFailure: (error) =>
message.respond(
Expand All @@ -626,10 +637,10 @@ export const makeRpc: Effect.Effect<
Effect.flatMap((reply) => Effect.orDie(decode(reply))),
Effect.flatMap(message.respond),
Effect.forever,
Effect.catchTag("RpcClientError", Effect.die),
Effect.provideContext(message.context),
Effect.catchTag("Done", (_) => Effect.void),
Effect.catchDefect(() => Effect.fail(new RunnerUnavailable({ address })))
Effect.catchTag("RpcClientError", () => Effect.fail(new RunnerUnavailable({ address }))),
Effect.catchDefect(respondDefect)
)
}),
Effect.scoped
Expand Down
Loading
Loading