Skip to content

feat(temporal): Go in-workflow query/signal/update handlers + env-default dispatch names#75

Closed
avfirsov wants to merge 3 commits into
zzet:mainfrom
avfirsov:feat/temporal-signal-query-and-env-default
Closed

feat(temporal): Go in-workflow query/signal/update handlers + env-default dispatch names#75
avfirsov wants to merge 3 commits into
zzet:mainfrom
avfirsov:feat/temporal-signal-query-and-env-default

Conversation

@avfirsov

@avfirsov avfirsov commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Two small, independent additions to the Temporal Go extractor/resolver, each mirroring an existing pattern in the codebase. They close two gaps in the Go-side Temporal surface; the larger cross-language Java→Go bridge is proposed separately for design discussion in #77 (not in this PR).

The two commits are independent and can be reviewed / merged / split separately.

Changes

1. Detect in-workflow query/signal/update handler declarations (feat(temporal): detect Go in-workflow query/signal/update handler declarations)

  • Recognises workflow.SetQueryHandler / GetSignalChannel / SetUpdateHandler[WithOptions] and emits a via=temporal.handler EdgeCalls edge from the enclosing workflow carrying temporal_kind (query/signal/update) + temporal_name.
  • Mirrors the existing via=temporal.register / via=temporal.stub emission exactly, and gives the graph a symmetric, queryable record of the named handlers a Go workflow exposes — the Go counterpart to the Java side's @QueryMethod / @SignalMethod / @UpdateMethod annotation edges.
  • High-precision: only the canonical workflow receiver alias matches and the handler name must be a string literal (runtime-matched names can't be pinned from a variable), consistent with the existing dispatch detector.

2. Resolve activity/workflow names from env-var-with-default variables (feat(temporal): resolve activity/workflow names from env-var-with-default vars)

  • When a dispatch name is a local variable read from an env var with a literal fallback, resolves the call to that literal default instead of leaving it unresolved:
    actName := cmp.Or(os.Getenv("CHARGE_ACTIVITY"), "ChargeCard")
    workflow.ExecuteActivity(ctx, actName, id)   // → activity "ChargeCard"
    
    name := os.Getenv("K"); if name == "" { name = "ChargeCard" }
  • The lookup is narrow and intra-procedural, anchored on a literal os.Getenv / os.LookupEnv read (so the value is provably env-sourced — not general constant propagation), and only considers assignments lexically before the call.
  • The resolved edge lands at the speculative tier (OriginSpeculative, confidence 0.4, MetaSpeculative=true) rather than ast_resolved — the runtime env override may name a different handler than the literal default, so the edge is present but hidden from default queries, reusing the existing speculative-edge convention.

Testing

  • All tests pass (go test -race ./...) — for the touched packages (internal/parser/languages, internal/resolver, internal/indexer temporal suites). The only failures in my environment are the internal/indexer watcher tests failing with inotify ... too many open files (a WSL fd-limit issue, unrelated — no watcher files are touched).
  • New tests added for new functionality — parser-level (go_temporal_test.go), resolver-level (temporal_calls_test.go), and end-to-end (temporal_e2e_test.go) for both features, including negative cases (non-literal handler name, plain non-env variable, env read with no literal default, unresolved-stays-placeholder).
  • Benchmarks run if performance-relevant — both passes are AST-local / reuse existing graph walks; no new whole-graph scans.

Checklist

  • Code follows existing patterns in the codebase (mirrors applyGoTemporalRegisterMeta, the speculative-dispatch tier, the existing detector's high-precision stance)
  • No unnecessary abstractions added
  • Language extractor includes Meta["methods"] for interfaces (if applicable) — n/a, no interface extraction changed
  • Methods have EdgeMemberOf edges to their containing type (if applicable) — n/a

Notes for reviewers

  • Both features are deliberately scoped to the Go side only. The cross-language join (linking a Java @WorkflowInterface start to the Go workflow / a Java @QueryMethod to the Go SetQueryHandler provider added here) raises edge-model and confidence-tier design questions, so it's filed as Cross-language Java→Go Temporal linking (starts_temporal_workflow by canonical name) #77 rather than bundled here.
  • The aliased-import limitation (only the canonical workflow receiver is matched) is preserved and pinned by a test, consistent with the existing dispatch detector.

avfirsov and others added 2 commits June 12, 2026 01:37
…larations

Surface workflow.SetQueryHandler / GetSignalChannel / SetUpdateHandler[WithOptions]
calls as via=temporal.handler EdgeCalls edges carrying temporal_kind
(query/signal/update) + temporal_name, originating from the enclosing
workflow function. This mirrors the Java side's per-method @QueryMethod /
@SignalMethod / @UpdateMethod annotation edges, giving the graph a
symmetric, queryable record of the named handlers each Go workflow exposes.

High-precision: only the canonical "workflow" receiver alias is matched and
the handler name must be a string literal (runtime-matched names can't be
pinned from a variable), consistent with the existing dispatch detector.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ault vars

When a workflow names its activity/child-workflow through a local variable
read from an env var with a literal fallback, resolve the dispatch to that
literal default instead of leaving it unresolved:

    actName := cmp.Or(os.Getenv("CHARGE_ACTIVITY"), "ChargeCard")
    workflow.ExecuteActivity(ctx, actName, id)   // -> activity "ChargeCard"

    name := os.Getenv("K"); if name == "" { name = "ChargeCard" }

The parser does a narrow, intra-procedural lookup anchored on a literal
os.Getenv / os.LookupEnv read (so the value is provably env-sourced, not a
general data-flow guess) and tags the stub edge temporal_name_origin=
env_default. The resolver then lands the edge at the speculative tier
(OriginSpeculative, confidence 0.4, MetaSpeculative=true) rather than
ast_resolved — the runtime env override may name a different handler than
the literal default, so the edge is present but hidden from default queries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The G4 refactor routed dispatch-name extraction through
goTemporalDispatchArg + goTemporalNameFromExpr, leaving
goTemporalDispatchName with no call sites. golangci-lint's 'unused'
linter (not caught by local 'go vet') flagged it. Fold its doc into
goTemporalDispatchArg and remove the dead wrapper.

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

Copy link
Copy Markdown
Contributor Author

Split into two focused PRs so each can be reviewed/merged independently:

Closing this combined PR in favour of those two.

@avfirsov avfirsov closed this Jun 12, 2026
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.

1 participant