atenet: report "unknown" when a resume does not complete - #1482
Jeff Luo (JeffLuoo) merged 3 commits into
Conversation
1cbcde7 to
9546f66
Compare
|
|
||
| // NormalizeTemplateDimension ensures a template dimension (atespace or name) is | ||
| // non-empty, falling back to TemplateUnknown if unset. | ||
| func NormalizeTemplateDimension(dim string) string { |
There was a problem hiding this comment.
Nice, but it's still only called from the router though, and the group brief now promises unknown for all 7 metrics that ref these attrs.
There was a problem hiding this comment.
Thanks for catching it. This change would touch 7 metrics. I would prefer to do it in a follow-up PR for a cleaner chagne.
| // isDefinitiveResumeError reports whether err represents a failure where no cold | ||
| // activation could be attempted (e.g. the actor does not exist, bad request, or | ||
| // permission denied), as opposed to in-flight capacity or transient failures. | ||
| func isDefinitiveResumeError(err error) bool { | ||
| switch status.Code(err) { | ||
| case codes.NotFound, codes.InvalidArgument, codes.PermissionDenied, codes.Unauthenticated: | ||
| return true | ||
| default: | ||
| return false | ||
| } | ||
| } |
There was a problem hiding this comment.
Can we flip the default? Right now Internal, Unimplemented, Unknown etc. fall through to triggered, so an unrecognized code is as an activation in the latency series.
Unknown means we don't know, so unattempted seems safer.
Also wondering about FailedPrecondition, its registry brief is "the state of the actor did not permit a route" and with parking off it fails immediately, which sounds definitive. And retryable's comment just above already groups DeadlineExceeded with NotFound and PermissionDenied, so it's a bit weird to have two classifiers for the same codes I think.
Wdyt?
There was a problem hiding this comment.
Agreed on it. But I want to discuss more on the flipping bit.
If I flip it, I still need a list of codes that mean "an activation was in flight". This is not quite maintainable by looking at the code now.
My proposed change will be:
- Delete
isDefinitiveResumeError. This removes the classifier. - Make err == nil a condition for triggered and joined. A resume that fails reports the fourth value (added in this PR).
FailedPreconditionandResourceExhaustedboth go there. It also corrects the words "initiated cold activation" that you found. - Change the name
unattemptedtounknown. The value includes requests where an activation did operate. One example is a canceled leader. Its flight is separate from the request context, and it continues to restore the actor. A second example isDeadlineExceededduring a restore. Thusunattemptedis not true for these requests, and new words in the brief cannot correct this.
WDYT?
There was a problem hiding this comment.
Yes, to all of these, you can just make the brief say "resume didn't complete, can't say if an activation ran" or similar.
There was a problem hiding this comment.
Thanks. Updated all three I mentioned above.
| @@ -197,6 +212,9 @@ const ( | |||
| RouterResumeTriggered = "triggered" | |||
There was a problem hiding this comment.
initiated cold activation
a request that is ResourceExhausted gets triggered without activating anything. Same wording in the registry brief.
There was a problem hiding this comment.
Updated.
A request with ResourceExhausted now gets unknown, not triggered. Only a resume that completed can get trigger.
| No cold activation was in flight. The request was invalid, the | ||
| actor did not exist, the direction does not resume, or the | ||
| request canceled before activation was attempted. |
There was a problem hiding this comment.
"No cold activation was in flight" and "request canceled" contradict each other, a canceled joiner was waiting on someone else's activation. Maybe "this request neither attempted nor observed an activation" is more accurate?
There was a problem hiding this comment.
Agreed. The two statements cannot both be true.
The value has the new name unknown, and the brief no longer makes a statement about the activation.
| mock := &resumerMockClient{ | ||
| resumeFn: func(ctx context.Context, in *ateapipb.ResumeActorRequest, opts ...grpc.CallOption) (*ateapipb.ResumeActorResponse, error) { | ||
| mu.Lock() | ||
| resumeCalled++ |
There was a problem hiding this comment.
Should we assert this?
There was a problem hiding this comment.
Updated
9546f66 to
7cd3661
Compare
The router tagged every failed resume with ate.router.resume="none", the same label a warm route on an already-running actor gets. That put the failures into the warm-route series and hid them from the cold-start rate and the activation-time distribution. A gRPC code cannot say whether an activation ran. A canceled leader's flight outlives its request and keeps restoring the actor, and a DeadlineExceeded can land in the middle of a restore. So reserve "none", "triggered" and "joined" for a resume that completed, and add a fourth value, "unknown", for every resume that did not: a failed flight, a caller that went away, a caller shed by a full parking lot, and a direction that never resumes an actor. atenet.router.route.duration also sent an empty string for ate.template.atespace and ate.template.name when the router failed before it resolved a template. The registry marks both labels required. Send the constant "unknown" instead. The constant does not come from the request, thus it adds no caller-controlled label value. Update the metric registry, the cardinality rules and the observability guide for the new value and the template fallback.
7cd3661 to
dfdfc66
Compare
|
Krisztian F (@krisztianfekete) gentle ping |
Krisztian F (krisztianfekete)
left a comment
There was a problem hiding this comment.
Added one comment, the rest looks good to me now, thanks!
The test held the flight open until every caller had started, then slept 20 ms so each one could attach. A caller that had started but not yet reached the flight makes its own RPC once the first one completes, thus a slow runner failed the count on scheduling alone. Run the test in a synctest bubble. synctest.Wait returns only after every caller parks on the flight, so the release cannot race them.
The caller selects on its own context and on the flight's done channel. The mock returned at once, so both cases could be ready and the select picked one at random. The test then saw a triggered resume and no error. It failed within 5000 runs. Hold the flight open for the whole call, so only the cancellation is ever ready. A synctest bubble does not help here, because the race is the select, not the scheduling.
Fixes #1474
atenet.router.route.durationcarries two labels that the router filled in wrongly on a failure path.ate.router.resumesaid "none" for a failed resumenonemeans "the resume found the actor already running" — the warm route. The router also gavenoneto every failed resume, every canceled caller, and every caller shed by a full parking lot. Those failures landed in the warm-route series, so they did not show in the cold-start rate and they polluted the warm-route latency.A gRPC code cannot recover the fact. A canceled leader's flight outlives its request and keeps restoring the actor, and a
DeadlineExceededcan land in the middle of a restore. So the PR does not classify the error. It reservesnone,triggeredandjoinedfor a resume that completed, and adds a fourth value,unknown, for every resume that did not:nonenonetriggeredtriggeredjoinedjoinednoneunknownnoneunknownnoneunknownnoneunknownate.template.*was the empty string on a failureThe registry marks
ate.template.atespaceandate.template.namerequired on this metric, but the router sent an empty string when it failed before it resolved a template.ateattr.NormalizeTemplateDimensionnow sends the constant"unknown"instead. The constant does not come from the request, thus it adds no caller-controlled label value — the cardinality rule indocs/metrics/substrate.yamlstill holds, and the PR records the exception there.How to read the change on a dashboard
A query that already splits by
ate.router.resumegains anunknownseries and loses the failures that used to hide insidenone. Anonerate reads lower after the change and a cold-start rate is unaffected. Do not read the time in theunknownseries as an activation time.Documentation
docs/metrics/registry/metrics.yaml: theunknownmember ofate.router.resume, the"unknown"fallback on the two template labels, and a note on the metric.docs/metrics/substrate.yaml: the cardinality-rule exception.docs/observability.md: the label list for the metric.Tests
resumer_test.go: a failed flight givesunknownto the leader and to all nine joiners; a canceled caller givesunknown; a shed caller and a shed joiner giveunknown.metrics_test.go: empty template dimensions record as"unknown";Result.resume()defaults tounknown.ateattr_test.go:NormalizeTemplateDimensionand the fourRouterResume*values.Tests pass
Appropriate changes to documentation are included in the PR