Skip to content

fix(selector/ewma): do not penalise nodes on context.Canceled (fixes #3834) - #3836

Open
akpradheeph wants to merge 2 commits into
go-kratos:mainfrom
akpradheeph:fix/ewma-context-canceled-node-degradation
Open

fix(selector/ewma): do not penalise nodes on context.Canceled (fixes #3834)#3836
akpradheeph wants to merge 2 commits into
go-kratos:mainfrom
akpradheeph:fix/ewma-context-canceled-node-degradation

Conversation

@akpradheeph

Copy link
Copy Markdown

Problem

Fixes #3834.

The EWMA node health callback treated context.Canceled identically to context.DeadlineExceeded, setting success = 0 (fully degraded) for both. These have different origins:

Error Caused by Backend healthy?
context.DeadlineExceeded Backend too slow Possibly not — penalise
context.Canceled Caller cancelled Yes — unrelated to backend
// Before
if errors.Is(context.DeadlineExceeded, di.Err) || errors.Is(context.Canceled, di.Err) || ...
    success = 0
}

Impact

Any workload with frequent client-side cancellations — mobile apps, frontend SPAs, aggressive ingress timeouts — causes the EWMA balancer to lower the health score of healthy backends. Under sustained load all nodes can reach success ≈ 0 simultaneously, producing erratic distribution that recovers only via the 600 ms EWMA decay window.

Change

Remove errors.Is(context.Canceled, di.Err) from the success = 0 condition. Canceled now updates the lag EWMA (reflecting real latency up to cancellation) but does not penalise node health.

Tests

TestCanceledDoesNotDegradeNode asserts:

  • Repeated context.Canceled errors do not reduce node weight
  • context.DeadlineExceeded still degrades node weight (regression guard)

All existing selector/node/ewma tests pass.

The EWMA node health callback marked a node success=0 (fully degraded)
whenever di.Err was context.Canceled, treating it identically to
context.DeadlineExceeded. These two errors have different origins:

  context.DeadlineExceeded — backend was too slow to respond within the
    client timeout. May indicate backend overload or latency issues.
    Penalising the node is reasonable.

  context.Canceled — the *caller* cancelled the in-flight RPC (user
    navigated away, upstream HTTP request was aborted, load test
    interrupted mid-flight, parent context cancelled). The backend is
    completely unaware of this cancellation and may be perfectly healthy.
    Penalising the backend node is incorrect.

Impact of the bug:
  Any workload with frequent client-side cancellations — mobile apps,
  frontend SPAs with AbortController, aggressive per-request timeouts on
  the ingress side — causes the EWMA balancer to continuously lower the
  health score of otherwise healthy backends. Under sustained cancellation
  load all nodes can reach success≈0 simultaneously, producing erratic
  load distribution that recovers only via the 600ms EWMA decay window.

Fix:
  Remove errors.Is(context.Canceled, di.Err) from the success=0
  condition. Canceled is now treated the same as any other non-fatal
  error: the node's lag EWMA is updated (reflecting real latency up to
  the cancellation point) but its health score is not penalised.

Tests:
  TestCanceledDoesNotDegradeNode asserts that repeated context.Canceled
  errors do not reduce node weight, while context.DeadlineExceeded still
  does (regression guard for the existing behaviour).

Fixes go-kratos#3834
@dosubot dosubot Bot added the size:XS This PR changes 0-9 lines, ignoring generated files. label May 26, 2026
@shenqidebaozi
shenqidebaozi requested a review from Copilot June 11, 2026 14:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adjusts the EWMA node health calculation so that context.Canceled (caller-initiated cancellation) no longer fully degrades a backend node’s health, while preserving degradation on backend-relevant failures like context.DeadlineExceeded.

Changes:

  • Exclude context.Canceled from the EWMA “success = 0” degradation condition in the node pick callback.
  • Add a new regression test intended to ensure context.Canceled does not degrade nodes, while context.DeadlineExceeded still does.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
selector/node/ewma/node.go Removes context.Canceled from the error conditions that set EWMA success to 0.
selector/node/ewma/node_test.go Adds a test covering cancellation vs deadline exceeded behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread selector/node/ewma/node.go Outdated
Comment on lines 156 to 164
if errors.Is(context.DeadlineExceeded, di.Err) ||
// context.Canceled is intentionally excluded: it means the caller
// cancelled the request (user navigation, upstream timeout, etc.) and
// says nothing about whether the backend is healthy. Penalising nodes
// for client-side cancellations causes healthy backends to lose weight
// under normal frontend workloads with frequent in-flight cancellations.
errors.IsServiceUnavailable(di.Err) || errors.IsGatewayTimeout(di.Err) || errors.As(di.Err, &netErr) {
success = 0
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in b444c58. Changed to errors.Is(di.Err, context.DeadlineExceeded) — correct stdlib/Kratos signature so wrapped deadline errors are matched properly.

Comment thread selector/node/ewma/node_test.go Outdated
Comment on lines +112 to +123
baseline := wn.Weight()

// Several picks that all report context.Canceled.
for i := 0; i < 4; i++ {
done = wn.Pick()
time.Sleep(time.Millisecond * 20)
done(context.Background(), selector.DoneInfo{Err: context.Canceled})
}
if wn.Weight() < baseline*0.9 {
t.Errorf("context.Canceled should not degrade node weight: before=%.2f after=%.2f",
baseline, wn.Weight())
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in b444c58. Now type-asserts wn.(*Node) and asserts directly on n.success.Load() (the health EWMA) rather than Weight(). Since the test is in package ewma it has direct access to the unexported field.

Comment thread selector/node/ewma/node_test.go Outdated
Comment on lines +130 to +140
baseline2 := wn2.Weight()

for i := 0; i < 4; i++ {
done = wn2.Pick()
time.Sleep(time.Millisecond * 20)
done(context.Background(), selector.DoneInfo{Err: context.DeadlineExceeded})
}
if wn2.Weight() >= baseline2 {
t.Errorf("context.DeadlineExceeded should degrade node weight: before=%.2f after=%.2f",
baseline2, wn2.Weight())
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in b444c58. Same approach — asserts on n2.success.Load() directly to verify the health EWMA drops on context.DeadlineExceeded, independent of lag/load fluctuations.

…not weight

- errors.Is(context.DeadlineExceeded, di.Err) had arguments reversed;
  fixed to errors.Is(di.Err, context.DeadlineExceeded) so wrapped
  deadline errors are matched correctly.
- TestCanceledDoesNotDegradeNode now asserts on the node's success EWMA
  (n.success.Load()) rather than Weight(), which also varies with lag/load
  and can fluctuate even when health is unchanged. Accessing the internal
  field directly is valid since the test is in package ewma.

Addresses reviewer feedback on PR go-kratos#3836.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XS This PR changes 0-9 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

selector/node/ewma: context.Canceled incorrectly penalises backend nodes — client cancellation is not a backend failure

2 participants