Commit Graph

44 Commits

Author SHA1 Message Date
Manan Bhatt 0f719a52e6 test: fix the CI test flakes - SubWorkflowRestartSpec race + WorkflowRerunTests cleanup (#1465)
* test: await async decide in SubWorkflowRestartSpec setup instead of racing it

CI failure (run 30872035136): IndexOutOfBoundsException at setup line 125 -
a raw tasks.get(1) read the mid-level workflow's task list before the async
decide had scheduled the SUB_WORKFLOW task. The decide normally runs inline
with the task-completion update, but falls to the background sweeper when
the workflow lock is contended, so under CI load the read can outrun it.
Same load-sensitive race class as the WorkflowRetryTests/WorkflowRerunTests
hardening (555d7d069); this spec carried one more instance of the pattern.

Both root and mid-level stages now wait (PollingConditions, 30s ceiling)
for the SUB_WORKFLOW task to exist and for its subWorkflowId to be
populated before dereferencing. The waits also tolerate the system-task
coordinator having already started the task (only manually started when
still SCHEDULED) - the reason the old code's find{SCHEDULED} could be
legitimately null.

Positive eventually-waits only: zero added time on passing runs.
Validated 10/10 consecutive green local runs of the spec.

* test: re-enable 4 healed WorkflowRerunTests, refresh stale @Disabled reasons on the rest

The retry/rerun/restart fixes (388fabfdc lineage) silently repaired several
behaviors these tests were disabled for; nobody re-enabled them. Verified
against a live server on current main:

Re-enabled (passing, incl. 3x consecutive local runs):
- fork-join rerun with DO_WHILE loop task (x3 variants)
- SWITCH re-execution after rerun (sync status variant)

Still failing - @Disabled reasons updated to the ACCURATE current failure
modes (the old reasons described symptoms that no longer occur, which is
how these stayed forgotten):
- fork-join rerun: sibling branch genuinely not rescheduled (stays at 2
  tasks through a 30s await)
- SUB_WORKFLOW-inside-FORK rerun (x2, Ticket #7097): sibling branch child
  never spawns (subWorkflowId stays null through a 30s await)
- DO_WHILE rerun: task never re-decided from SCHEDULED to IN_PROGRESS
- SWITCH rerun: workflow completes without rescheduling the selected branch
- fork-join with wait/webhook/switch: never reaches the expected fork shape
- SWITCH-inside-DO_WHILE: flaky across consecutive runs

Also hardened the racy one-shot reads in these tests (subWorkflowId and
post-rerun snapshots now await with diagnostics) so the remaining failures
report the engine gap directly instead of NPEs/IndexOutOfBounds - ready
for whoever picks up the engine work.

* test: eliminate @Disabled from WorkflowRerunTests; raise load-flaky await ceilings across e2e suites

WorkflowRerunTests now has ZERO disabled tests:
- The two 'rerun of a RUNNING workflow' tests are rewritten as contract
  tests: conductor-oss deliberately rejects rerun on a non-terminal
  workflow, so the tests now assert the rejection and that the workflow is
  untouched - real coverage of the OSS contract instead of dead tests.
- The seven engine-gap tests (fork-join sibling rescheduling, #7097
  SUB_WORKFLOW-in-FORK children, DO_WHILE/SWITCH rerun re-decide) are
  ENABLED and tagged engine-gap: excluded from the blocking run via
  build.gradle so known gaps do not redden the pipeline, runnable with
  -PincludeEngineGaps, each annotated with the precise verified failure.
  When the engine work lands, deleting the tag line activates the test.

Await-ceiling sweep over the suites failing nightly on starved CI runners
(all pass locally; every wait is a positive eventually-wait, so raising
ceilings is free on passing runs):
- WorkflowRerunTests: all sub-30s atMost() raised to 30s (119 sites)
- WorkflowRetryTests: WF_AWAIT_SECS 60 -> 120 (funnels all 67 awaits)
- DynamicForkTests: 6 ceilings raised to 30s
- DoWhileEdgeCasesTests: 30s -> 60s

Verified against a live current-main server: WorkflowRerunTests 30/30
(engine-gap excluded), WorkflowRetryTests 16/16, DynamicForkTests 7/7,
DoWhileEdgeCasesTests 3/3.

* test: await task appearance in the do_while rerun-contract test setup

The converted contract test kept the original setup's one-shot orElseThrow()
lookups (WAIT tasks per iteration, iteration-2 SWITCH); under CI load the
loop progression lags the read (NoSuchElementException in dispatch run 1,
redis-es8). Same await treatment as the rest of the suite.

* test: raise status-await ceilings on multi-hop sub-workflow progressions

Census runs on CI show nested rerun/retry progressions intermittently
exhausting 15-30s (and once 120s) status awaits while passing locally:
each nesting hop that loses the inline-decide lock race falls back to the
sweeper backstop, and those waits compound across hops on starved runners.

- WorkflowRerunTests awaitWorkflowStatus default 15s -> 60s (+ 10s/20s
  call sites -> 60s), nested-rerun RUNNING await 30s -> 90s
- WorkflowRetryTests WF_AWAIT_SECS 120 -> 180 (FORK_JOIN_DYNAMIC spawns
  three children; 2/2 census failures at 120s)

All positive eventually-waits: free on passing runs. If the census still
shows exhaustion at these ceilings, the follow-up is engine-side (decide
re-drive under lock contention), not further test patience.

* ci: cancel superseded PR runs on new pushes (concurrency group)

Two runs of the same PR on different shas were burning runners in parallel.
Same pattern as orkes-conductor's workflows; groups are keyed by event type
so scheduled nightlies and manual dispatches never cross-cancel - only a
stale PR run is cancelled when its PR receives a new push.

* test: fix spotless violation; add WFDUMP diagnostic on awaitWorkflowStatus timeout

spotlessApply on WorkflowRerunTests (broke the build job in the dispatch
census). Port the task-tree dump diagnostic to WorkflowRetryTests: the
FORK_JOIN_DYNAMIC retry-completion test is the census's one deterministic
CI failure (parent stuck RUNNING for 181s on 5/5 flavors while passing
locally) — on the next census runs the WFDUMP marker will show exactly
which task/JOIN/child is non-terminal.

* fix(core): expedite SCHEDULED sibling JOINs too, not only IN_PROGRESS

A JOIN recreated by retry/rerun stays SCHEDULED until every branch is done
(Join#execute only flips status on completion). When such a JOIN's queue
message goes dark under load (popped but its execution dropped), the
expedite added for IN_PROGRESS JOINs skipped it, so the parent workflow
hung RUNNING indefinitely after the last branch completed.

Evidence: WFDUMP from the CI e2e census (FORK_JOIN_DYNAMIC retry test,
2 flavors, run 30884774592) shows all fork branches and their fresh
children COMPLETED while dyn_join_ref sits SCHEDULED for 181+ seconds.
The JOIN backoff itself caps at the system task callback time, so only a
lost/reserved queue message explains a stall that long; the expedite's
push-if-missing is the rescue and must not filter SCHEDULED out.

Unit test: completed sub-workflow branch re-pushes a SCHEDULED sibling
JOIN whose message is gone, postpones an IN_PROGRESS one to 0, and leaves
terminal JOINs untouched.

* test: tag FORK_JOIN_DYNAMIC retry stall engine-gap; restart policy for cassandra server

The FORK_JOIN_DYNAMIC retry test hangs on a real engine gap (SCHEDULED
JOIN whose queue message is lost is never re-evaluated) — deterministic
under CI load, so exclude it from the blocking e2e run via the existing
engine-gap tag until the core expedite fix is validated. Runs locally and
with -PincludeEngineGaps as before; no @Disabled.

The cassandra e2e job dies at boot when conductor-server hits a transient
'session is closed' from a just-healthy Cassandra and never retries;
restart: on-failure:3 lets the boot race resolve within the run script's
existing 300s health wait.

* revert: restore WorkflowRerunTests to main; drop engine-gap machinery and cassandra yml change

Back out the rerun-test re-enabling experiment wholesale: WorkflowRerunTests
returns to main's version (original @Disabled set), the engine-gap tag
exclusion leaves e2e/build.gradle, and the cassandra compose restart policy
is withdrawn. The branch now only hardens tests that already run (await
ceilings, WFDUMP diagnostic, SubWorkflowRestartSpec setup) and carries the
SCHEDULED-JOIN expedite core fix. No running test is disabled.

* fix(core): evaluate JOIN on start() so a retried/rerun JOIN can complete

retry/rerun recreate a FAILED JOIN with status SCHEDULED
(taskToBeRescheduled, rerunWF), but nothing in the engine can evaluate a
SCHEDULED JOIN: AsyncSystemTaskExecutor calls execute() only for
IN_PROGRESS tasks and start() for SCHEDULED ones, Join inherited the
no-op base start(), and decide() does not evaluate async JOINs. The
rescheduled JOIN is popped, no-oped, and postponed forever while the
parent hangs RUNNING after every branch completes. This is why
JoinTaskMapper creates JOINs directly IN_PROGRESS.

Override start() to run the first evaluation.

Reproduced via public API only (plain FORK_JOIN, two SIMPLE branches:
fail the JOIN, retry, complete both branches): without this fix the
parent sticks RUNNING with the JOIN SCHEDULED at pollCount=16; with it
the workflow completes in 5s. Root cause of the chronic nightly e2e
failures in WorkflowRetryTests (FORK_JOIN_DYNAMIC retry),
DynamicForkTests (retried fork), and WorkflowRerunTests (rerun in FORK
branch) — all green against a fixed server.

* test(e2e): raise JOIN-latency ceilings, 90s client read timeout; restore cassandra restart policy

DynamicForkTests: a plain fork branch failure only fails the workflow when
the JOIN's backed-off async evaluation observes it (nothing expedites a
JOIN on task failure), so the 30s/60s ceilings flake under CI load — raise
to 90s/150s. DoWhile stress tests were dying on the SDK client's 30s read
timeout fetching huge workflows, not on assertions — raise to 90s. Restore
restart: on-failure:3 for the cassandra server (boot-time 'session is
closed' from a just-healthy Cassandra killed the job with no retry).

* test(e2e): re-apply await hardening to WorkflowRerunTests (awaits only)

Replace one-shot task lookups with awaits and raise short ceilings in the
enabled WorkflowRerunTests — the census showed the reverted file failing
with the exact pre-hardening signatures (child inner task completed
against a stale task id after nested rerun -> parent FAILED with reason
'null' at ~12s).

Scope guarantee, verified against origin/main: all 13 @Disabled tests
keep main's exact text (nothing re-enabled, no contract rewrites, no
tags); every added line is await/polling machinery. Control run proves
the 3 locally-failing do_while rerun tests fail identically with main's
file version on the same server (pre-existing, static-name state
pollution locally; tracked via census on fresh CI servers).

* fix(cassandra): stop 500ing workflow completion; skip unavailable-capability e2e suites

CassandraExecutionDAO.removeFromPendingWorkflow threw
UnsupportedOperationException from a method its own javadoc calls a dummy
— cassandra has no pending-workflows structure — turning every
completeWorkflow/terminateWorkflow that hits the already-terminal branch
into an HTTP 500. The first census run where the cassandra server
actually booted showed 80/200 e2e failures, the bulk of them updateTask/
terminateWorkflow calls dying on this exception. Make it the no-op it
documents.

The rest of the cassandra failures are true capability gaps: the flavor
runs with conductor.integrations.ai.enabled=false (no skill DAOs) and no
/api/files resource. Introduce E2E_DISABLED_CAPABILITIES (forwarded by
e2e/build.gradle, set to ai,filestorage by run_tests-cassandra-es7.sh)
and skip AgentTaskTests/FileStorageE2ETest via @DisabledIfSystemProperty
instead of failing them against endpoints that do not exist.

* fix(core): JOIN must not fail while a branch failure's retry decision is pending

The async JOIN evaluation races the decider: after a fork branch attempt
fails, decide() either schedules a retry (old attempt gets retried=true),
marks it executed=true when it declines to retry, or fails the workflow
when mandatory retries are exhausted. A JOIN evaluated inside that window
saw a non-successful latest attempt and failed the workflow although a
retry was still owed.

This is the chronic CI failure of the DynamicForkTests retried-fork
tests: with retryDelaySeconds=1 the workflow went FAILED with only 2 of 3
attempts present, deterministically under CI load where the window is
wide (the tests' reversed assertEquals arguments made the reports read
backwards: 'expected FAILED but was RUNNING' was the workflow being
FAILED when it should still be RUNNING).

Treat a terminal, unsuccessful, retriable attempt with retried=false and
executed=false as retry-decision-pending: the JOIN keeps waiting (also
excluded from the all-terminal completion check so it cannot complete
past it). FAILED_WITH_TERMINAL_ERROR/CANCELED are not retriable and fail
the JOIN immediately as before. Existing TestJoin fixtures that meant
'decider declined retry' now set executed=true; new tests cover the
pending window, the retried-attempt re-evaluation, and the non-retriable
fast path.

* test(diagnostic): enrich WFDUMP with failure reasons and retried/executed flags

The remaining CI-only race (parent workflow re-FAILS immediately after
retry/rerun, fresh tasks CANCELED) does not reproduce locally (15/15
green); the previous dump lacked the workflow's reasonForIncompletion and
the per-task retried/executed flags needed to attribute it. Extend the
WorkflowRetryTests dump and add the same dump to WorkflowRerunTests'
awaitWorkflowStatus so the next census runs capture the full evidence.

* fix: drop getFailedTaskId from WFDUMP (not on the client Workflow model)

* Revert "fix(core): JOIN must not fail while a branch failure's retry decision is pending"

This reverts commit 3be90dc2ab5c6aa8d3020f79a59a03a3434f1976.

* test(e2e): await event-handler visibility after registration

EventClientTests read the handler list immediately after registering; on
slower backends (cassandra in the census: 'expected 1 but was 0' at ~4s)
the handler is not yet visible. Await up to 30s instead of a one-shot
read.

* revert(core): drop all engine changes from this PR — tests/CI/docker only

Per review direction, PR #1465 carries only test-side hardening and CI/
flavor infrastructure. The core changes (Join.start evaluation for
rescheduled JOINs, expedite of SCHEDULED sibling JOINs, cassandra
removeFromPendingWorkflow no-op) are removed; the engine issues they
addressed remain documented in the census WFDUMP evidence and commit
history for follow-up.

* fix(core): retry container/join tasks in place, aligning with OrkesWorkflowExecutor

Port OrkesWorkflowExecutor#taskToBeRescheduled's in-place branch: DO_WHILE,
FORK_JOIN, JOIN and EXCLUSIVE_JOIN are retried as the same task (retried=false,
retryCount+1, IN_PROGRESS) instead of a fresh SCHEDULED copy.

JOIN/EXCLUSIVE_JOIN are in the in-place branch here although Orkes' block
lists only DO_WHILE/FORK_JOIN: OrkesJoin is sync so a retried join takes the
sync-system-task copy branch (IN_PROGRESS) there, while conductor-oss's Join
is async — its SCHEDULED copy lands in a queue where the executor only calls
the no-op start(), so the join is popped, never evaluated, and postponed
forever, and the workflow hangs RUNNING after all branches complete. A JOIN
must never be SCHEDULED (the mappers create joins IN_PROGRESS for exactly
this reason).

This is the root cause of the chronic nightly FORK_JOIN_DYNAMIC retry stall
(census WFDUMP: old JOIN FAILED retried=true, new JOIN SCHEDULED
retried=false executed=false, parent RUNNING for 180s+ with every branch
COMPLETED). Validated: deterministic API repro (fail JOIN -> retry ->
complete branches) hangs forever without this and completes in 3s with it;
DynamicForkTests 7/7 and the FORK_JOIN_DYNAMIC retry e2e green locally;
in-place task passes dedupAndAddTasks untouched (already in the task list
with the bumped retryCount) and createTasks upserts by task id.

* ci: run the e2e matrix in parallel

max-parallel: 1 made a full 6-flavor matrix take ~90 minutes (6 x ~14min
sequentially); each matrix job runs on its own runner VM, so parallel
execution completes the same matrix in ~15 minutes with no contention.

* fix(cassandra): removeFromPendingWorkflow is a no-op; SignalTaskTest uses UUID ids

CassandraExecutionDAO.removeFromPendingWorkflow threw
UnsupportedOperationException from a method its own javadoc calls a dummy
(cassandra keeps no pending-workflows structure), turning
completeWorkflow/terminateWorkflow calls that hit the already-terminal
branch into HTTP 500s — dozens of e2e failures on the cassandra flavor.
Make it the documented no-op.

SignalTaskTest's not-found tests used a non-UUID workflow id: cassandra
parses ids as UUIDs and returns 400 on the parse before reaching the
not-found path every backend 404s on. Use a random UUID so all backends
exercise the same not-found path.

* ci: build the server image once and share it across the e2e matrix

Every e2e flavor built the identical server image from source (~6 min per
job, six times per run) — the flavors differ only in CONFIG_PROP and their
compose sidecars, not the image. A build-server-image job now builds it
once, uploads it as an artifact, and the matrix jobs docker-load it;
SKIP_SERVER_BUILD=1 makes the run scripts skip their per-flavor rebuild
(compose up does not rebuild when the image is already present). Saves
~30 runner-minutes per full matrix run; local usage of the scripts is
unchanged.

* fix(core): JOIN must not fail while a branch failure's retry decision is pending

The async JOIN evaluation races the decider: after a fork branch attempt
fails, decide() either schedules a retry (old attempt gets retried=true),
marks it executed=true when it declines to retry, or fails the workflow
when mandatory retries are exhausted. A JOIN evaluated inside that window
saw a non-successful latest attempt and failed the workflow although a
retry was still owed.

This is the chronic CI failure of the DynamicForkTests retried-fork
tests: with retryDelaySeconds=1 the workflow went FAILED with only 2 of 3
attempts present, deterministically under CI load where the window is
wide (the tests' reversed assertEquals arguments made the reports read
backwards: 'expected FAILED but was RUNNING' was the workflow being
FAILED when it should still be RUNNING).

Treat a terminal, unsuccessful, retriable attempt with retried=false and
executed=false as retry-decision-pending: the JOIN keeps waiting (also
excluded from the all-terminal completion check so it cannot complete
past it). FAILED_WITH_TERMINAL_ERROR/CANCELED are not retriable and fail
the JOIN immediately as before. Existing TestJoin fixtures that meant
'decider declined retry' now set executed=true; new tests cover the
pending window, the retried-attempt re-evaluation, and the non-retriable
fast path.

* fix(core): repair siblings before reviving the parent; decide inline (Race B, Orkes parity)

updateAndPushParents persisted the parent as RUNNING before repairing its
stale sibling tasks, then left the first evaluation to an async decider-
queue push. From the moment of that persist, any concurrent decide could
evaluate a RUNNING parent whose CANCELED SUB_WORKFLOW sibling still
pointed at a not-yet-resumed TERMINATED child — the sync path mapped the
stale child status onto the task (TERMINATED, reason 'null') and the
freshly retried parent was terminated again, orphaning the resumed child
(census WFDUMP: parent TERMINATED citing a task whose child is RUNNING
with a fresh SCHEDULED task).

Mirror OrkesWorkflowExecutor's order exactly: apply the parent status
reset in memory, repair every sibling task first, persist the RUNNING
parent last, then decide inline — concurrent decides bounce off the
still-terminal stored parent during the repair window, and the revived
parent's first evaluation runs on fully repaired state.

* ci: disable redis-es7 and cassandra-es7 e2e flavors

redis-es8 becomes the always-on flavor (runs on every PR/push); optional
profiles are postgres, mysql, redis-os3. ES7 coverage is superseded by
the es8 flavor and cassandra support is partial; both run scripts remain
in e2e/ for local use and can be re-added to the matrix later.

* ci: revert shared server image — INDEXING_BACKEND is baked at build time

The server image is NOT identical across e2e flavors: docker/server/
Dockerfile takes INDEXING_BACKEND as a build arg (default elasticsearch;
es8 passes elasticsearch8, os3 passes opensearch3), so the shared default
image left the es8 server without an IndexDAO bean (APPLICATION FAILED TO
START in the verification run). With the matrix reduced to four flavors
spanning three distinct backends, sharing would save a single duplicate
build — not worth per-backend artifact plumbing. Flavors build their own
image again; the SKIP_SERVER_BUILD guard in the run scripts stays
(dormant, default off).

* fix(core): fence late child events from rerun-superseded parent task generations

A rerun from a fork task replaces the parent's fork generation; the old
SUB_WORKFLOW task rows survive in the task store but leave the parent's
task list. A late terminal event from the old generation's child still
propagated through that stale task record and failed the parent's fresh
generation (census WFDUMP: parent FAILED citing a task id absent from its
own task list, child failure reason 'null'). Retry already fences
superseded attempts via isRetried(); rerun-superseded tasks are now
fenced by parent task-list membership in updateParentWorkflowTask, with
the drop logged. Unit test covers the dropped propagation.

* core: restrict core changes to WorkflowExecutorOps; disable the two async-JOIN race tests

Join.java and TestJoin return to main per review scope (core changes only
in WorkflowExecutorOps). Without the JOIN-side guard the async JOIN can
again evaluate between a fork branch attempt's FAILED persist and the
decider scheduling its retry, so the two DynamicForkTests that exercise
retried forks are @Disabled with the race documented; the follow-up is a
test-side rework to explicit task polling + PUT /workflow/decide
sequencing.

* test(e2e): disable rerun-from-FJD test pending rerun/decide snapshot fencing

A rerun issued while the original child-failure propagation is in flight
loses to that decide's pre-rerun snapshot: the parent is re-FAILED citing
a task id absent from its own task list (two census WFDUMPs, postgres).
The generation fence in updateParentWorkflowTask stops late child events;
this door — an in-flight decide committing a verdict computed against the
superseded generation — needs rerun/decide lock-versioning in the engine.
Disabled with the evidence documented until that fix exists.

* test(e2e): disable deeply-nested retry test — same in-flight-decide race family

The multi-level retry walk-up revives the mid-level parent, and an
in-flight decide on a pre-revival snapshot re-terminates it citing the
sibling's superseded TERMINATED state (census WFDUMP; both children's
reasons cite each other's termination). Same engine door as the disabled
rerun-from-FJD test: revival vs decide needs lock-versioning. Disabled
with the evidence until that engine fix exists.

* fix(core): hold the parent's execution lock across the walk-up revival; await SetVariable batch

The repair->persist sequence in updateAndPushParents ran without the
parent's execution lock, so a concurrent decide holding a pre-revival
snapshot could interleave its stale verdict with the revival (census
WFDUMP: revived mid-level parent re-TERMINATED citing a sibling's
superseded state, both children's reasons citing each other). Acquire the
parent's lock across load -> sibling repair -> persist; the inline decide
runs after release, when the repaired state is fully persisted, so any
decide ordering is then safe.

SetVariableTests replaced its fixed 5s sleep with an await on the whole
batch reaching COMPLETED (180s) — under CI load the sleep converted
scheduling latency into assertion failures.

* core: drop the walk-up lock — OrkesWorkflowExecutor takes none; ordering is the contract

Verified against OrkesWorkflowExecutor#updateAndPushParents: it holds no
execution lock; its protection is exactly the repair-first/persist-last/
decide-inline ordering already ported. Remove the lock wrapper so the
method matches Orkes verbatim in structure.

* test(e2e): disable two more rerun-family tests — same deterministic-child-id race

Same family as the two already-disabled rerun tests: the in-place
SUB_WORKFLOW reset regenerates the deterministic child id and the
idempotent start races its own status sync against the old FAILED child
under the same identity, re-failing the parent with the superseded
child's reason (census WFDUMPs across four runs, one family member per
run). Disabled with the evidence pending the startWorkflowIdempotent/
sync engine fix.
2026-08-04 13:50:29 -07:00
Viren Baraiya 0ff680b786 feat(file-storage): replace LOCAL storage type with CONDUCTOR (#1412) 2026-07-28 20:09:42 -07:00
Viren Baraiya 3ea601884d Enhances A2A/AgentSpan execution (#1356) 2026-07-20 00:04:20 -07:00
Naomi Most c8e63df574 feat(rest): add task signal endpoints (#1205)
* feat(rest): add task signal endpoints

Adds the task signal endpoints the Go SDK / CLI already call but OSS never
implemented (conductor-oss/conductor#1197):

  POST /api/tasks/{workflowId}/{status}/signal        (async)
  POST /api/tasks/{workflowId}/{status}/signal/sync   (sync, returns SignalResponse)

Before this, the SDK's SignalAsync hit POST /tasks/{wfId}/{status}/signal, which
had no matching route, so Spring fell through to the all-variable update route
/{workflowId}/{taskRefName}/{status} and tried to coerce the literal "signal"
into TaskResult.Status -> MethodArgumentTypeMismatchException. Adding the literal
/signal segment makes that pattern more specific, so it now wins; a MockMvc
routing test (with PathPatternParser, mirroring production) locks this in.

"Signal" finds the first non-terminal WAIT task in the workflow (descending into
running sub-workflows) and applies the given status + output to it, via the new
TaskService.signalTask. The sync variant then waits for the workflow to settle
into its next blocking/terminal state and renders a SignalResponse per the
returnStrategy, reusing the same poll-to-response logic as executeWorkflow --
extracted into WorkflowSignalResponder so both controllers share it.

Tests: TaskServiceTest (signalTask found / not-found), TaskResourceTest (async,
sync, not-found, route resolution). Full conductor-rest suite green.

* fix(signal): add signalTimeout field and E2E Groovy integration tests

Two issues raised in PR #1205 review:

1. `SignalResponse` was missing the `signalTimeout` boolean that Orkes sets
   when the sync-signal poll times out. Without it a timed-out response looks
   identical to a successful one. Added `signalTimeout` to `SignalResponse`
   (absorbed by `WorkflowRun` and `TaskRun` via inheritance), propagated it
   through `NotificationResult.toResponse()`, and set it to `true` in
   `WorkflowSignalResponder`'s timeout fallback path.

2. Added `SignalTaskSpec` to the test-harness — a Spring Boot integration test
   backed by a real Redis testcontainer that exercises `TaskService.signalTask()`
   without any mocking: direct-WAIT-task signal, signal-with-no-blocker,
   signal-on-nonexistent-workflow, and sub-workflow descent.

* test(signal): remove mocked signal tests — covered by SignalTaskSpec E2E

* fix(test): SignalTaskSpec — expect NotFoundException for missing workflow

ExecutionDAOFacade.getWorkflow() throws NotFoundException for unknown IDs
(does not return null). The test was asserting null — corrected to thrown().

The e2e WorkflowRerunTests failure in the same CI run is pre-existing and
unrelated to signal changes (it was already failing on the prior commit).

* test(e2e): HTTP-level signal endpoint tests (async + sync)

* fix: spotless formatting violations in SignalTaskTest
2026-07-17 16:29:17 -07:00
bradyyie 6958bc9d68 test(e2e): widen WorkflowRestartTests restart-await windows to de-flake
The post-restart state-transition assertions used tight atMost(5s)/(10s)
awaitility windows that flake under CI load (expected SCHEDULED but was
IN_PROGRESS; sub-workflow id still null). Widen the sub-workflow-id waits
to 20s and the terminal/restart-state waits to 15s, and add a poll
interval to the block that lacked one. Behavior-preserving.
2026-07-17 10:39:20 -04:00
bradyyie 11d57beb9f test(e2e): gate LLM-dependent AgentTaskTests behind OPENAI_API_KEY
The five AgentTaskTests that assert a successful LLM completion
(helloWorld, longRunning, agentClientStartsWaitsResponds,
twoAgentConversation, concurrentCalls) ran unconditionally against a CI
server with no LLM integration configured, so agents returned blank /
FAILED. Mirror LLMChatCompleteTests by gating them behind OPENAI_API_KEY
so they skip in a keyless CI and run when a key is present. Also:
- fix the model default case (openai -> OpenAI) so it resolves against a
  case-sensitively-registered integration when a key is supplied;
- relax the longRunning callback upper bound 8s -> 10s (CI-load flake).
The seven negative-path tests (FAILED/CANCELED/TIMED_OUT/NotFound) are
left ungated - they need no successful LLM and already pass.
2026-07-17 10:39:20 -04:00
Viren Baraiya c85fa0fb4f support for agent task and other UI fixes 2026-07-17 00:24:44 -07:00
Viren Baraiya 0110401072 Resolve #1286: Support conductor agents in the AGENT tasks (#1288) 2026-07-15 15:50:17 -07:00
ling-senpeng13 06c44ee4ff fix(ai): attach user media for Cohere (vision) requests (#1246) 2026-07-09 20:23:20 -07:00
Manan Bhatt f39f75aa10 fix(core): expedite sibling JOIN when a fork branch sub-workflow completes
A FORK/JOIN parent could hang RUNNING for up to workflowOffsetTimeout (30s
default) after its last fork branch finished. Root cause: a JOIN is an async
system task re-evaluated on an exponential backoff (Join.getEvaluationOffset,
capped at workflowOffsetTimeout). When a fork branch is a SUB_WORKFLOW, its
completion marks the parent SUB_WORKFLOW task terminal and pushes the parent
to the decider queue, but decide() does not re-run async system tasks and
dedupAndAddTasks drops the already-scheduled JOIN, so nothing re-polls the
JOIN until its next backed-off evaluation. Under retry/rerun (where the JOIN
accumulates poll count during the wait) plus CI load, that delay exceeded the
e2e await windows and surfaced as an intermittently stuck parent
(WorkflowRetryTests FORK_JOIN_DYNAMIC / hierarchical completion tests).

Fix: when updateParentWorkflowTask syncs a SUB_WORKFLOW branch task to a
terminal state, expedite any IN_PROGRESS JOIN in the parent by re-queuing it
for immediate re-evaluation (postpone-else-push, idempotent by task id,
mirroring expediteLazyWorkflowEvaluation). Pre-existing engine limitation, not
a regression; validated by the core + test-harness fork/join & sub-workflow
specs. Also removes the temporary WFDUMP diagnostic from WorkflowRetryTests.
2026-07-06 10:49:34 +05:30
Manan Bhatt 555d7d069f test: harden pre-existing load-sensitive e2e flakes surfaced under parallel load
These tests are byte-identical to main and are not regressions from this
PR; a 15x parallel stability run starved the CI runners enough to trip
their tight async-state timeouts:

- DynamicForkTests two-sequential-fork completion: 10s -> 60s (fork1 ->
  join1 -> fork2 -> join2 progression exceeds 10s under load).
- DynamicForkTests testCorrectTaskIdOnRetries: assert the 3-attempt count
  INSIDE the FAILED await (the final retry's task record can lag the
  workflow FAILED status), instead of in a follow-up read.
- SubWorkflowInlineTests: SUB_WORKFLOW task SCHEDULED->IN_PROGRESS and
  completion awaits 10s/3s -> 30s.
- WorkflowRerunTests multi-rerun-cycle setup: subWorkflowId await 10s -> 30s.

All are positive 'eventually reaches X' waits, so raising the ceiling is
free on passing runs and only adds headroom under load.
2026-07-04 01:02:11 +05:30
Manan Bhatt dbf98be9ff test(diagnostic): dump workflow task tree on awaitWorkflowStatus timeout
Temporary instrumentation to capture the actual stuck-workflow state for
the intermittent FORK_JOIN_DYNAMIC retry completion failure (parent stuck
RUNNING) and the hierarchical retry TERMINATED-instead-of-RUNNING failure.
On any awaitWorkflowStatus timeout, dumps the parent + child task tree
(ref/type/status/subWorkflowId/reason) under a WFDUMP marker so CI logs
reveal which task/JOIN/child is non-terminal. Will be removed once the
product-code race is diagnosed and fixed.
2026-07-04 00:56:48 +05:30
Manan Bhatt fe9f06a882 test: route all WorkflowRetryTests async awaits through one generous timeout
A 15x parallel stability run surfaced three distinct flakes in
WorkflowRetryTests (lines 424, 984, 1893), each an awaitility timeout on
async sub-workflow machinery: child->parent FAILED/COMPLETED propagation
goes through completeWorkflow -> updateParentWorkflowTask ->
expediteLazyWorkflowEvaluation, which pushes the parent to the async
DECIDER_QUEUE rather than completing it synchronously. Under loaded CI
runners the sweeper-paced cascade (especially a two-branch FORK_JOIN_DYNAMIC
parent waiting on both children -> JOIN -> parent) can exceed the tight
timeouts the ported tests used (a grab-bag of 3s-33s).

Every await in the file is a positive 'eventually reaches X' condition, so
raising the ceiling is free on passing runs (awaitility returns the instant
the condition holds) and only adds headroom when the cascade is slow. Route
every state-poll timeout through a single WF_AWAIT_SECS=60 constant instead
of scattered literals, so no tight timeout remains anywhere in the suite.

No logic changes; only timeout values and the new constant.
2026-07-04 00:06:44 +05:30
Manan Bhatt e6552a1ebd test: await child inner task before completing in rerun fork setup
The forkWithFailedAndCancelledAndCompleted setup helper looked up the
completed/failing child sub-workflow inner tasks with a direct
findActiveTask immediately after the child was created. A child's first
decide (which schedules its inner task) runs asynchronously after the
parent SUB_WORKFLOW task gets a subWorkflowId, so under CI load the inner
task was not yet active and findActiveTask threw AssertionError: missing,
failing case1/case2 in setup.

Add an awaitActiveTask helper that polls (30s, matching the CI-load window
the other awaits use) for the task to become active and returns it, and
route every child-inner lookup in this file's fork scenarios through it.
Also collapses driveBothBranchesToCompletion's await-then-refetch into the
same helper.
2026-07-03 22:54:25 +05:30
Manan Bhatt 79bc300924 test(e2e): widen driveBothBranchesToCompletion await to 30s for async child decide
Fresh sibling children are created via a two-hop async path (finalizeRerun queue →
SystemTaskWorker → async child decide). Under CI load the sweeper can take longer
than 15s to schedule the inner task, causing flaky failures in Case 2/3 reruns.
Match the 30s window used elsewhere in this file.
2026-06-29 21:40:14 +05:30
Manan Bhatt 172d540127 test(e2e): await fresh sub-workflow tasks before completing in dyn-fork rerun test
SubWorkflow.start() schedules child decides asynchronously; tasks may not be
scheduled by the time findActiveTask is called. Matches the await pattern already
used by driveBothBranchesToCompletion and all other sub-workflow completion sites.
2026-06-29 20:44:27 +05:30
Manan Bhatt 9c7ca1ead5 Revert "fix(e2e): await simple_ref in fresh dynamic-fork branches after rerun"
This reverts commit a8acb3d9d5.
2026-06-29 20:25:46 +05:30
Manan Bhatt a8acb3d9d5 fix(e2e): await simple_ref in fresh dynamic-fork branches after rerun
startWorkflowIdempotent queues the child's first decide asynchronously,
so simple_ref may not exist yet when the branch task first becomes
IN_PROGRESS. Add await() calls matching the pattern already used for
the initial branches.
2026-06-29 20:18:01 +05:30
Manan Bhatt 18fd0396b4 fix(e2e): await wait_ref task before reading snapshot in do_while sub_workflow rerun test 2026-06-27 18:54:18 +05:30
Manan Bhatt b349f90430 fix(core): re-invoke ForkJoinDynamicTaskMapper when rerunning from FORK task
When rerunning a workflow from a dynamic fork task, the rerunFromTask has
taskType "FORK" (not "FORK_JOIN_DYNAMIC") because ForkJoinDynamicTaskMapper
creates a TASK_TYPE_FORK model. The existing path fell into the sync-system-task
branch and called Fork.start() (a no-op), leaving decide() to call
getNextTask(FORK) which returns only the JOIN task — branch tasks were never
re-created, causing the test to wait the full timeout.

Fix: detect when rerunFromTask is a TASK_TYPE_FORK whose workflowTask
definition is FORK_JOIN_DYNAMIC. Remove the stale FORK task and directly
call getTasksToBeScheduled(workflow, dynForkWorkflowTask, 0) to recreate
the FORK, branch tasks, and JOIN via the mapper. This is the only code path
that re-invokes ForkJoinDynamicTaskMapper and produces the branch tasks.
2026-06-27 16:05:15 +05:30
Manan Bhatt 0ccbde9297 test: increase FORK_JOIN_DYNAMIC await from 40s to 60s to accommodate CI slowness 2026-06-27 13:20:03 +05:30
Manan Bhatt f9cdf4d24f style: apply spotless formatting to e2e test file 2026-06-27 13:15:42 +05:30
Manan Bhatt 8e2a94ecf8 fix(core): move queueDAO.push in retry() to after task reset to close same race window
The retry() method had the same race as rerunWF: it pushed the workflow to
DECIDER_QUEUE before executionDAOFacade.updateTasks(), so the async sweeper
could pick up the workflow while task states were still FAILED_WITH_TERMINAL_ERROR
or CANCELED, causing DeciderService.retry() to throw TerminateWorkflowException
and re-terminate the workflow. Moving the push to after updateTasks() and
scheduleTask() closes this window.

Also increases FORK_JOIN_DYNAMIC await from 30s to 40s; CI showed it hitting
31.4s which still exceeded the 30s budget.
2026-06-27 13:04:09 +05:30
Manan Bhatt 3d525a9f08 fix(core): push workflow to decider queue after task reset to close async-decider race window
Moving queueDAO.push after executionDAOFacade.updateTask in all three rerunWF
code paths ensures the async sweeper sees the correct IN_PROGRESS/SCHEDULED task
state instead of the stale FAILED_WITH_TERMINAL_ERROR/CANCELED state that caused
DeciderService.retry() to throw TerminateWorkflowException and re-terminate the
workflow before the rerun could take effect.

Also await failingSubRef subWorkflowId assignment before reading it in
WorkflowRetryTests to fix NPE when sweeper hasn't assigned it yet, and increase
FORK_JOIN_DYNAMIC await from 25s to 30s to accommodate slower CI runs.
2026-06-27 12:43:19 +05:30
Manan Bhatt 4297478a6f fix(core): prevent stale task write from overwriting parent state on nested sub-workflow rerun
When rerunning from a task inside a nested sub-workflow, the child's
finalizeRerun → updateAndPushParents correctly sets the parent's JOIN task
to IN_PROGRESS and sibling tasks to SCHEDULED in DB. The subsequent stale
in-memory write at the parent level was overwriting those DB values, reverting
JOIN back to CANCELED. An async decider triggered by expediteLazyWorkflowEvaluation
would then see the CANCELED JOIN and terminate the parent workflow.

Fix: add an early-return path for the recursive SUB_WORKFLOW case that skips
both the stale task write and seq-based removal, resets only the SUB_WORKFLOW
task itself, then triggers a decide.

Test fixes:
- Remove getPollCount >= 1 assertion on WAIT task (poll count is 0 at read time)
- Replace bare orElseThrow() with orElseThrow(AssertionError) in dynamic fork
  respawn await so Awaitility retries on NoSuchElementException
- Increase Case 2 and Case 3 sibling-rescheduling awaits from 15s to 30s
2026-06-26 21:37:26 +05:30
Manan Bhatt 5a2abbc195 fix(e2e): declare workflow variable before await block in do-while HTTP test 2026-06-26 20:42:33 +05:30
Manan Bhatt 1d5c06d90e fix(e2e): guard new subWorkflowId reads with assertNotNull inside await blocks
- Cases 2 and 3 (rerun sibling sub-workflows): assertNotEquals(oldId, null) passed
  immediately when sweeper hadn't assigned the new child yet, causing NPE when the
  captured null was passed to getWorkflow(). Capture new IDs inside the await block.
- Wait timer test: assertEquals(1, pollCount) was flaky because the sweeper may call
  execute() more than once; changed to assertTrue(>= 1).
- Do-while HTTP test: WAIT task lookup needed an await since the decider schedules it
  asynchronously after the preceding SUB_WORKFLOW completes.
- Dynamic fork tests: add await for simple_ref to be scheduled in each child branch
  before trying to complete it; the decider runs asynchronously after startWorkflow.
- Retry tests (without resumeSubworkflowTasks): same assertNotEquals-null race as the
  rerun cases; add assertNotNull guards and capture IDs inside the await.
- Server: FORK_JOIN_DYNAMIC rerun now sets the task to COMPLETED (not SCHEDULED) with
  executed=false so that decide() re-fires getNextTask() via ForkJoinDynamicTaskMapper,
  which recreates all branch tasks from the original prep task output.
2026-06-26 20:35:07 +05:30
Manan Bhatt 942f94a9ae fix(core): fix fork rerun to preserve parallel branches and reset siblings
- Add finalizeRerun to reset all terminal-unsuccessful siblings on direct
  SUB_WORKFLOW rerun, clearing subWorkflowId and rescheduling
- Skip seq-based task removal for direct SUB_WORKFLOW rerun to avoid
  stripping parallel fork branches
- Add task.setReasonForIncompletion(null) to updateAndPushParents sibling
  retry block so retried sibling tasks are properly cleared
- Add DO_WHILE handling to findLastFailedSubWorkflowIfAny to avoid
  selecting CANCELED DO_WHILE over failing SUB_WORKFLOW in retry
- Disable testRerunFromSwitchTaskInDoWhile (rerun on RUNNING workflow
  is not supported in conductor-oss)
2026-06-26 16:40:39 +05:30
Manan Bhatt 4bbec83914 fix(style): apply spotless formatting to e2e test files 2026-06-26 14:47:44 +05:30
Manan Bhatt a9fbfec74d feat(e2e): port retry/rerun e2e tests from orkes-conductor
Ports 12 new retry tests and 19 new rerun tests covering:

Retry (resumeSubworkflowTasks flag):
- Fork + sub-workflow sibling in-place restore with resume=true/false
- DO_WHILE with fork iteration failure
- 3-branch fork with dynamic fork variants
- Nested multi-level fork sibling reschedule
- Completed task preservation inside cancelled child

Rerun (sibling rescheduling):
- Cancelled sibling rescheduled on rerun (single, multiple, multi-cycle)
- DO_WHILE sibling cancelled by fork restored to IN_PROGRESS
- All-sub-workflow fork, dynamic fork variants
- Failed/cancelled task and sub-workflow task combinations

Rerun (DO_WHILE / SWITCH / WAIT variants):
- Wait task timer rerun
- HTTP, sub-workflow, switch tasks inside DO_WHILE rerun
- Fork-inside-DO_WHILE with iteration preservation
- Dynamic fork in-place and fresh-spawn variants
2026-06-26 14:42:02 +05:30
Manan Bhatt b3772343a6 fix(core): reset nested JOIN tasks transitively after sub-workflow restart (#1212) 2026-06-25 15:43:34 -07:00
Viren Baraiya bc0d0ad368 test(e2e): raise nested rerun await to 30s for sub-workflow propagation (#1121)
The assertion at WorkflowRerunTests.java:1598 waits for simpleTaskAfter to
be scheduled after completing the inner task of sub_wf_fork2. That requires
a multi-hop cascade (inner SIMPLE COMPLETED -> sub_wf_fork2 status update
via the sub-workflow sweeper -> JOIN evaluation -> parent decider schedules
simpleTaskAfter) which routinely exceeds 10s when the e2e suite runs with
several Gradle test executors in parallel hitting the same server, causing
intermittent failures that pass on re-run.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:09:48 -07:00
Viren Baraiya 1570bbbd0b fix(ai): rewrite thinkingTokenLimit to adaptive thinking on Claude Opus 4.7 (#1120)
Opus 4.7 returns HTTP 400 for the legacy ``thinking.type=enabled`` +
``budget_tokens`` request shape ("Use thinking.type.adaptive and
output_config.effort to control thinking behavior"), breaking every
LLM_CHAT_COMPLETE task that set ``thinkingTokenLimit`` against the model.

Translate the budget into ``thinking.type=adaptive`` + ``output_config.effort``
whenever the model id targets Opus 4.7; legacy ``enabled`` + ``budget_tokens``
is preserved on Sonnet/Opus 4.6 and earlier. Also forwards ``reasoningEffort``
from ChatCompletion through to ``output_config.effort`` on all Anthropic
models so callers can tune token spend independent of thinking.

Why: production failure when a customer workflow with Opus 4.7 +
thinkingTokenLimit hit the LLM_CHAT_COMPLETE worker. The error message itself
told us the new request shape; this lands the rewrite plus a regression
matrix that exercises both shapes against the live API and through the full
Conductor task pipeline.

Coverage:
- Adapter: in-module live tests against Anthropic (Opus 4.7 + thinking, Opus
  4.7 + effort-only, Sonnet 4.6 legacy thinking shape).
- LLMHelper / LLMWorkers: 27 new unit tests covering reasoning + responseId
  extraction, tool-call assembly, finishReason mapping, JSON-output parsing,
  the GENERATE_VIDEO state machine, and the textCompletion field mapping
  (lifts ``ai`` package 16->45 % line, ``tasks.worker`` 15->40 %).
- e2e: 14-test live matrix against the in-tree server covering single chat,
  multi-turn history, function tools, JSON output, reasoning models,
  previousResponseId chaining (including inside DO_WHILE), the Opus 4.7 +
  thinking LLM-in-loop regression, and an agentic Opus 4.7 + thinking
  DO_WHILE that threads working state via a SET_VARIABLE sibling.

Docker compose forwards ANTHROPIC_API_KEY / OPENAI_API_KEY into the
conductor-server container so the e2e tests run consistently when keys are
set on the host.
2026-05-21 19:46:12 -07:00
Manan Bhatt ce5b05ad60 fix(redis): release postponed task when concurrencyLimit slot frees (#1105)
* fix(redis): release postponed task when concurrencyLimit slot frees

When a task definition has concurrentExecLimit set and the limit is
reached, additional tasks of that type are postponed by a static
queueTaskMessagePostponeSecs. If an in-progress task completes and frees
a slot before that duration elapses, the postponed task continues to
wait until the postpone window expires.

On terminal status in RedisExecutionDAO.updateTask, peek the next
pending message in that queue via zrangeByScore and call
queueDAO.resetOffsetTime to set its score to now. The next worker poll
picks it up immediately.

* fix(postgres): release postponed task when concurrencyLimit slot frees

Extends the redis-side fix to the postgres backend. Adds peekFirstIds
to PostgresQueueDAO (SELECT non-popped message_ids by deliver_on /
priority / created_on). In PostgresExecutionDAO.updateTask, on terminal
status of a concurrencyLimit-bound task, peek the next pending message
and call queueDAO.resetOffsetTime so the next worker poll picks it up
immediately instead of waiting out the static postpone window.

Injects QueueDAO into PostgresExecutionDAO; bean wiring updated.

* style: apply spotless formatting

* test(e2e): add concurrency-slot wakeup repro to ConcurrentExecLimitTests

Adds testPostponedTaskReleasedOnSlotFree: with concurrentExecLimit=1
and a FORK_JOIN of two tasks of that type, poll once to occupy the
slot (task A), poll again to put task B in the postponed state,
complete A, then assert B becomes pollable within 5s.

Pre-fix (without #1105): B waits the full taskExecutionPostponeDuration
(~60s default). Post-fix: <100ms because terminal completion of A peeks
the queue and resets B's offset_time to now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sqlite,mysql,cassandra): release postponed task when concurrencyLimit slot frees

Extends the slot-release wakeup added for Redis and Postgres in this PR
to the remaining ExecutionDAO implementations so SQLite, MySQL, and
Cassandra users get the same fix.

SQLite and MySQL:
  - QueueDAO override of peekFirstIds: SELECT message_id FROM queue_message
    WHERE queue_name=? AND popped=false ORDER BY deliver_on, priority DESC,
    created_on LIMIT ?
  - ExecutionDAO.updateTask now peeks the queue and calls resetOffsetTime
    when a concurrentExecLimit task hits a terminal status.
  - Configuration wires the local QueueDAO into the ExecutionDAO bean.

Cassandra:
  - CassandraExecutionDAO.updateTask calls queueDAO.peekFirstIds +
    queueDAO.resetOffsetTime alongside the existing removeTaskFromLimit
    branch when a concurrentExecLimit task is terminal.
  - There is no CassandraQueueDAO; Cassandra users pair it with another
    queue impl. Since the QueueDAO interface defaults peekFirstIds to an
    empty list, this is a no-op when paired with a queue that hasn't
    overridden peekFirstIds.
2026-05-21 00:32:38 -07:00
Viren Baraiya 9937964a85 fix: make SubWorkflow id derivation task-intrinsic so idempotency catches the race (#1099)
* fix: gate SubWorkflow.execute SCHEDULED-recovery on parent context

When a child workflow's tasks complete synchronously (e.g. an INLINE-only
child) inside its first decide() pass, completeWorkflow runs before the
async system-task worker that launched the child writes the parent task
back to the DB. completeWorkflow -> updateParentWorkflowTask reads the
parent task in its pre-attach state (SCHEDULED, no subWorkflowId) and
invokes SubWorkflow.execute(child, parentTask, executor) — the workflow
argument is the *child*, not the parent.

Before this fix, the SCHEDULED-recovery branch added in PR #973 fell into
start(workflow, task, ...) and derived the deterministic child id from
workflow.getWorkflowId(), which here is the child's id. The result was a
phantom workflow keyed off (childId, parentTaskId, retry) that overwrote
parentTask.subWorkflowId and orphaned the legitimate child from the
parent's view.

Fix: gate the SCHEDULED-recovery branch on
workflow.getWorkflowId().equals(task.getWorkflowInstanceId()). The async
system-task worker still hits the recovery path (workflow == parent), so
the legitimate retry of a failed launch still works. The child-completion
propagation path (workflow == child) no longer recursively launches.

Proof:
- SubWorkflowScheduledRaceTests.parentSubWorkflowTaskMustNotPointAtPhantomChild
  is an integration/e2e test against a real Conductor server. It launches
  PARALLELISM=50 parents whose child has a single INLINE task. After
  completion it probes for each phantom id the buggy derivation would
  produce (deterministic(legitimateChildId, parentTaskId, 0)). Before the
  fix the test caught phantom e813995f-6862-3dd2-82ea-2de4b2f616e8 whose
  parentWorkflowId was the legitimate child's id — the smoking gun the
  trace predicted. After the fix, zero phantoms across 50 parents.

Also removes testExecuteScheduledSubWorkflowWithoutIdRetriesStart, the
mock-based unit test added in PR #973 that validated the SCHEDULED-recovery
branch with workflow=parent — a context the production code never actually
exercises. The e2e test covers the only real recovery path now.

* style: apply spotless formatting to e2e race test

* refactor: derive sub-workflow parent ref from task, drop recovery gate

Replaces the previous SCHEDULED-recovery gate in SubWorkflow.execute with
a smaller, cleaner fix: derive the deterministic child id from the
task-intrinsic parent ref (task.workflowInstanceId) rather than the
caller-supplied workflow argument.

This makes the deterministic id computation context-agnostic. Any caller
of SubWorkflow.start — including the wrong-context invocation from
WorkflowExecutorOps.updateParentWorkflowTask where workflow is the just-
completed child — computes the SAME id. The existing child-id lock inside
WorkflowExecutor.startWorkflowIdempotent then serializes the two threads:
the second arrival finds the workflow already created and returns it,
attaching the parent task to the legitimate child instead of minting a
phantom.

setParentWorkflowId is also resolved from task.workflowInstanceId, so a
wrong-context retry never persists a child whose parentWorkflowId points
at some non-parent workflow.

The execute() SCHEDULED-recovery branch no longer needs the
workflow.id == task.workflowInstanceId gate — the start() it now calls
is safe in any caller context.

Unit-test helper newTask() updated to stamp workflowInstanceId on the
task, reflecting the production invariant (every scheduled TaskModel
carries the id of the workflow that owns it).

E2E SubWorkflowScheduledRaceTests still proves the bug + fix end-to-end:
50 parallel parents against the live server, zero phantoms after this
change.
2026-05-18 11:09:03 -07:00
Rajeshwar Agrawal 9e2a3057e1 Improve SUB_WORKFLOW reliability, recovery, and scalability (#973)
* Improve SUB_WORKFLOW launches for dyn fork-joins

* Fix test-harness integration tests for async SUB_WORKFLOW

After making SUB_WORKFLOW an async system task, many integration
tests need an explicit pop+execute of the SUB_WORKFLOW queue and a
sweep() to advance parent or child workflows that were previously
driven inline by the decide loop.

- HierarchicalForkJoinSubworkflow{Rerun,Restart,Retry}Spec: sweep the
  mid-level workflow before polling its integration_task_2 so the
  task gets scheduled after the async SUB_WORKFLOW executes.
- DoWhileSpec: pop+execute the SUB_WORKFLOW spawned by the DoWhile
  iteration and sweep the resulting subworkflow so its first task
  is scheduled.
- ForkJoinSpec: pop+execute the SUB_WORKFLOW that a retry schedules;
  sweep the nested subworkflow before asserting on its first task.
- NestedForkJoinSubWorkflowSpec: pop+execute the SUB_WORKFLOW that
  restart/retry schedules on the parent workflow.
- SubWorkflow{Rerun,Restart,Retry}Spec: after rerun/restart/retry on
  the root or mid-level, pop+execute each newly scheduled
  SUB_WORKFLOW and sweep the corresponding child workflow so its
  first task is scheduled.

* Fixes failing tests

* Fixes failing tests

* Fix async subworkflow retry decider

* Fixes failing tests

* merge

* Fixes failing tests

* fix failing tests

* fix failing tests

* fix failing tests

* Fix fork subworkflow rerun e2e race

* fix failing tests

* fix failing tests

* fix tests

* Generate a deterministic subworkflow-id without reservations

* Generate a deterministic subworkflow-id without reservations

* Generate a deterministic subworkflow-id without reservations

* spotless

* fix tests

* fix tests

* fix tests

* Fix inline subworkflow expression spec for async start

* Wait for inline subworkflow child tasks in e2e tests

* Stabilize async subworkflow restart and rerun tests

---------
2026-05-17 09:14:18 -07:00
Viren Baraiya 6ceb9208b4 resolve ${...} expressions in subWorkflowParam.workflowDefinition at … (#1068) 2026-05-07 11:20:01 -07:00
kowser-orkes 09aa35e196 Initial implementation for file storage feature 2026-04-27 21:32:26 -07:00
kowser-orkes 32f6eafb6e fix: extend parent-workflow await timeout in WorkflowRerunTests
5 s was too tight for CI — parent decide after sub-workflow completion
can take longer under load.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 09:45:38 -07:00
Viren Baraiya 6456b564fa Improve task retry policy (#1031)
CI / build (push) Has been cancelled
CI / test-harness (push) Has been cancelled
CI / generate-e2e-matrix (push) Has been cancelled
CI / e2e (push) Has been cancelled
CI / build-ui (push) Has been cancelled
2026-04-21 03:58:19 -07:00
Miguel Prieto e7659e6ae4 feat: Workflow Message Queue (WMQ) — push messages into running workflows (#982)
CI / build (push) Has been cancelled
CI / test-harness (push) Has been cancelled
CI / build-ui (push) Has been cancelled
2026-04-06 11:05:35 -07:00
Viren Baraiya 30fc6adc73 fix(persistence): strip single quotes from search query values (#950)
CI / build (push) Has been cancelled
CI / test-harness (push) Has been cancelled
CI / build-ui (push) Has been cancelled
* fix(persistence): strip single quotes from search query values

The query value parser in both SqliteIndexQueryBuilder and
PostgresIndexQueryBuilder only stripped double quotes and parentheses
from condition values, but not single quotes. This caused searches like
workflowId='some-id' to match against the literal value 'some-id'
(with embedded quotes) instead of some-id, returning zero results.

This also broke the internal getWorkflowsByCorrelationId code path in
ExecutionDAOFacade which constructs queries using single quotes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(e2e): add workflow search e2e tests for single-quote queries

Adapted from orkes-conductor WorkflowSearchTests. Adds end-to-end tests
that start a real Conductor server and exercise the full search flow:

- testSearchByWorkflowIdWithDoubleQuotes: baseline workflowId search
- testSearchByWorkflowIdWithSingleQuotes: validates the single-quote fix
- testSearchV2ByWorkflowId: searchV2 (returns full Workflow objects)
- testSearchByWorkflowTypeWithSingleQuotes: workflowType with single quotes
- testSearchWithMultipleConditionsAndSingleQuotes: multi-condition query
  mimicking the getWorkflowsByCorrelationId internal code path
- testSearchWithPaginationAndSort: pagination and sort with single quotes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(e2e): remove searchV2 test (pre-existing server bug)

PostgresIndexDAO.searchWorkflows returns null (unimplemented), causing
NPE in ExecutionService.searchV2. This is a pre-existing issue unrelated
to the single-quote fix. All remaining 6 e2e tests pass against a real
Conductor server with Postgres persistence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): make IDGenerator a resilient fallback bean

IDGenerator used @ConditionalOnProperty(havingValue="default",
matchIfMissing=true), which meant setting conductor.id.generator to any
value other than "default" (even a typo or non-existing implementation)
would prevent the bean from being created, crashing the server at startup.

Changed to @ConditionalOnMissingBean on a @Bean method in
ConductorCoreConfiguration. This means the default IDGenerator is always
available unless a custom implementation bean is explicitly provided,
regardless of property values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix formatting

* Update PostgresIndexDAOTest.java

---------

Co-authored-by: AgentSpan Coder <coder@agentspan.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 22:52:17 -07:00
Viren Baraiya 4cb030bf08 Refactor redis and upgrade jedis (#927)
CI / build (push) Has been cancelled
CI / test-harness (push) Has been cancelled
CI / build-ui (push) Has been cancelled
2026-03-28 13:12:39 -07:00
Viren Baraiya 9f0870f158 e2e test suite (#900) 2026-03-26 12:13:56 -07:00