@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 +68 -0
- package/build/index.js +102 -96
- package/build/index.js.map +3 -3
- package/build/types/service/src/createAmqpConnection.d.ts.map +1 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
const
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
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
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
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
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
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
|
-
|
|
2896
|
-
|
|
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=
|
|
3149
|
+
//# debugId=00BE6DF82B2A72F564756E2164756E21
|
|
3144
3150
|
//# sourceMappingURL=index.js.map
|