Skip to content

Commit dd77619

Browse files
committed
Refactor Nexus dispatch result classification
1 parent 5ed21eb commit dd77619

7 files changed

Lines changed: 1788 additions & 193 deletions

File tree

common/nexus/dispatch_outcome.go

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
package nexus
2+
3+
import (
4+
"github.com/nexus-rpc/sdk-go/nexus"
5+
commonpb "go.temporal.io/api/common/v1"
6+
failurepb "go.temporal.io/api/failure/v1"
7+
nexuspb "go.temporal.io/api/nexus/v1"
8+
"go.temporal.io/server/api/matchingservice/v1"
9+
"go.temporal.io/server/common/metrics"
10+
)
11+
12+
// DispatchOutcome names the arm of matching's DispatchNexusTaskResponse that came back from a
13+
// DispatchNexusTask call. The nested oneofs and the deprecated variants collapse into one flat set of
14+
// cases.
15+
//
16+
// The zero value is the empty string and is not a valid outcome, so a switch over a DispatchOutcome
17+
// needs a default clause. For metric tags use DispatchResult.OutcomeTag, not the string value.
18+
type DispatchOutcome string
19+
20+
const (
21+
// DispatchOutcomeUnrecognized is a response this build cannot interpret: no outcome set, an
22+
// outcome variant added after this build, or a worker answer that carries nothing usable.
23+
DispatchOutcomeUnrecognized DispatchOutcome = "unrecognized-outcome"
24+
25+
// DispatchOutcomeSyncSuccess means the worker ran the operation to completion inline.
26+
DispatchOutcomeSyncSuccess DispatchOutcome = "sync-success"
27+
28+
// DispatchOutcomeAsyncSuccess means the worker started the operation; the result arrives later,
29+
// out of band.
30+
DispatchOutcomeAsyncSuccess DispatchOutcome = "async-success"
31+
32+
// DispatchOutcomeCancelAccepted means the worker accepted the cancellation request.
33+
DispatchOutcomeCancelAccepted DispatchOutcome = "cancel-accepted"
34+
35+
// DispatchOutcomeOperationFailure means the worker ran the operation and it failed or was
36+
// canceled. The task was handled; this is the handler's answer, not a delivery problem.
37+
DispatchOutcomeOperationFailure DispatchOutcome = "operation-failure"
38+
39+
// DispatchOutcomeOperationFailureDeprecated is DispatchOutcomeOperationFailure as reported by a
40+
// worker predating Temporal failure responses.
41+
DispatchOutcomeOperationFailureDeprecated DispatchOutcome = "operation-failure-deprecated"
42+
43+
// DispatchOutcomeHandlerFailure means the worker refused the task with a Nexus handler error,
44+
// whose retry behavior says whether another attempt is worthwhile.
45+
DispatchOutcomeHandlerFailure DispatchOutcome = "nexus-handler-failure"
46+
47+
// DispatchOutcomeWorkerFailure means the worker failed the task with a failure that is not a
48+
// Nexus handler error, e.g. an application error sent via RespondNexusTaskFailed.
49+
DispatchOutcomeWorkerFailure DispatchOutcome = "worker-failure"
50+
51+
// DispatchOutcomeHandlerFailureDeprecated is DispatchOutcomeHandlerFailure as reported by a worker
52+
// predating Temporal failure responses.
53+
DispatchOutcomeHandlerFailureDeprecated DispatchOutcome = "nexus-handler-failure-deprecated"
54+
55+
// DispatchOutcomeRequestTimeout means matching gave up before the task was answered: no worker
56+
// was polling the task queue, or a worker took the task and never responded.
57+
DispatchOutcomeRequestTimeout DispatchOutcome = "request-timeout"
58+
)
59+
60+
// Succeeded reports whether the worker accepted the request. An asynchronous start counts: the worker
61+
// took responsibility for the operation, even though it has not finished it.
62+
func (o DispatchOutcome) Succeeded() bool {
63+
switch o {
64+
case DispatchOutcomeSyncSuccess, DispatchOutcomeAsyncSuccess, DispatchOutcomeCancelAccepted:
65+
return true
66+
default:
67+
return false
68+
}
69+
}
70+
71+
// DispatchResult is the classified form of a DispatchNexusTaskResponse: the outcome, plus whatever
72+
// that arm of the response carried, hoisted out of the nested oneofs.
73+
//
74+
// Exactly one field group is populated, determined by Outcome. Everything else is nil or empty.
75+
type DispatchResult struct {
76+
Outcome DispatchOutcome
77+
78+
// SyncPayload is the operation's result. Set for DispatchOutcomeSyncSuccess, where it may still
79+
// be nil: an operation is allowed to succeed with no value.
80+
SyncPayload *commonpb.Payload
81+
82+
// OperationToken is set for DispatchOutcomeAsyncSuccess, with the deprecated operation ID
83+
// already folded in.
84+
OperationToken string
85+
86+
// Links are the handler links the worker attached to a successful start. Set for
87+
// DispatchOutcomeSyncSuccess and DispatchOutcomeAsyncSuccess.
88+
Links []*nexuspb.Link
89+
90+
// Failure is the Temporal failure the worker reported. Set for DispatchOutcomeHandlerFailure,
91+
// DispatchOutcomeWorkerFailure and DispatchOutcomeOperationFailure.
92+
//
93+
// This aliases the proto inside the response rather than copying it, so callers that convert it
94+
// in place mutate the response too.
95+
Failure *failurepb.Failure
96+
97+
// HandlerError is set only for DispatchOutcomeHandlerFailureDeprecated.
98+
HandlerError *nexuspb.HandlerError
99+
100+
// OperationError is set only for DispatchOutcomeOperationFailureDeprecated.
101+
OperationError *nexuspb.UnsuccessfulOperationError
102+
}
103+
104+
// handlerErrorType returns the Nexus handler error type the worker reported, or "" when the outcome is
105+
// not a handler error.
106+
func (r DispatchResult) handlerErrorType() string {
107+
switch r.Outcome {
108+
case DispatchOutcomeHandlerFailure:
109+
return r.Failure.GetNexusHandlerFailureInfo().GetType()
110+
case DispatchOutcomeHandlerFailureDeprecated:
111+
//nolint:staticcheck // Deprecated field on a deprecated variant.
112+
return r.HandlerError.GetErrorType()
113+
default:
114+
return ""
115+
}
116+
}
117+
118+
// ClassifyStartOperationDispatch classifies matching's response to a dispatched StartOperation task.
119+
func ClassifyStartOperationDispatch(resp *matchingservice.DispatchNexusTaskResponse) DispatchResult {
120+
return classifyDispatchNexusTaskResponse(resp, classifyStartOperationResponse)
121+
}
122+
123+
// ClassifyCancelOperationDispatch classifies matching's response to a dispatched CancelOperation task.
124+
func ClassifyCancelOperationDispatch(resp *matchingservice.DispatchNexusTaskResponse) DispatchResult {
125+
return classifyDispatchNexusTaskResponse(
126+
resp,
127+
func(*nexuspb.StartOperationResponse) DispatchResult {
128+
// A cancel response carries no fields, so any response means the worker accepted.
129+
return DispatchResult{Outcome: DispatchOutcomeCancelAccepted}
130+
})
131+
}
132+
133+
// classifyDispatchNexusTaskResponse converts a DispatchNexusTaskResponse into a DispatchResult object.
134+
func classifyDispatchNexusTaskResponse(
135+
resp *matchingservice.DispatchNexusTaskResponse,
136+
onResponseFn func(*nexuspb.StartOperationResponse) DispatchResult,
137+
) DispatchResult {
138+
switch t := resp.GetOutcome().(type) {
139+
case *matchingservice.DispatchNexusTaskResponse_Failure:
140+
// A handler error is a Nexus-level refusal whose retry behavior is meaningful; anything else
141+
// is an arbitrary failure the worker chose to report.
142+
outcome := DispatchOutcomeWorkerFailure
143+
if t.Failure.GetNexusHandlerFailureInfo() != nil {
144+
outcome = DispatchOutcomeHandlerFailure
145+
}
146+
return DispatchResult{Outcome: outcome, Failure: t.Failure}
147+
148+
case *matchingservice.DispatchNexusTaskResponse_HandlerError: //nolint:staticcheck // Deprecated, still sent by older workers.
149+
return DispatchResult{
150+
Outcome: DispatchOutcomeHandlerFailureDeprecated,
151+
//nolint:staticcheck // Deprecated field on a deprecated variant.
152+
HandlerError: t.HandlerError,
153+
}
154+
155+
case *matchingservice.DispatchNexusTaskResponse_RequestTimeout:
156+
return DispatchResult{Outcome: DispatchOutcomeRequestTimeout}
157+
158+
case *matchingservice.DispatchNexusTaskResponse_Response:
159+
// How we handle the "Response" field depends on the context. (i.e. if the Nexus task was to
160+
// start a new operation or cancel an existing one.)
161+
return onResponseFn(t.Response.GetStartOperation())
162+
163+
default:
164+
return DispatchResult{Outcome: DispatchOutcomeUnrecognized}
165+
}
166+
}
167+
168+
// classifyStartOperationResponse classifies the answer a worker gave to a StartOperation request.
169+
func classifyStartOperationResponse(resp *nexuspb.StartOperationResponse) DispatchResult {
170+
switch t := resp.GetVariant().(type) {
171+
case *nexuspb.StartOperationResponse_SyncSuccess:
172+
return DispatchResult{
173+
Outcome: DispatchOutcomeSyncSuccess,
174+
SyncPayload: t.SyncSuccess.GetPayload(),
175+
Links: t.SyncSuccess.GetLinks(),
176+
}
177+
178+
case *nexuspb.StartOperationResponse_AsyncSuccess:
179+
token := t.AsyncSuccess.GetOperationToken()
180+
if token == "" {
181+
// Workers predating the operation-token rename only set the operation ID.
182+
//nolint:staticcheck // Deprecated, still sent by older workers.
183+
token = t.AsyncSuccess.GetOperationId()
184+
}
185+
return DispatchResult{
186+
Outcome: DispatchOutcomeAsyncSuccess,
187+
OperationToken: token,
188+
Links: t.AsyncSuccess.GetLinks(),
189+
}
190+
191+
case *nexuspb.StartOperationResponse_Failure:
192+
return DispatchResult{
193+
Outcome: DispatchOutcomeOperationFailure,
194+
Failure: t.Failure,
195+
}
196+
197+
case *nexuspb.StartOperationResponse_OperationError: //nolint:staticcheck // Deprecated, still sent by older workers.
198+
return DispatchResult{
199+
Outcome: DispatchOutcomeOperationFailureDeprecated,
200+
//nolint:staticcheck // Deprecated field on a deprecated variant.
201+
OperationError: t.OperationError,
202+
}
203+
204+
default:
205+
return DispatchResult{Outcome: DispatchOutcomeUnrecognized}
206+
}
207+
}
208+
209+
// OutcomeTag returns the metrics outcome tag for a dispatch.
210+
//
211+
// The handler-error suffix is bounded by boundHandlerErrorType(). A worker failure that is not a handler
212+
// error or has a non-spec type will be reported as "handler_error:UNKNOWN".
213+
func (r DispatchResult) OutcomeTag() metrics.Tag {
214+
return metrics.OutcomeTag(r.metricOutcome())
215+
}
216+
217+
func (r DispatchResult) metricOutcome() string {
218+
// NOTE: Some of these are confusing (e.g. "success" for CancelAccepted), but
219+
// changing these would break existing dashboards.
220+
switch r.Outcome {
221+
case DispatchOutcomeSyncSuccess:
222+
return "sync_success"
223+
case DispatchOutcomeAsyncSuccess:
224+
return "async_success"
225+
case DispatchOutcomeCancelAccepted:
226+
return "success"
227+
case DispatchOutcomeOperationFailure:
228+
return "failure"
229+
case DispatchOutcomeOperationFailureDeprecated:
230+
return "operation_error"
231+
case DispatchOutcomeHandlerFailure,
232+
DispatchOutcomeWorkerFailure,
233+
DispatchOutcomeHandlerFailureDeprecated:
234+
// A worker failure has no handler error type to report and will map to UNKNOWN.
235+
return "handler_error:" + boundHandlerErrorType(r.handlerErrorType())
236+
case DispatchOutcomeRequestTimeout:
237+
return "handler_timeout"
238+
default:
239+
return "handler_error:EMPTY_OUTCOME"
240+
}
241+
}
242+
243+
// handlerErrorTypes are the handler error types that may appear verbatim in a metric tag.
244+
// Keep in sync with the HandlerErrorType consts in nexus-rpc/sdk-go/nexus/errors.go.
245+
var handlerErrorTypes = map[string]struct{}{
246+
string(nexus.HandlerErrorTypeBadRequest): {},
247+
string(nexus.HandlerErrorTypeUnauthenticated): {},
248+
string(nexus.HandlerErrorTypeUnauthorized): {},
249+
string(nexus.HandlerErrorTypeNotFound): {},
250+
string(nexus.HandlerErrorTypeRequestTimeout): {},
251+
string(nexus.HandlerErrorTypeConflict): {},
252+
string(nexus.HandlerErrorTypeResourceExhausted): {},
253+
string(nexus.HandlerErrorTypeInternal): {},
254+
string(nexus.HandlerErrorTypeNotImplemented): {},
255+
string(nexus.HandlerErrorTypeUnavailable): {},
256+
string(nexus.HandlerErrorTypeUpstreamTimeout): {},
257+
}
258+
259+
// boundHandlerErrorType bounds the metric cardinality a worker can introduce through a handler error
260+
// type. Types in the Nexus spec pass through; anything else, including the empty string, collapses to
261+
// UNKNOWN.
262+
func boundHandlerErrorType(errType string) string {
263+
if _, ok := handlerErrorTypes[errType]; ok {
264+
return errType
265+
}
266+
return "UNKNOWN"
267+
}

0 commit comments

Comments
 (0)