@requence/service 0.8.3 → 0.8.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,73 @@
1
1
  # @requence/service
2
2
 
3
+ ## 0.8.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 87f2347: A failed `act()` releases its defer key, and fails the node instead of parking it.
8
+
9
+ **The key leaked on every exit path but the last one.** `act(messageKey, actor)`
10
+ added the key to a per-connection `Set` at the top and deleted it only as the
11
+ final statement, with no `try`/`finally`. A malformed key, a throwing actor, a
12
+ rejected send or — the routine one — a trailing `publish({ done: true })` that
13
+ rejects with `Channel ended, no reply will be forthcoming` on a broker blip all
14
+ left the key in the set for the life of the process. Every later `act()` with it
15
+ threw `a defer key can only be used once` _before publishing anything_, so the
16
+ one operation that could recover the message was the operation the leak blocked.
17
+ The node upstream stayed parked and perfectly resumable; the pod simply refused
18
+ to talk to it until it restarted. Reported from production as a transient hiccup
19
+ that hung an accounting pipeline forever and turned a "re-deliver the answers"
20
+ action into a silent no-op.
21
+
22
+ The key is now released in a `finally`. The `Set` therefore means "an `act()` for
23
+ this key is in flight right now", which is the only thing a per-connection,
24
+ in-memory set can enforce — a second worker, or this one after a restart, was
25
+ never held by it. Nor is a defer key one-shot to begin with: the operator rewinds
26
+ a deferred message's index space whenever the acting **connection** changes
27
+ (`createStateMachine.ts`), precisely so a re-attach after a restart lands, and it
28
+ is `done` that spends the key. So the set is a mutual-exclusion lock, not a spend
29
+ ledger.
30
+
31
+ **And an actor that threw reported nothing at all.** Only a failed publish
32
+ reached the abort channel; a throw from the actor itself propagated straight out
33
+ of `act()` with no `abort` and no `done`, leaving the node deferred forever with
34
+ nothing logged anywhere. The arm now covers the actor, the generator loop, the
35
+ sends and the settle gate — everything between the resume row and the done
36
+ marker, all of which leave the same wreckage: whatever the actor already sent is
37
+ published _and_ already dispatched downstream, while the emission that ended the
38
+ actor never happened.
39
+
40
+ Re-attaching is not a way out of that, on either connection. On a **new**
41
+ connection the operator rewinds the index space, so the re-publish lands — but
42
+ `used` is deliberately not reset, so on a LINEAR node the first re-published
43
+ value trips `cannot send multiple times in linear mode` and fails the node with a
44
+ message that names the wrong problem. On the **same** connection nothing rewinds:
45
+ every re-publish is discarded as a duplicate index and only the done marker
46
+ lands, quietly completing the node with the _first_ attempt's data. So the node
47
+ now fails there and then, with the actor's own error, over the same channel a
48
+ failed publish already used.
49
+
50
+ The one exit that is a real recovery is deliberately left outside that arm: if
51
+ only the `done` publish fails, every value the actor sent did land and the node is
52
+ merely missing its marker, so re-running `act()` with the same key completes it —
53
+ which is exactly the call the leaked key used to refuse.
54
+
55
+ Two smaller fixes ride along. The abort publish is itself guarded, because the
56
+ channel that would carry it is usually the reason the arm was entered — the
57
+ failure is logged and the actor's original error stays the one the caller sees.
58
+ And the callback registry is now freed on both paths, not just the happy one.
59
+
60
+ **The Python SDK had neither arm, and no defer-key guard at all.** Both are
61
+ ported, along with the registry leak: `Service.act` now refuses a key already in
62
+ flight (locked, because `act()` is the one entry point an author calls from their
63
+ own threads), releases it in a `finally`, and aborts on a failure — flushing
64
+ first, since Python's `abort` only schedules its publish. Its settle gate already
65
+ raised an `ExceptionGroup`, which nothing caught, so that case was broken there
66
+ too; the group is unwrapped to its first entry so the node's error message is the
67
+ diagnosis rather than a sub-exception count. `BaseException` is deliberately not
68
+ caught: a `KeyboardInterrupt` or `sys.exit()` inside an actor is the caller ending
69
+ the process, not a node failing. Those still release the key.
70
+
3
71
  ## 0.8.3
4
72
 
5
73
  ### Patch Changes
