Skip to main content

new-diagnostics-invariant

Step-by-step guide for adding a new invariant to Workflow Diagnostics (service/worker/diagnostics). Use when asked to create, extend, or wire up a diagnostics invariant that detects issues or risks in a workflow execution's history.

インストールへ移動

ソース情報

リポジトリ
cadence-workflow/cadence
ソースの最終更新活動
2026年9月9日 11:09
検出された SKILL.md の言語
英語
スター
9,442
フォーク
912

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
new-diagnostics-invariant
description
Step-by-step guide for adding a new invariant to Workflow Diagnostics (service/worker/diagnostics). Use when asked to create, extend, or wire up a diagnostics invariant that detects issues or risks in a workflow execution's history.
# Adding a Workflow Diagnostics Invariant Workflow Diagnostics (`service/worker/diagnostics`) reads one workflow execution's history and reports issues, root causes, and runbook links. Each check is an "invariant". Existing ones: `timeout`, `failure`, `retry`, `timeoutrisk`, `antipatterns`. Read the one closest to yours before writing anything. ## The interface Defined in `service/worker/diagnostics/invariant/interface.go`: - `Check(ctx, input)` — scan the history, return issues. The full event list is `input.WorkflowExecutionHistory.GetHistory().GetEvents()`. - `RootCause(ctx, input)` — explain the issues. Can be a no-op. - Each issue is an `InvariantCheckResult{IssueID, InvariantType, Reason, Metadata}`. `IssueID` starts at 0 and counts up within one `Check` call — it is not globally unique; `InvariantType` is what tells issues apart. `Metadata` is JSON, built with `invariant.MarshalData(...)`. Two templates to copy from: - **Pure history scan** → copy `invariant/retry/`: empty struct, `NewInvariant()` with no arguments, `RootCause` returns an empty non-nil slice with a comment saying the issues explain themselves. - **Needs live lookups** (e.g. `DescribeTaskList`) → copy `invariant/timeout/`: client passed in via `NewInvariant(Params{Client: ...})`, real `RootCause`. New root-cause categories are `RootCause` consts in `interface.go`, shared by all invariants. ## The new package: `service/worker/diagnostics/invariant/<name>/` Package name is one lowercase word (`timeout`, `retry`, `timeoutrisk`). **types.go** - `<Name>Type` string type + consts + `String()` — these become the `InvariantType` values. - `IssueType` string type + consts + `String()` — these become the `Reason` values. Each reason is a plain sentence ending with a period. Never build reasons with `fmt.Sprintf` — numbers belong in the metadata. - Thresholds are package consts, each with a comment saying what it means. - One `<Name>IssuesMetadata` struct per invariant, and `Check` marshals it directly. Fields shared by every check are top-level, and every issue sets a top-level `EventID` (the event the issue anchors on). Check-specific fields go in a per-check struct held by a pointer field named after the check, tagged `json:",omitempty"`, with exactly one set per issue (see `timeoutrisk.TimeoutRiskIssuesMetadata`). If all checks share one shape, skip the sub-structs (see `retry.RetryMetadata`). Never nest event IDs: cadence-web links only top-level `EventID`, `ActivityScheduledID`, `ActivityStartedID` keys and renders nested objects as one raw-JSON row. **<name>.go** - `type <Name> invariant.Invariant`, an unexported struct, `NewInvariant()`. - In `Check`: find the started event by scanning for a non-nil `GetWorkflowExecutionStartedEventAttributes()`, and handle it being missing. One flat loop over events. One `issueID` counter, +1 per issue. - Don't deduplicate by ActivityID — repeated scheduled events are separate invocations, and server-side retries don't write new scheduled events. **<name>_test.go** - Plain table-driven `Test__Check`. No testify/suite, no testify mocks — use uber-go/mock if a client is needed (see `timeout_test.go`). - Build fixtures by hand: `*types.GetWorkflowExecutionHistoryResponse{History: &types.History{Events: ...}}` with explicit event IDs. - Expected metadata is `json.Marshal` of the struct; expected reasons come from the `IssueType` consts. - Cover: each check firing alone, values right at each threshold, several issues at once (IssueIDs count up), a healthy workflow, and a history with no started event. ## Gotcha: history shows what the server saved, not what the client asked for The server rewrites decision attributes before saving them (`validateActivityScheduleAttributes` in `service/history/decision/checker.go`, plus siblings for child workflows and timers). For activities: - All four timeouts get capped at the workflow's total execution timeout. A "timeout > workflow timeout" check can never fire; a silently capped value shows up as exactly equal to the workflow timeout. - With a retry policy, ScheduleToStart and ScheduleToClose get raised up to that cap. Equality there is normal, not a mistake. Before writing any check on configured values, read the validator to see what the server rewrites. Also check the existing invariants so you don't repeat a check (`retry` already flags heartbeat >= StartToClose and expiration < initial interval). ## Wiring checklist (all required) 1. `service/worker/diagnostics/workflow.go`: - Add `<Name>s *<name>Diagnostics` to `DiagnosticsWorkflowResult`. - Add `<name>Diagnostics{Issues, Runbook}` and `<name>IssuesResult{IssueID, InvariantType, Reason, Metadata}` structs. Add a `RootCause` field only if `RootCause` does real work. - Add `retrieve<Name>Issues(checkResult)`: check membership in your `InvariantType` consts and unmarshal the metadata into `<Name>IssuesMetadata` (copy `retrieveTimeoutRiskIssues`). - In `DiagnosticsWorkflow`, add an `if len(issues) > 0 { ... Runbook: linkTo<Name>Runbook }` block and a field in the final result. 2. `service/worker/diagnostics/activities.go`: add `linkTo<Name>Runbook` (`https://cadenceworkflow.io/docs/workflow-troubleshooting/<topic>/`). The 10-issues-per-invariant cap applies on its own. 3. `service/worker/diagnostics/parent_workflow.go`: add an `issueType<Name>` const and a branch in `getIssueType` (same `-` joining as the others). 4. `cmd/server/cadence/server.go` (~line 306): add `<name>.NewInvariant(...)` to `params.DiagnosticsInvariants`, plus the import. 5. Existing tests: add the invariant to the lists in `workflow_test.go` `SetupTest` and `activities_test.go` `testDiagnosticWorkflow()`. Check first that existing fixtures don't set off your checks; update expectations if they do. Add `Test__retrieve<Name>Issues` to `workflow_test.go`, copying `Test__retrieveTimeoutRiskIssues`. ## Runbook page (cadence-docs repo) The `linkTo<Name>Runbook` URL has to exist eventually. Write the page in your local cadence-docs checkout as `docs/08-workflow-troubleshooting/<NN>-<topic>.md`, taking the next free number. `01-timeouts.md` on origin/master shows the style (the local checkout can lag — check against origin/master): - Frontmatter: `layout: default`, `title`, a one-sentence `description`, `keywords`, and `permalink: /docs/workflow-troubleshooting/<topic>` — must match the `linkTo<Name>Runbook` path. - No H1 in the body (the title comes from frontmatter). Start with a 2-3 sentence intro. - One `##` section per issue. The heading describes the situation ("Many activities scheduled in quick succession"), not a command. Then a paragraph on the cause, then the fixes: a single fix is a paragraph starting with `Mitigation:`; multiple fixes go under a `Mitigations:` line as a bulleted list, best fix first. - Internal doc links are relative paths like `/docs/concepts/activities#timeouts` — no domain, no trailing slash. Only external links (GitHub, pkg.go.dev) are absolute. - Point to built-in solutions first (`workflow.NewBatchFuture` for big fan-outs, Schedules instead of cron) before manual workarounds. - Describe thresholds loosely ("50 or more within a few seconds") so the page doesn't go stale when the consts are tuned. The page is a separate PR to cadence-workflow/cadence-docs. Until it merges, say in the server PR that the page doesn't exist yet. ## Verify ```bash go build ./service/worker/... ./cmd/server/... go test -race -count=1 ./service/worker/diagnostics/... make lint # no codegen needed — Invariant has no generated mock ``` Before the PR: `make pr GEN_DIR=service/worker/diagnostics`. PR title: `feat(diagnostics): ...`.
GitHubで見る