Skip to content

feat(temporal): detect Go in-workflow query/signal/update handler declarations#78

Merged
zzet merged 2 commits into
zzet:mainfrom
avfirsov:feat/temporal-workflow-handlers
Jun 12, 2026
Merged

feat(temporal): detect Go in-workflow query/signal/update handler declarations#78
zzet merged 2 commits into
zzet:mainfrom
avfirsov:feat/temporal-workflow-handlers

Conversation

@avfirsov

Copy link
Copy Markdown
Contributor

Summary

Surface Go-side Temporal in-workflow handler declarations (query / signal / update) as graph edges, symmetric with the Java side's existing @QueryMethod / @SignalMethod / @UpdateMethod annotation edges.

This is one half of a split from #75 (the other half — env-default dispatch names — is in a separate PR so each can be reviewed independently).

Changes

  • Recognises workflow.SetQueryHandler / workflow.GetSignalChannel / workflow.SetUpdateHandler[WithOptions] and emits a via=temporal.handler EdgeCalls edge from the enclosing workflow function, carrying temporal_kind (query / signal / update) + temporal_name (the handler's string name).
  • Mirrors the existing via=temporal.register emission (applyGoTemporalRegisterMeta) exactly — same edge shape, same stamping site — so the graph gains a queryable, per-workflow record of the named handlers a Go workflow exposes.
  • High-precision, consistent with the existing dispatch detector: only the canonical workflow receiver alias matches, and the handler name must be a string literal (query/signal/update names are matched by string at runtime, so a non-literal can't be pinned).

Testing

  • All tests pass (go test -race ./...) for the touched packages (internal/parser/languages, internal/indexer). The only failures in my environment are the pre-existing internal/indexer watcher tests failing with inotify ... too many open files (a local WSL fd-limit issue, unrelated — no watcher files touched).
  • New tests added — parser-level (go_temporal_test.go: query/signal/update detection + negative cases for non-literal name and aliased import) and end-to-end (temporal_e2e_test.go).
  • Benchmarks — n/a, the detector is a per-call AST check on the existing walk.

Checklist

  • Code follows existing patterns in the codebase (mirrors applyGoTemporalRegisterMeta, the canonical-alias-only stance of the dispatch detector)
  • No unnecessary abstractions added
  • Language extractor includes Meta["methods"] for interfaces (if applicable) — n/a
  • Methods have EdgeMemberOf edges to their containing type (if applicable) — n/a

Notes

The natural consumer of this data is the cross-language Java→Go match (a Java @QueryMethod(name="status") consumer ↔ this Go SetQueryHandler(ctx, "status", …) provider), which is proposed for design discussion in #77 — out of scope here; this PR only adds the Go-side provider edges.

…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>
…nnelWithOptions

Complete the in-workflow handler set: the detector already handled
SetUpdateHandlerWithOptions but missed the WithOptions variants of the
query and signal handlers. Note: the Go SDK has no SetSignalHandler —
signals are received via GetSignalChannel[WithOptions].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
// Temporal in-workflow handler declaration:
// `workflow.SetQueryHandler(ctx, "name", fn)` etc.
if name := goTemporalHandlerName(expr.Node, src); name != "" {
dc.tempHandlerKind = hkind

@zzet zzet Jun 12, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@avfirsov I need to think about it. My main concern for now: why not do the same as with the Register kind?

dc.tempKind = "register_" + kind`

In such a case, it could be

dc.tempKind = "handle_" + kind`

without exploding the schema.

I'll take a look at other PRs and return to this point later. There might be benefits for resolution purposes, so I'm not sure atm.

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.

Good question — and you're right that the goDeferredCall field is the part worth trimming. Let me separate the two axes, because I think they answer your "benefits for resolution?" doubt:

The via value is the real schema; the struct field is just a parser-internal carrier. The reason I didn't fold this into tempKind is that handler declarations sit on a different axis than dispatch/register:

  • temporal.stub (ExecuteActivity) and temporal.register are about the activity/workflow namespace, and temporal.stub edges get rewritten by ResolveTemporalCalls (placeholder → real function). The tempKind values (activity / workflow / register_activity / register_workflow) all live on that one axis and feed that one resolver pass.
  • temporal.handler is the query/signal/update namespace and is not rewritten — it's pure provider metadata (“this workflow serves query status”), the Go-side counterpart of Java's @QueryMethod/@SignalMethod/@UpdateMethod annotation edges. A distinct via lets a consumer ask "all handlers" (via == "temporal.handler") without decoding a handle_* prefix out of tempKind, and keeps the dispatch-intercept condition (tempKind == "activity" || "workflow") untouched.

So I'd keep the distinct via=temporal.handler (different namespace, different resolver treatment), but I fully agree about not adding the field. I can drop tempHandlerKind and carry it as tempKind = "handle_" + kind, with applyGoTemporalHandlerMeta routing on that prefix — exactly mirroring register_. Same for the tempOutKind field in #81 (signalout_/querycall_). That removes the schema growth you're flagging while preserving the semantic/via distinction.

No rush — happy to push that refactor whenever you've settled on the direction (or leave it as-is if you decide the separate field reads cleaner). Thanks for taking the time across the three PRs.

zzet added a commit that referenced this pull request Jun 12, 2026
Temporal cluster: merge #78/#79/#81 + cross-language Java→Go join (#77) + const retention (#80)
@zzet zzet merged commit 0e00c0a into zzet:main Jun 12, 2026
9 checks passed
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