conductor-oss / conductor-oss/go-sdk

NewForkTask() auto-join has empty joinOn — JOIN task completes immediately without waiting for fork branches

Open Beginner friendly
#263 0 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
Go
Stars
66
Forks
25
Avg merge
3h 44m
Merged PRs (30d)
2

Description

## Summary

`NewForkTask()` automatically creates a JOIN task with empty `joinOn: []`. An empty `joinOn` causes the server JOIN executor to complete the join immediately (before any fork branch finishes), defeating the purpose of the FORK_JOIN.

Tested against: **Conductor OSS 3.32.0-rc.9**

## Root Cause

```go
// sdk/workflow/fork_join.go
func (task *ForkTask) getJoinTask() model.WorkflowTask {
join := task.join
if join == nil {
join = NewJoinTask(task.taskReferenceName + "_join") // no joinOn args
}
return (join.toWorkflowTask())[0]
}
```

`NewJoinTask(name)` with no varargs → `joinOn: nil` → serialized as `joinOn: []`.

The server's Join executor uses `joinOn.stream().allMatch(...)` to check branch completion. On an empty stream, `allMatch` returns `true` immediately — the JOIN transitions to `COMPLETED` without waiting for any branch.

## Live Test Evidence (Conductor OSS 3.32.0-rc.9)

Two workflows with a 10-second WAIT branch, checked 3 seconds after start:

```
joinOn=[] → wf=RUNNING join1=COMPLETED w1=IN_PROGRESS ← BUG (join done, branch still running)
joinOn=[w1] → wf=RUNNING join1=IN_PROGRESS w1=IN_PROGRESS ← CORRECT (join waiting)
```

Any workflow built with `NewForkTask()` exhibits this behavior — the downstream tasks run without waiting for the fork branches.

## Workaround

`NewForkTaskWithJoin(ref, joinTask, ...)` lets the caller supply a `*JoinTask` with explicit `joinOn`. This avoids the bug but requires users to know to avoid `NewForkTask()`.

## Fix

Infer `joinOn` from the last task reference name in each fork branch (the last task in each branch is what the JOIN should wait for):

```go
func (task *ForkTask) getJoinTask() model.WorkflowTask {
join := task.join
if join == nil {
joinOn := make([]string, 0, len(task.forkedTasks))
for _, branch := range task.forkedTasks {
if len(branch) > 0 {
last := branch[len(branch)-1].toWorkflowTask()
if len(last) > 0 {
joinOn = append(joinOn, last[0].TaskReferenceName)
}
}
}
join = NewJoinTask(task.taskReferenceName+"_join", joinOn...)
}
return (join.toWorkflowTask())[0]
}
```

Same root cause as [conductor-oss/javascript-sdk#135](https://github.com/conductor-oss/javascript-sdk/issues/135).

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in sdk/workflow/fork_join.go at ForkTask.getJoinTask and inspect how NewForkTask serializes its generated JOIN. Run the relevant Go tests or a workflow using a delayed fork branch, then verify that the generated joinOn contains each branch's terminal task reference and remains incomplete until those branches finish.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
distributed-systems
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.