mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-21 03:01:03 +02:00
Fail pending request responses on stop
This commit is contained in:
parent
0fb943c0ef
commit
1218e827d4
5 changed files with 95 additions and 12 deletions
|
|
@ -12,8 +12,8 @@ Verified source roots:
|
||||||
- Effect v4 subtree: `/home/elpresidank/YeeBois/projects/beep-effect2/.repos/effect-v4`
|
- Effect v4 subtree: `/home/elpresidank/YeeBois/projects/beep-effect2/.repos/effect-v4`
|
||||||
- Installed Effect beta used by this workspace: `ts/node_modules/effect`
|
- Installed Effect beta used by this workspace: `ts/node_modules/effect`
|
||||||
|
|
||||||
Current signal counts from `ts/packages` after the 2026-06-02 consumer
|
Current signal counts from `ts/packages` after the 2026-06-02
|
||||||
concurrency ownership slice:
|
request-response stop signal slice:
|
||||||
|
|
||||||
| Signal | Count |
|
| Signal | Count |
|
||||||
| --- | ---: |
|
| --- | ---: |
|
||||||
|
|
@ -21,8 +21,8 @@ concurrency ownership slice:
|
||||||
| `Effect.runPromiseWith` | 0 |
|
| `Effect.runPromiseWith` | 0 |
|
||||||
| `Effect.cached` | 0 |
|
| `Effect.cached` | 0 |
|
||||||
| `Layer.succeed` | 12 |
|
| `Layer.succeed` | 12 |
|
||||||
| `Map<` | 82 |
|
| `Map<` | 88 |
|
||||||
| `WebSocket` | 64 |
|
| `WebSocket` | 74 |
|
||||||
| `new Map` | 60 |
|
| `new Map` | 60 |
|
||||||
| `toPromiseRequestor` | 0 |
|
| `toPromiseRequestor` | 0 |
|
||||||
| `makeAsyncProcessor` | 19 |
|
| `makeAsyncProcessor` | 19 |
|
||||||
|
|
@ -121,6 +121,10 @@ Notes:
|
||||||
of sharing a single `BackendConsumer.receive()` handle. `stop` is now
|
of sharing a single `BackendConsumer.receive()` handle. `stop` is now
|
||||||
idempotent through `Ref`, so explicit stop and scoped finalizers do not close
|
idempotent through `Ref`, so explicit stop and scoped finalizers do not close
|
||||||
workers twice.
|
workers twice.
|
||||||
|
- The request-response stop signal slice added a `Deferred` shutdown signal to
|
||||||
|
`makeEffectRequestResponseFromPubSub`. Pending requests now race response
|
||||||
|
waiting against runtime stop and fail promptly with a tagged
|
||||||
|
`MessagingLifecycleError` instead of waiting for timeout.
|
||||||
- The gateway streaming callback slice added Effect-returning dispatcher
|
- The gateway streaming callback slice added Effect-returning dispatcher
|
||||||
streaming methods, switched the RPC stream server off nested
|
streaming methods, switched the RPC stream server off nested
|
||||||
`Effect.runPromiseWith(context)` queue offers, and replaced the client
|
`Effect.runPromiseWith(context)` queue offers, and replaced the client
|
||||||
|
|
@ -1263,6 +1267,32 @@ Notes:
|
||||||
- `cd ts && bun run build`
|
- `cd ts && bun run build`
|
||||||
- `cd ts && bun run test`
|
- `cd ts && bun run test`
|
||||||
|
|
||||||
|
### 2026-06-02: Request-Response Stop Signal Slice
|
||||||
|
|
||||||
|
- Status: migrated and root-verified.
|
||||||
|
- Completed:
|
||||||
|
- `makeEffectRequestResponseFromPubSub` now owns a `Deferred` stop signal for
|
||||||
|
the lifetime of the scoped request-response runtime.
|
||||||
|
- `request()` races response waiting against that stop signal before applying
|
||||||
|
the request timeout, so pending calls fail promptly when the runtime stops.
|
||||||
|
- `stop()` fails the stop signal with a tagged `MessagingLifecycleError`
|
||||||
|
before interrupting the dispatch loop and closing the producer/consumer
|
||||||
|
resources.
|
||||||
|
- Flow PDF decoder and graph embeddings service error unions now include
|
||||||
|
`MessagingLifecycleError` because requestor failures can surface shutdown.
|
||||||
|
- Added Effect-native runtime coverage proving a pending request fails with
|
||||||
|
the tagged lifecycle error when the request-response runtime stops.
|
||||||
|
- Verification:
|
||||||
|
- `cd ts && bun run check:tsgo`
|
||||||
|
- `cd ts/packages/base && bunx --bun vitest run src/__tests__/messaging-runtime.test.ts`
|
||||||
|
- `bun run --cwd ts/packages/base build`
|
||||||
|
- `bun run --cwd ts/packages/base test`
|
||||||
|
- `bun run --cwd ts/packages/flow build`
|
||||||
|
- `bun run --cwd ts/packages/flow test`
|
||||||
|
- `cd ts && bun run check`
|
||||||
|
- `cd ts && bun run build`
|
||||||
|
- `cd ts && bun run test`
|
||||||
|
|
||||||
## Subagent Findings To Preserve
|
## Subagent Findings To Preserve
|
||||||
|
|
||||||
- MCP/workbench:
|
- MCP/workbench:
|
||||||
|
|
@ -1299,9 +1329,10 @@ Notes:
|
||||||
construction and stream/consumer state ownership.
|
construction and stream/consumer state ownership.
|
||||||
- Consumer rate-limit retry timeout behavior is now wired in both legacy and
|
- Consumer rate-limit retry timeout behavior is now wired in both legacy and
|
||||||
Effect-native consumer paths. Effect-native consumer concurrency now owns
|
Effect-native consumer paths. Effect-native consumer concurrency now owns
|
||||||
one backend consumer per worker. Remaining consumer runtime work should
|
one backend consumer per worker, and request-response pending shutdown now
|
||||||
focus on request/response pending shutdown semantics and the legacy
|
fails through a tagged lifecycle error. Remaining consumer runtime work
|
||||||
consumer facade's blocking compatibility shape.
|
should focus on the legacy consumer facade's blocking compatibility shape
|
||||||
|
and scoped backend/layer construction.
|
||||||
- Existing constructor shims preserve callable-plus-newable public exports;
|
- Existing constructor shims preserve callable-plus-newable public exports;
|
||||||
removing them needs a public API split or real class redesign.
|
removing them needs a public API split or real class redesign.
|
||||||
- Typed string registries in `Flow` now have Schema-backed parameter specs
|
- Typed string registries in `Flow` now have Schema-backed parameter specs
|
||||||
|
|
@ -1376,10 +1407,13 @@ Notes:
|
||||||
create-on-failure behavior. Future backend slices should move
|
create-on-failure behavior. Future backend slices should move
|
||||||
connection/stream state into scoped Effect services.
|
connection/stream state into scoped Effect services.
|
||||||
- Treat rate-limit retry timeout semantics as complete; next consumer slices
|
- Treat rate-limit retry timeout semantics as complete; next consumer slices
|
||||||
should focus on shutdown, not retry policy.
|
should focus on blocking compatibility and backend/layer ownership, not
|
||||||
|
retry policy.
|
||||||
- Treat Effect-native per-worker consumer ownership as complete; do not flag
|
- Treat Effect-native per-worker consumer ownership as complete; do not flag
|
||||||
`makeEffectConsumerFromPubSub` concurrency for shared backend receive
|
`makeEffectConsumerFromPubSub` concurrency for shared backend receive
|
||||||
handles.
|
handles.
|
||||||
|
- Treat request-response pending shutdown semantics as complete; do not flag
|
||||||
|
`waitForResponse` timeout behavior for stopped runtimes.
|
||||||
- Tests:
|
- Tests:
|
||||||
- Fake backend ack/nak/backoff/stop tests, NATS close finalizer tests, and
|
- Fake backend ack/nak/backoff/stop tests, NATS close finalizer tests, and
|
||||||
config-push stream tests.
|
config-push stream tests.
|
||||||
|
|
@ -1472,6 +1506,10 @@ Do not flag these as rewrite blockers without additional proof:
|
||||||
rechecked against
|
rechecked against
|
||||||
`/home/elpresidank/YeeBois/projects/beep-effect2/.repos/effect-v4/packages/effect/src/PubSub.ts`,
|
`/home/elpresidank/YeeBois/projects/beep-effect2/.repos/effect-v4/packages/effect/src/PubSub.ts`,
|
||||||
whose exported API is local publish/subscribe over Effect queues.
|
whose exported API is local publish/subscribe over Effect queues.
|
||||||
|
- Request-response pending shutdown semantics are complete in
|
||||||
|
`makeEffectRequestResponseFromPubSub`: pending calls race response waiting
|
||||||
|
against a `Deferred` stop signal and fail with tagged
|
||||||
|
`MessagingLifecycleError`.
|
||||||
- `ts/packages/flow/src/gateway/rpc-protocol.ts` is a Fastify socket
|
- `ts/packages/flow/src/gateway/rpc-protocol.ts` is a Fastify socket
|
||||||
compatibility bridge. Do not flag its internal connection maps/sets as a
|
compatibility bridge. Do not flag its internal connection maps/sets as a
|
||||||
standalone replacement target until the gateway is ready to move onto Effect
|
standalone replacement target until the gateway is ready to move onto Effect
|
||||||
|
|
|
||||||
|
|
@ -408,8 +408,8 @@ describe("Effect-native messaging runtime", () => {
|
||||||
it.effect(
|
it.effect(
|
||||||
"fails request-response calls with a typed timeout",
|
"fails request-response calls with a typed timeout",
|
||||||
Effect.fnUntraced(function* () {
|
Effect.fnUntraced(function* () {
|
||||||
const responseConsumer = new ScriptedConsumer<string>();
|
const responseConsumer = new ScriptedConsumer<unknown>();
|
||||||
const backend = new RuntimeBackend(responseConsumer as BackendConsumer<unknown>);
|
const backend = new RuntimeBackend(responseConsumer);
|
||||||
|
|
||||||
const error = yield* Effect.scoped(
|
const error = yield* Effect.scoped(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
@ -440,6 +440,41 @@ describe("Effect-native messaging runtime", () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it.effect(
|
||||||
|
"fails pending request-response calls when the runtime stops",
|
||||||
|
Effect.fnUntraced(function* () {
|
||||||
|
const responseConsumer = new ScriptedConsumer<string>();
|
||||||
|
const backend = new RuntimeBackend(responseConsumer as BackendConsumer<unknown>);
|
||||||
|
|
||||||
|
const error = yield* Effect.scoped(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const requestor = yield* makeEffectRequestResponseFromPubSub<string, string>(
|
||||||
|
PubSub.fromBackend(backend),
|
||||||
|
{
|
||||||
|
...defaultMessagingRuntimeConfig,
|
||||||
|
consumerReceiveTimeoutMs: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
requestTopic: "tg.test.request",
|
||||||
|
responseTopic: "tg.test.response",
|
||||||
|
subscription: "sub",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const fiber = yield* requestor.request("request", { timeoutMs: 1_000 }).pipe(Effect.forkChild);
|
||||||
|
yield* TestClock.adjust(Duration.millis(5));
|
||||||
|
yield* requestor.stop;
|
||||||
|
return yield* Fiber.join(fiber).pipe(Effect.flip);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(error).toMatchObject({
|
||||||
|
_tag: "MessagingLifecycleError",
|
||||||
|
operation: "stop",
|
||||||
|
resource: "tg.test.request:tg.test.response",
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
it.effect(
|
it.effect(
|
||||||
"owns Flow lifecycle through a scoped Effect boundary",
|
"owns Flow lifecycle through a scoped Effect boundary",
|
||||||
Effect.fnUntraced(function* () {
|
Effect.fnUntraced(function* () {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { Context, Duration, Effect, Fiber, Layer, Queue, Ref, Result, Schedule, Scope, Stream } from "effect";
|
import { Context, Deferred, Duration, Effect, Fiber, Layer, Queue, Ref, Result, Schedule, Scope, Stream } from "effect";
|
||||||
import * as O from "effect/Option";
|
import * as O from "effect/Option";
|
||||||
import * as S from "effect/Schema";
|
import * as S from "effect/Schema";
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -92,7 +92,7 @@ export interface EffectRequestResponse<TReq, TRes> {
|
||||||
readonly request: <E = never, R = never>(
|
readonly request: <E = never, R = never>(
|
||||||
request: TReq,
|
request: TReq,
|
||||||
options?: EffectRequestOptions<TRes, E, R>,
|
options?: EffectRequestOptions<TRes, E, R>,
|
||||||
) => Effect.Effect<TRes, MessagingDeliveryError | MessagingTimeoutError | E, R>;
|
) => Effect.Effect<TRes, MessagingDeliveryError | MessagingLifecycleError | MessagingTimeoutError | E, R>;
|
||||||
readonly stop: Effect.Effect<void, MessagingLifecycleError | MessagingDeliveryError>;
|
readonly stop: Effect.Effect<void, MessagingLifecycleError | MessagingDeliveryError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -476,12 +476,17 @@ export const makeEffectRequestResponseFromPubSub = Effect.fn("makeEffectRequestR
|
||||||
};
|
};
|
||||||
const backend = yield* pubsub.createConsumer<TRes>(createOptions);
|
const backend = yield* pubsub.createConsumer<TRes>(createOptions);
|
||||||
const subscribers = new Map<string, Queue.Queue<TRes>>();
|
const subscribers = new Map<string, Queue.Queue<TRes>>();
|
||||||
|
const stoppedSignal = yield* Deferred.make<never, MessagingLifecycleError>();
|
||||||
const fiber = yield* dispatchResponseLoop(backend, options.responseTopic, subscribers, config).pipe(Effect.forkScoped);
|
const fiber = yield* dispatchResponseLoop(backend, options.responseTopic, subscribers, config).pipe(Effect.forkScoped);
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
|
|
||||||
const stop = Effect.fn(`RequestResponse.stop:${options.requestTopic}`)(function* () {
|
const stop = Effect.fn(`RequestResponse.stop:${options.requestTopic}`)(function* () {
|
||||||
if (stopped) return;
|
if (stopped) return;
|
||||||
stopped = true;
|
stopped = true;
|
||||||
|
yield* Deferred.fail(
|
||||||
|
stoppedSignal,
|
||||||
|
messagingLifecycleError(`${options.requestTopic}:${options.responseTopic}`, "stop", "RequestResponse stopped"),
|
||||||
|
).pipe(Effect.ignore);
|
||||||
yield* Fiber.interrupt(fiber);
|
yield* Fiber.interrupt(fiber);
|
||||||
yield* producer.close;
|
yield* producer.close;
|
||||||
yield* closeConsumerBackend(backend, options.responseTopic, options.subscription);
|
yield* closeConsumerBackend(backend, options.responseTopic, options.subscription);
|
||||||
|
|
@ -517,6 +522,7 @@ export const makeEffectRequestResponseFromPubSub = Effect.fn("makeEffectRequestR
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* producer.send(id, request);
|
yield* producer.send(id, request);
|
||||||
const result = yield* waitForResponse(queue, requestOptions).pipe(
|
const result = yield* waitForResponse(queue, requestOptions).pipe(
|
||||||
|
Effect.raceFirst(Deferred.await(stoppedSignal)),
|
||||||
Effect.timeoutOption(Duration.millis(timeoutMs)),
|
Effect.timeoutOption(Duration.millis(timeoutMs)),
|
||||||
);
|
);
|
||||||
return yield* O.match(result, {
|
return yield* O.match(result, {
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import {
|
||||||
type LibrarianRequest,
|
type LibrarianRequest,
|
||||||
type LibrarianResponse,
|
type LibrarianResponse,
|
||||||
type MessagingDeliveryError,
|
type MessagingDeliveryError,
|
||||||
|
type MessagingLifecycleError,
|
||||||
type MessagingTimeoutError,
|
type MessagingTimeoutError,
|
||||||
type Spec,
|
type Spec,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
|
@ -54,6 +55,7 @@ export class PdfDecoderError extends S.TaggedErrorClass<PdfDecoderError>()(
|
||||||
type PdfDecoderHandlerError =
|
type PdfDecoderHandlerError =
|
||||||
| FlowResourceNotFoundError
|
| FlowResourceNotFoundError
|
||||||
| MessagingDeliveryError
|
| MessagingDeliveryError
|
||||||
|
| MessagingLifecycleError
|
||||||
| MessagingTimeoutError
|
| MessagingTimeoutError
|
||||||
| PdfDecoderError;
|
| PdfDecoderError;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
type FlowContext,
|
type FlowContext,
|
||||||
type FlowResourceNotFoundError,
|
type FlowResourceNotFoundError,
|
||||||
type MessagingDeliveryError,
|
type MessagingDeliveryError,
|
||||||
|
type MessagingLifecycleError,
|
||||||
type MessagingTimeoutError,
|
type MessagingTimeoutError,
|
||||||
type EntityContexts,
|
type EntityContexts,
|
||||||
type EmbeddingsRequest,
|
type EmbeddingsRequest,
|
||||||
|
|
@ -41,6 +42,7 @@ type GraphEmbeddingsStoreRequirements = QdrantGraphEmbeddingsStoreService;
|
||||||
type GraphEmbeddingsStoreError =
|
type GraphEmbeddingsStoreError =
|
||||||
| FlowResourceNotFoundError
|
| FlowResourceNotFoundError
|
||||||
| MessagingDeliveryError
|
| MessagingDeliveryError
|
||||||
|
| MessagingLifecycleError
|
||||||
| MessagingTimeoutError
|
| MessagingTimeoutError
|
||||||
| QdrantGraphEmbeddingsStoreError;
|
| QdrantGraphEmbeddingsStoreError;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue