select/race flattens activity errors into the winner's value instead of surfacing a failure
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 48/100
- Tipo di issue
- Bug
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Tranquilla
- Stack tecnologico
- javascript, rust
- Ambito
- backend, distributed-systems
Direzione di ricerca
Inizia da src/handlers.rs, in particolare da make_select_future e ScheduledTask::Select, quindi confronta il percorso di errore diretto in handlers.rs e lib/duroxide.js con make_join_future. Esegui la riproduzione RaceBoom fornita e ispeziona i punti di ingresso race() / raceTyped(). Il lavoro è completato quando un ramo vincente che ha avuto esito negativo non può essere scambiato per un valore riuscito e il comportamento esistente di select è coperto dalla validazione di regressione.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Summary
When an activity participates in ctx.race() / ctx.raceTyped() and fails, the select resolves successfully with the branch's raw error string as its value instead of surfacing a failure. The orchestration receives { index: 0, value: "<error message>" } and has no way to distinguish a failed activity from a successful activity that legitimately returned that same string.
This is inconsistent with the SDK's own semantics elsewhere:
- A directly yielded activity that fails is delivered via
driveStepWithError→gen.throw(...)— the orchestration sees a thrown error (handlers.rsbuilds the step payload withisError,lib/duroxide.jscallsgen.throwwhenisErroris set). ctx.all()/ctx.allTyped()(join) deliberately preserves the distinction:make_join_futurewraps each branch as{ok: v}/{err: e}("Unlike make_select_future, this preserves the ok/err distinction so JS can tell success from failure").
Only select flattens. The comment in src/handlers.rs documents it as intentional:
/// Convert a ScheduledTask into a type-erased future returning a raw string for use in select.
/// Activity/sub-orch errors are flattened (Ok and Err both become the raw string value).
fn make_select_future(...) -> ... {
match task {
ScheduledTask::Activity { .. } => Box::pin(async move {
...
match future.await {
Ok(v) => v,
Err(e) => e, // <-- error becomes the winning *value*
}
}),
...
The same flattening applies to ActivityWithRetry, SubOrchestration*, and GetValueFromInstance branches.
Why this is a footgun
The failure is silent. Orchestration code like:
const winner = yield ctx.race(
ctx.scheduleActivity("CallModel", input), // may fail
ctx.dequeueEvent("cancel"),
);
if (winner.index === 0) {
const result = JSON.parse(winner.value); // error string flows in here
...
}
happily treats the error message as the activity's result. Any error-handling try/catch around the yield — which works correctly for the direct-yield form — never fires. If the activity's legitimate return type is a plain string, the two cases are indistinguishable even in principle.
We hit this in PilotSwarm while converting a directly-yielded long-running activity into race(activityTask, dequeueEvent(stopQueue)) for a stop-button feature: the conversion silently disabled the entire activity retry path until we noticed the flattening in the bridge source and added a shape-sniffing workaround (parse the value; if it isn't the expected JSON payload, re-throw it as an error). That workaround only works because our activity returns structured JSON, not strings.
Repro
runtime.registerActivity("Boom", async () => { throw new Error("kaboom"); });
runtime.registerOrchestration("RaceBoom", function* (ctx) {
try {
const winner = yield ctx.race(
ctx.scheduleActivity("Boom", null),
ctx.scheduleTimer(60_000),
);
// Reached with winner = { index: 0, value: "kaboom" } — no throw.
return `unexpected success: ${JSON.stringify(winner)}`;
} catch (err) {
return `caught: ${err.message}`; // never reached
}
});
Expected (to match direct-yield semantics): the catch fires, or the winner carries an explicit error marker.
Actual: the orchestration completes with unexpected success: {"index":0,"value":"kaboom"}.
Possible fixes
- Throw into the generator when the select winner is a failed branch — consistent with the direct-yield contract. (Losing branches are already cancel-requested; only the winner's disposition changes.)
- Preserve the marker like join does: resolve select with
{ index, ok }/{ index, err }(or{ index, value, isError }). Breaking change for existing callers, but the current shape is unreliable anyway. - At minimum, document the flattening prominently on
race()/raceTyped()in the README and JSDoc — today it's only visible in a Rust source comment.
Option 1 seems most consistent; option 2 is more expressive if you'd rather races never throw.
Environment
- duroxide-node 0.1.27 (npm), also verified against current
mainsources (src/handlers.rsmake_select_future/ScheduledTask::Selecthandling) - macOS arm64, Node 20+
- Lingua principale
- JavaScript
- Stelle
- 36
- Fork
- 20
- Merge medio
- 2g 9h
- PR unite (30g)
- 3
Preparare l'ambiente
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di microsoft/duroxide-node
-
Expose orchestrator_lock_timeout in JsRuntimeOptions (Rust supports it, Node bindings do not)Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
microsoft/duroxide-node#11 · 3 commenti ·
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 28/100
microsoft/duroxide-node#16 ·
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 48/100
Tutte le issue di microsoft/duroxide-node
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
CopilotKit/aimock#491 ·
I maintainer di solito rispondono entro 1 giorno
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
keyxmakerx/Chronicle#967 ·
I maintainer di solito rispondono entro 1 giorno
-
good first issue hacktoberfest
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 92/100
RogueAlg0/taken#386 · 3 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 92/100
DietrichGebert/ponytail#978 ·
I maintainer di solito rispondono entro 2 giorni
-
bug component: bulk editor support
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
Yoast/wordpress-seo#23669 ·
I maintainer di solito rispondono entro 3 giorni