Skip to content

Error-handling cleanup: surface swallowed failures, branch on values not strings#411

Open
lilydoar wants to merge 6 commits into
mainfrom
lilydoar/go-error-handling
Open

Error-handling cleanup: surface swallowed failures, branch on values not strings#411
lilydoar wants to merge 6 commits into
mainfrom
lilydoar/go-error-handling

Conversation

@lilydoar

@lilydoar lilydoar commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

What

A focused error-handling cleanup across the omes Go code, prompted by an audit against our Go error-handling conventions (don't drop errors, log XOR return, branch on values not strings, propagate at boundaries). Each fix is its own commit. Scope was limited to High and Medium severity findings — bugs and correctness issues — not the broad stylistic layer (noise-prefix messages, capitalized error strings).

Changes (one commit each)

1. Return dropped search-attribute registration error (scenarios/ebb_and_flow.go)
spawnWorkflowWithActivities called RegisterDefaultSearchAttributes(ctx) and ignored its error. A registration failure was lost and only surfaced later as an obscure workflow-start error. The sibling caller in generic_executor.go already checks this return value.

2. Surface failed ebbAndFlowTrack workflows instead of swallowing them (scenarios/ebb_and_flow.go)
wf.Get failures were logged and then return nil, so they never reached the MaxConsecutiveErrors guard, and a failed workflow was still counted as completed — inflating TotalCompletedWorkflows, which post-scenario verification relies on. Now the error is returned and the completion count is gated on success. The backlog still drains on either outcome, so the rate controller's behavior is unchanged.

3. Fix dropped terminate error and pointer log (loadgen/scenario.go)
In ExecuteKitchenSinkWorkflow's client-actions goroutine, the failure log printed the atomic.Pointer[error] value (an address) rather than the error, and the result of the cleanup TerminateWorkflow call was silently discarded. Now it logs the actual error, and logs the terminate failure (the goroutine has no return path to propagate it).

4. Stop double-handling terminal iteration errors (loadgen/generic_executor.go)
The per-iteration goroutine logged a terminal failure and sent it on doneCh, where Run returns it wrapped for its caller to report — the same error handled twice. The terminal branch now propagates only. The retry-branch log is kept on purpose: that error is superseded by the retry and never returned, so the log is its only record.

5. Branch on gRPC status code, not error string (loadgen/scenario.go)
RegisterDefaultSearchAttributes decided whether to ignore an "already exists" failure via strings.Contains(err.Error(), ...). Replaced with status.Code(err) == codes.AlreadyExists, matching ensureNexusEndpoint in the same file. The second match string was a typo ("unavailble") that could never match the real message, so no working behavior is lost.

6. Wrap cert-load error with %w (clioptions/client.go)
loadTLSConfig used %s to format the cert-load error, severing the unwrap chain at this library-ish boundary. Switched to %w, matching the two sibling wraps in the same function.

Testing

  • go build clean across all first-party packages.
  • go test ./clioptions/ ./scenarios/ — pass.
  • go test ./loadgen/ worker-free unit tests (cover the generic_executor change) — pass.
  • SDK=go go test ./loadgen/ -run TestKitchenSink — pass (ok … 25.5s); exercises the ExecuteKitchenSinkWorkflow and RegisterDefaultSearchAttributes paths against a real devserver.

Note: a no-SDK go test ./loadgen/ fails locally because TestKitchenSink runs the full SDK matrix (Go/Java/Python/Ruby/TS/.NET) and only the Go worker toolchain is built here. That's environmental and pre-existing — CI sets SDK per the test's own skip logic.

lilydoar added 6 commits June 25, 2026 15:42
spawnWorkflowWithActivities ignored the error from
RegisterDefaultSearchAttributes, so a registration failure was silently
lost and surfaced later as an obscure workflow-start failure.
spawnWorkflowWithActivities logged wf.Get failures and returned nil, so
they never reached the MaxConsecutiveErrors guard and a failed workflow
was still counted as completed (inflating TotalCompletedWorkflows, which
post-scenario verification relies on). Return the error and gate the
completion count on success; the backlog still drains either way so the
controller's behavior is unchanged.
…flow

The client-actions goroutine logged the atomic.Pointer rather than the
error value, and silently dropped the result of the cleanup
TerminateWorkflow call. Log the actual error, and log the terminate
failure (the goroutine has no return path for it).
The per-iteration goroutine logged the terminal failure and also sent it
on doneCh, where Run returns it wrapped for its caller to report. Drop
the goroutine log for the terminal branch (propagate only); keep the
retry-branch log, whose transient error is superseded and never
returned.
…hAttributes

Replace strings.Contains(err.Error(), ...) with status.Code(err) ==
codes.AlreadyExists, matching ensureNexusEndpoint in the same file. The
second match string was a typo ("unavailble") that never matched, so no
working behavior is lost.
Preserve the error chain at this library boundary with %w, matching the
sibling wraps in the same function (lines 93, 118).
@lilydoar lilydoar requested review from a team as code owners June 26, 2026 18:20
@lilydoar lilydoar enabled auto-merge (squash) June 26, 2026 21:32
Comment thread loadgen/scenario.go
Comment on lines +309 to +312
// Throw an error if the attributes could not be registered, but ignore the
// case where they already exist (re-registration on a reused namespace).
if err != nil && status.Code(err) != codes.AlreadyExists {
return fmt.Errorf("failed to register search attributes: %w", err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had assumed that there maybe were cases (i.e. old server versions) where we don't return codes.AlreadyExists, hence the string matching.

@stephanos I think you wrote this? Are we ok to do away with the string matching now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing worth flagging regardless of the codes.AlreadyExists decision: the second string in the old list, "attributes mapping unavailble", is a typo — the real server message is ...unavailable. So strings.Contains never matched it, meaning only the "already exists" branch was ever actually doing anything. Removing that second string drops no working behavior.

Comment thread scenarios/ebb_and_flow.go
return fmt.Errorf("ebbAndFlowTrack workflow failed for iteration %d: %w", iteration, err)
}
e.completedActivities.Add(activities)
e.incrementTotalCompletedWorkflow()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is desired.

I don't have the context here (I think @dnr wrote this scenario), but it could be intended to continue through failed workflows.

Comment thread scenarios/ebb_and_flow.go
Comment on lines -284 to +286
e.RegisterDefaultSearchAttributes(ctx)
if err := e.RegisterDefaultSearchAttributes(ctx); err != nil {
return fmt.Errorf("failed to register search attributes for iteration %d: %w", iteration, err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise here, unsure if the preference would be to log or to stop the scenario with an error

Comment thread loadgen/scenario.go
Comment on lines -435 to +425
err := r.Client.TerminateWorkflow(
ctx, options.StartOptions.ID, "", "client actions failed", nil)
if err != nil {
return
if terminateErr := r.Client.TerminateWorkflow(
ctx, options.StartOptions.ID, "", "client actions failed", nil); terminateErr != nil {
r.Logger.Errorf("Failed to terminate workflow after client actions failure: %v", terminateErr)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we log here instead of return ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block runs inside the go func() client-actions goroutine, so a bare return would just exit the goroutine and silently drop the terminate error — which is exactly what the old code did (if err != nil { return }). There's no error channel back to ExecuteKitchenSinkWorkflow.

The error we actually care about — the client-actions failure — does get propagated: it's stored in clientActionsErrPtr (line 418) and returned by the main path at line 435. The TerminateWorkflow call is best-effort cleanup after that failure has already happened, so its result is secondary

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants