Skip to content

atenet: report "unknown" when a resume does not complete - #1482

Merged
Jeff Luo (JeffLuoo) merged 3 commits into
agent-substrate:mainfrom
JeffLuoo:fix/issue-1474-router-route-duration-labels
Sep 22, 2026
Merged

Jeff Luo (JeffLuoo) merged 3 commits into
agent-substrate:mainfrom
JeffLuoo:fix/issue-1474-router-route-duration-labels

Conversation

@JeffLuoo

@JeffLuoo Jeff Luo (JeffLuoo) commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1474

atenet.router.route.duration carries two labels that the router filled in wrongly on a failure path.

ate.router.resume said "none" for a failed resume

none means "the resume found the actor already running" — the warm route. The router also gave none to 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 DeadlineExceeded can land in the middle of a restore. So the PR does not classify the error. It reserves none, triggered and joined for a resume that completed, and adds a fourth value, unknown, for every resume that did not:

Case Before After
Actor already running none none
Leader completed a cold activation triggered triggered
Joiner waited on a completed cold activation joined joined
Resume failed (leader and joiners) none unknown
Caller canceled before the flight finished none unknown
Caller shed by a full parking lot none unknown
Egress, which never resumes an actor none unknown

ate.template.* was the empty string on a failure

The registry marks ate.template.atespace and ate.template.name required on this metric, but the router sent an empty string when it failed before it resolved a template. ateattr.NormalizeTemplateDimension now sends the constant "unknown" instead. The constant does not come from the request, thus it adds no caller-controlled label value — the cardinality rule in docs/metrics/substrate.yaml still holds, and the PR records the exception there.

How to read the change on a dashboard

A query that already splits by ate.router.resume gains an unknown series and loses the failures that used to hide inside none. A none rate reads lower after the change and a cold-start rate is unaffected. Do not read the time in the unknown series as an activation time.

Documentation

  • docs/metrics/registry/metrics.yaml: the unknown member of ate.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 gives unknown to the leader and to all nine joiners; a canceled caller gives unknown; a shed caller and a shed joiner give unknown.

  • metrics_test.go: empty template dimensions record as "unknown"; Result.resume() defaults to unknown.

  • ateattr_test.go: NormalizeTemplateDimension and the four RouterResume* values.

  • Tests pass

  • Appropriate changes to documentation are included in the PR

Comment thread internal/ateattr/ateattr.go
Comment thread cmd/atenet/internal/router/ingress/resumer.go Outdated
Comment thread cmd/atenet/internal/router/ingress/resumer.go Outdated
Comment thread docs/metrics/registry/metrics.yaml Outdated
Comment thread cmd/atenet/internal/router/ingress/resumer_test.go Outdated

// NormalizeTemplateDimension ensures a template dimension (atespace or name) is
// non-empty, falling back to TemplateUnknown if unset.
func NormalizeTemplateDimension(dim string) string {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

Comment on lines +164 to +174
// 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
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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:

  1. Delete isDefinitiveResumeError. This removes the classifier.
  2. Make err == nil a condition for triggered and joined. A resume that fails reports the fourth value (added in this PR). FailedPrecondition and ResourceExhausted both go there. It also corrects the words "initiated cold activation" that you found.
  3. Change the name unattempted to unknown. 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 is DeadlineExceeded during a restore. Thus unattempted is not true for these requests, and new words in the brief cannot correct this.

WDYT?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks. Updated all three I mentioned above.

Comment thread internal/ateattr/ateattr.go Outdated
Comment on lines 211 to 212
@@ -197,6 +212,9 @@ const (
RouterResumeTriggered = "triggered"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

initiated cold activation

a request that is ResourceExhausted gets triggered without activating anything. Same wording in the registry brief.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated.

A request with ResourceExhausted now gets unknown, not triggered. Only a resume that completed can get trigger.

Comment thread docs/metrics/registry/metrics.yaml Outdated
Comment on lines +434 to +436
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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

"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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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++

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we assert this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated

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.
@JeffLuoo
Jeff Luo (JeffLuoo) force-pushed the fix/issue-1474-router-route-duration-labels branch from 7cd3661 to dfdfc66 Compare September 18, 2026 13:51
@JeffLuoo Jeff Luo (JeffLuoo) changed the title atenet: preserve cold resume outcome and fallback empty template dimension atenet: report "unknown" when a resume does not complete Sep 18, 2026
@JeffLuoo

Copy link
Copy Markdown
Collaborator Author

Krisztian F (@krisztianfekete) gentle ping

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Added one comment, the rest looks good to me now, thanks!

Comment thread cmd/atenet/internal/router/ingress/resumer_test.go
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.
@JeffLuoo
Jeff Luo (JeffLuoo) added this pull request to the merge queue Sep 22, 2026
Merged via the queue into agent-substrate:main with commit b374862 Sep 22, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/network area/observability kind/bug Something isn't working / bugfixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: atenet.router.route.duration mislabels failed cold resumes as "none" and emits empty template attributes on routing failures

3 participants