cloudwego / cloudwego/eino

compose: fan-in merge panics nondeterministically when a predecessor returns (nil, nil)

Open
#1,263 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
13k
Forks
1.1k
Avg merge
4h 6m
Merged PRs (30d)
41

Description

## Summary

When a node in a fan-in returns `(nil, nil)`, `mergeValues` (`compose/values_merge.go:40-41`) receives the untyped nil among the values to merge. Channel `Values` are stored in a Go map, so iteration order is random:

- when the nil lands at `vs[0]`: `reflect.ValueOf(vs[0]).Type()` **panics** with `reflect: call of reflect.Value.Type on zero Value` (re-panicked out of `Invoke` via `graph_run.go:112`);
- otherwise: the type-specific merge func fails with e.g. `(values merge map) field type mismatch. expected: 'map[string]interface {}', got: ''`.

Same compiled graph, same input — panic or error depending on map iteration order.

## To Reproduce

```go
func TestNilFanIn(t *testing.T) {
for i := 0; i < 30; i++ {
g := compose.NewGraph[string, any]()
g.AddLambdaNode("nilnode", compose.InvokableLambda(func(ctx context.Context, in string) (any, error) {
return nil, nil
}))
g.AddLambdaNode("mapnode", compose.InvokableLambda(func(ctx context.Context, in string) (any, error) {
return map[string]any{"k": "v"}, nil
}))
g.AddLambdaNode("sink", compose.InvokableLambda(func(ctx context.Context, in any) (any, error) {
return in, nil
}))
g.AddEdge(compose.START, "nilnode")
g.AddEdge(compose.START, "mapnode")
g.AddEdge("nilnode", "sink")
g.AddEdge("mapnode", "sink")
g.AddEdge("sink", compose.END)
r, _ := g.Compile(context.Background())
_, _ = r.Invoke(context.Background(), "x") // panics on some iterations
}
}
```

Unit-level, both forms reproduce deterministically:

```go
mergeValues([]any{nil, map[string]any{"k": "v"}}, nil) // panic
mergeValues([]any{map[string]any{"k": "v"}, nil}, nil) // field type mismatch error
mergeValues([]any{nil, nil}, nil) // panic
```

## Expected behavior

An untyped nil output carries no value: the merge should deterministically equal the merge of the remaining values (all-nil merges to nil), instead of panicking or erroring at random.

## Note

This is distinct from the runtime edge type check: an `any`-typed predecessor's nil output flowing into a `map[string]any`-typed successor fails the `assignableTypeMay` edge check with a clear, deterministic error — that behavior is intentional and unchanged. This issue is specifically about the fan-in merge of values that legitimately reached the merge step.

## Version

Current main (`9d983b36`). I have a fix with unit + graph-level regression tests ready and will open a PR shortly.

Contributor guide

Open the contributing guide

Research direction

Read compose/values_merge.go:40-41 and graph_run.go:112 first, then reproduce the unit cases and TestNilFanIn shown in the issue. Done means fan-in handling is deterministic for nil and non-nil values, all-nil merges return nil, and the unit and graph-level regression tests pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.