package/build/index.js CHANGED
@@ -2008,7 +2008,7 @@ function createConnection({
2008
2008
  clientProperties: {
2009
2009
  ...connectionOptions?.clientProperties,
2010
2010
  ...buildServiceClientProperties({
2011
- libVersion: "javascript@0.8.3",
2011
+ libVersion: "javascript@0.8.4",
2012
2012
  serviceVersion: version,
2013
2013
  connectionId,
2014
2014
  prefetch,
@@ -2569,7 +2569,7 @@ function createConnection({
2569
2569
  begin: true,
2570
2570
  epoch,
2571
2571
  ...buildServiceIdentityHeaders({
2572
- libVersion: "javascript@0.8.3",
2572
+ libVersion: "javascript@0.8.4",
2573
2573
  name,
2574
2574
  holdsSlot: !(isGeneratorHandler && mode === "CONTINUOUS")
2575
2575
  })
@@ -2805,107 +2805,113 @@ function createConnection({
2805
2805
  throw new Error("a defer key can only be used once");
2806
2806
  }
2807
2807
  activeMessageKeys.add(messageKey);
2808
- let publishIndex = 0;
2809
- const { taskId, messageId, ...options } = messageKeySchema.parse(messageKey);
2810
- const activeMessage = async () => {
2811
- if (activeMessages.has(messageId)) {
2812
- await activeMessages.get(messageId);
2813
- }
2814
- };
2815
- const registeredCallbackIds = new Set;
2816
- const outbound = createOutboundBoundary(options);
2817
- const growth = callbackGrowth.forDelivery({ taskId });
2818
- const registerCallback = (callback, path2) => {
2819
- const callbackId = crypto.randomUUID();
2820
- registeredCallbackIds.add(callbackId);
2821
- callbacks.set(callbackId, {
2822
- callback,
2823
- onCallbackInvoke: dispatchInvocation
2824
- });
2825
- growth.record(path2);
2826
- return { sourceId: connectionId, callbackId };
2827
- };
2828
- const dispatchInvocation = (invocation) => {
2829
- outbound.track(outbound.send(invocation.arg, {
2830
- outputName: invocation.outputName,
2831
- onCallback: registerCallback
2832
- }).then((data) => publish({
2833
- index: 0,
2834
- version,
2808
+ try {
2809
+ let publishIndex = 0;
2810
+ const { taskId, messageId, ...options } = messageKeySchema.parse(messageKey);
2811
+ const activeMessage = async () => {
2812
+ if (activeMessages.has(messageId)) {
2813
+ await activeMessages.get(messageId);
2814
+ }
2815
+ };
2816
+ const registeredCallbackIds = new Set;
2817
+ const outbound = createOutboundBoundary(options);
2818
+ const growth = callbackGrowth.forDelivery({ taskId });
2819
+ const registerCallback = (callback, path2) => {
2820
+ const callbackId = crypto.randomUUID();
2821
+ registeredCallbackIds.add(callbackId);
2822
+ callbacks.set(callbackId, {
2823
+ callback,
2824
+ onCallbackInvoke: dispatchInvocation
2825
+ });
2826
+ growth.record(path2);
2827
+ return { sourceId: connectionId, callbackId };
2828
+ };
2829
+ const dispatchInvocation = (invocation) => {
2830
+ outbound.track(outbound.send(invocation.arg, {
2831
+ outputName: invocation.outputName,
2832
+ onCallback: registerCallback
2833
+ }).then((data) => publish({
2834
+ index: 0,
2835
+ version,
2836
+ taskId,
2837
+ messageId,
2838
+ data: invocationPayload(invocation, data),
2839
+ outputName: invocation.outputName,
2840
+ propertyPath: invocation.propertyPath,
2841
+ ...invocationHeaders(invocation, options.nodeId)
2842
+ })));
2843
+ };
2844
+ const publishDeps = {
2845
+ outbound,
2846
+ registerCallback,
2835
2847
  taskId,
2836
2848
  messageId,
2837
- data: invocationPayload(invocation, data),
2838
- outputName: invocation.outputName,
2839
- propertyPath: invocation.propertyPath,
2840
- ...invocationHeaders(invocation, options.nodeId)
2841
- })));
2842
- };
2843
- const publishDeps = {
2844
- outbound,
2845
- registerCallback,
2846
- taskId,
2847
- messageId,
2848
- nextIndex: () => publishIndex++,
2849
- gate: activeMessage
2850
- };
2851
- const renderSurface = buildRender(publishDeps);
2852
- const sendApi = {
2853
- ...buildSendApi(publishDeps),
2854
- render(ui, props, renderOptions) {
2855
- renderSurface({
2856
- ui,
2857
- props,
2858
- surfaceId: renderOptions?.surfaceId ?? DEFAULT_SURFACE_ID
2859
- });
2860
- },
2861
- async abort(error) {
2862
- await activeMessage();
2863
- channelWrapper.publish(`${exchange}-retry`, "operator", error instanceof Error ? error.message : error, {
2864
- persistent: true,
2865
- correlationId: messageId,
2866
- expiration: 0,
2867
- priority: 0,
2868
- contentType: "text/plain",
2869
- headers: {
2870
- taskId,
2871
- index: publishIndex++,
2872
- abort: true
2849
+ nextIndex: () => publishIndex++,
2850
+ gate: activeMessage
2851
+ };
2852
+ const renderSurface = buildRender(publishDeps);
2853
+ const sendApi = {
2854
+ ...buildSendApi(publishDeps),
2855
+ render(ui, props, renderOptions) {
2856
+ renderSurface({
2857
+ ui,
2858
+ props,
2859
+ surfaceId: renderOptions?.surfaceId ?? DEFAULT_SURFACE_ID
2860
+ });
2861
+ },
2862
+ async abort(error) {
2863
+ await activeMessage();
2864
+ channelWrapper.publish(`${exchange}-retry`, "operator", error instanceof Error ? error.message : error, {
2865
+ persistent: true,
2866
+ correlationId: messageId,
2867
+ expiration: 0,
2868
+ priority: 0,
2869
+ contentType: "text/plain",
2870
+ headers: {
2871
+ taskId,
2872
+ index: publishIndex++,
2873
+ abort: true
2874
+ }
2875
+ });
2876
+ }
2877
+ };
2878
+ announceResume({ taskId, messageId });
2879
+ try {
2880
+ const result = await actor(sendApi);
2881
+ if (result != null && typeof result === "object" && ((Symbol.asyncIterator in result) || (Symbol.iterator in result))) {
2882
+ const iterable = result;
2883
+ for await (const value of iterable) {
2884
+ if (value !== undefined) {
2885
+ await sendApi.send(value);
2886
+ }
2873
2887
  }
2874
- });
2875
- }
2876
- };
2877
- announceResume({ taskId, messageId });
2878
- const result = await actor(sendApi);
2879
- if (result != null && typeof result === "object" && ((Symbol.asyncIterator in result) || (Symbol.iterator in result))) {
2880
- const iterable = result;
2881
- for await (const value of iterable) {
2882
- if (value !== undefined) {
2883
- await sendApi.send(value);
2888
+ } else if (result !== undefined) {
2889
+ await sendApi.send(result);
2890
+ }
2891
+ await new Promise((resolve2) => setImmediate(resolve2));
2892
+ await activeMessage();
2893
+ await outbound.settle();
2894
+ } catch (error) {
2895
+ registeredCallbackIds.forEach((id) => callbacks.delete(id));
2896
+ const first = error instanceof AggregateError && error.errors[0] instanceof Error ? error.errors[0] : error instanceof Error ? error : new Error(String(error));
2897
+ try {
2898
+ await sendApi.abort(first);
2899
+ } catch (abortError) {
2900
+ simple_default.error("Requence act() could not report its failure to the operator:", abortError);
2884
2901
  }
2902
+ throw error;
2885
2903
  }
2886
- } else if (result !== undefined) {
2887
- await sendApi.send(result);
2888
- }
2889
- await new Promise((resolve2) => setImmediate(resolve2));
2890
- await activeMessage();
2891
- try {
2892
- await outbound.settle();
2893
- } catch (error) {
2894
2904
  registeredCallbackIds.forEach((id) => callbacks.delete(id));
2895
- const first = error instanceof AggregateError && error.errors[0] instanceof Error ? error.errors[0] : error instanceof Error ? error : new Error(String(error));
2896
- await sendApi.abort(first);
2905
+ await publish({
2906
+ index: publishIndex++,
2907
+ version,
2908
+ messageId,
2909
+ taskId,
2910
+ done: true
2911
+ });
2912
+ } finally {
2897
2913
  activeMessageKeys.delete(messageKey);
2898
- throw error;
2899
2914
  }
2900
- registeredCallbackIds.forEach((id) => callbacks.delete(id));
2901
- await publish({
2902
- index: publishIndex++,
2903
- version,
2904
- messageId,
2905
- taskId,
2906
- done: true
2907
- });
2908
- activeMessageKeys.delete(messageKey);
2909
2915
  }
2910
2916
  };
2911
2917
  return api;
@@ -3140,5 +3146,5 @@ export {
3140
3146
  PayloadTooLargeError
3141
3147
  };
3142
3148
 
3143
- //# debugId=A75FE0293DB162D764756E2164756E21
3149
+ //# debugId=00BE6DF82B2A72F564756E2164756E21
3144
3150
  //# sourceMappingURL=index.js.map