Skip to content

fix(server): refresh the device cache on every watch tick - #130

Merged
hami-robot[bot] merged 1 commit into
Project-HAMi:mainfrom
pescn:fix/refresh-device-cache-every-tick
Aug 20, 2026
Merged

fix(server): refresh the device cache on every watch tick#130
hami-robot[bot] merged 1 commit into
Project-HAMi:mainfrom
pescn:fix/refresh-device-cache-every-tick

Conversation

@pescn

@pescn pescn commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

What type of PR is this?

Bug fix. This repository has no kind/* labels, so the /kind command cannot be used here. The enhancement label currently on this PR was applied automatically and is not accurate; please relabel it to bug.

What this PR does / why we need it:

watchAndRegister refreshed the cached device view only while GetUnHealthIDs() reported at least one unhealthy device:

unhealthy := ps.mgr.GetUnHealthIDs()
if len(unhealthy) > 0 {
    if err := ps.mgr.UpdateDevice(); err != nil { ... }
    ps.healthCh <- unhealthy[0]
}

When a device is unhealthy while the plugin starts — a node that boots faster than the Ascend driver initialises — and recovers shortly afterwards, that condition is already false on the next iteration. The cache keeps the startup snapshot forever, and because ListAndWatch only sends again when it receives on healthCh, kubelet also keeps the startup device list.

The node then advertises no allocatable devices and Pods fail admission with Allocate failed due to no healthy devices present, while npu-smi reports every device healthy. registerHAMi() keeps publishing the same stale snapshot to hami.io/node-register-<commonWord>, so the scheduler sees the stale health too. Only restarting the Pod recovers the node. The same condition keeps a partially enumerated device list from ever growing.

This PR:

  • refreshes the device cache on every iteration of watchAndRegister;
  • publishes a ListAndWatch update whenever the device set changes, in either direction, using a fingerprint over the published device UUIDs and their health so the steady state stays free of redundant updates;
  • bounds the healthCh send, so that a missing ListAndWatch consumer cannot stall the loop and thereby also stop the HAMi node registration that follows it (healthCh is unbuffered);
  • adds regression tests for recovery, late device discovery, steady state, and the missing-consumer case.

Which issue(s) this PR fixes:

Fixes #129

Special notes for your reviewer:

  • The change is limited to the device-cache refresh loop. Device allocation, scheduling, resource accounting, vNPU handling, and both slicing modes are untouched; go.mod / go.sum are unchanged.

  • ListAndWatch itself is not modified, so this does not overlap with fix(server): stop ListAndWatch on stream closure #126 (stream-closure handling); the two are complementary — that PR fixes the consumer side of the stream lifecycle, this one fixes the producer side never producing.

  • healthUpdateSendTimeout is a package-level variable rather than a constant only so the regression test can shorten it.

  • Each of the four new tests fails on main and passes with this change:

    test failure on main
    TestWatchAndRegister_RepublishesRecoveredDevices expected kubelet to receive an updated device list, got 1 response(s)
    TestWatchAndRegister_PublishesLateDiscoveredDevices expected 16 devices in the last response, got 8
    TestWatchAndRegister_SteadyStateDoesNotResend expected the device cache to be refreshed at least once
    TestWatchAndRegister_ContinuesWithoutListAndWatchConsumer registration stops, annotation stays Health:false

Verification performed on macOS/arm64 (go1.26.6):

  • make test — all packages pass
  • go vet ./internal/... — clean
  • golangci-lint v2.1.0 run ./internal/...0 issues
  • go test -race -count=5 ./internal/server/ -run TestWatchAndRegister — clean
  • device-plugin binary build — succeeds

Field context: the failure was observed on a two-node RKE2 v1.35.5 cluster with 4 × Ascend 910B4-1 per node running v1.4.0, and the observable signature is a disagreement between hami.io/node-register-<commonWord> and the device series exported by the plugin's own dcmi collector on :9395 (the latter reads the hardware live and stays correct during the failure).

Does this PR introduce a user-facing change?:

No.

Summary by CodeRabbit

  • Bug Fixes
    • Device availability updates now reflect health recovery and newly discovered devices.
    • Unchanged device lists are no longer resent unnecessarily.
    • Device registration continues reliably even when no consumer is connected.
    • Updates honor shutdown signals and use bounded delivery timeouts.
  • Tests
    • Added coverage for device recovery, late discovery, steady-state behavior, and registration without an active consumer.

@hami-robot

hami-robot Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

@pescn: The label(s) kind/bug cannot be applied, because the repository doesn't have them.

Details

In response to this:

What type of PR is this?

/kind bug

What this PR does / why we need it:

watchAndRegister refreshed the cached device view only while GetUnHealthIDs() reported at least one unhealthy device:

unhealthy := ps.mgr.GetUnHealthIDs()
if len(unhealthy) > 0 {
   if err := ps.mgr.UpdateDevice(); err != nil { ... }
   ps.healthCh <- unhealthy[0]
}

When a device is unhealthy while the plugin starts — a node that boots faster than the Ascend driver initialises — and recovers shortly afterwards, that condition is already false on the next iteration. The cache keeps the startup snapshot forever, and because ListAndWatch only sends again when it receives on healthCh, kubelet also keeps the startup device list.

The node then advertises no allocatable devices and Pods fail admission with Allocate failed due to no healthy devices present, while npu-smi reports every device healthy. registerHAMi() keeps publishing the same stale snapshot to hami.io/node-register-<commonWord>, so the scheduler sees the stale health too. Only restarting the Pod recovers the node. The same condition keeps a partially enumerated device list from ever growing.

This PR:

  • refreshes the device cache on every iteration of watchAndRegister;
  • publishes a ListAndWatch update whenever the device set changes, in either direction, using a fingerprint over the published device UUIDs and their health so the steady state stays free of redundant updates;
  • bounds the healthCh send, so that a missing ListAndWatch consumer cannot stall the loop and thereby also stop the HAMi node registration that follows it (healthCh is unbuffered);
  • adds regression tests for recovery, late device discovery, steady state, and the missing-consumer case.

Which issue(s) this PR fixes:

Fixes #129

Special notes for your reviewer:

  • The change is limited to the device-cache refresh loop. Device allocation, scheduling, resource accounting, vNPU handling, and both slicing modes are untouched; go.mod / go.sum are unchanged.
  • ListAndWatch itself is not modified, so this does not overlap with fix(server): stop ListAndWatch on stream closure #126 (stream-closure handling); the two are complementary — that PR fixes the consumer side of the stream lifecycle, this one fixes the producer side never producing.
  • healthUpdateSendTimeout is a package-level variable rather than a constant only so the regression test can shorten it.
  • Each of the four new tests fails on main and passes with this change:
test failure on main
TestWatchAndRegister_RepublishesRecoveredDevices expected kubelet to receive an updated device list, got 1 response(s)
TestWatchAndRegister_PublishesLateDiscoveredDevices expected 16 devices in the last response, got 8
TestWatchAndRegister_SteadyStateDoesNotResend expected the device cache to be refreshed at least once
TestWatchAndRegister_ContinuesWithoutListAndWatchConsumer registration stops, annotation stays Health:false

Verification performed on macOS/arm64 (go1.26.6):

  • make test — all packages pass
  • go vet ./internal/... — clean
  • golangci-lint v2.1.0 run ./internal/...0 issues
  • go test -race -count=5 ./internal/server/ -run TestWatchAndRegister — clean
  • device-plugin binary build — succeeds

Field context: the failure was observed on a two-node RKE2 v1.35.5 cluster with 4 × Ascend 910B4-1 per node running v1.4.0, and the observable signature is a disagreement between hami.io/node-register-<commonWord> and the device series exported by the plugin's own dcmi collector on :9395 (the latter reads the hardware live and stays correct during the failure).

Does this PR introduce a user-facing change?:

No.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@hami-robot
hami-robot Bot requested review from DSFans2014 and archlitchi August 19, 2026 15:15
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: da41ac9b-8e74-43c7-8c9a-76e5b105f42d

📥 Commits

Reviewing files that changed from the base of the PR and between 1cf92f2 and 8e4a50f.

📒 Files selected for processing (3)
  • internal/server/register.go
  • internal/server/server.go
  • internal/server/watch_and_register_test.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The server now refreshes device state on every timer tick, fingerprints device UUIDs and health states, republishes changed device lists, bounds update delivery, and preserves registration progress when no ListAndWatch consumer is connected. Tests cover recovery, late discovery, steady state, and shutdown behavior.

Changes

Device state republishing

Layer / File(s) Summary
Watcher refresh and publication flow
internal/server/register.go, internal/server/server.go
watchAndRegister refreshes devices on every tick and republishes only when device UUIDs or health states change. Start records the initial fingerprint, and update sends use a configurable timeout.
Watcher and registration regression coverage
internal/server/watch_and_register_test.go
Tests cover recovered devices, devices discovered after startup, unchanged steady state, and registration without a ListAndWatch consumer.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to 8e4a5

This localized change refreshes device state and republishes updates when needed; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant watchAndRegister
  participant AscendManager
  participant ListAndWatch
  participant NodeRegistration
  watchAndRegister->>AscendManager: UpdateDevice on each timer tick
  AscendManager-->>watchAndRegister: Current device state
  watchAndRegister->>ListAndWatch: Publish when fingerprint changes
  watchAndRegister->>NodeRegistration: Register current device state
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: archlitchi, dsfans2014

Poem

A rabbit watches devices turn,
Healthy lights return and burn.
New ones join the published line,
Steady states send once, then shine.
Registration hops along—
Even when the stream is gone.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address issue #129 by republishing recovered and late-discovered devices without restarting the plugin.
Out of Scope Changes check ✅ Passed The implementation and regression tests remain within the stated device-cache refresh and republishing objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: refreshing the server device cache on every watch tick.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added the enhancement New feature or request label Aug 19, 2026
watchAndRegister refreshed the cached device view only while
GetUnHealthIDs() reported at least one unhealthy device. When a device
was unhealthy as the plugin started and recovered shortly afterwards,
that condition was already false on the next iteration, so the cache
kept the startup snapshot forever and no further ListAndWatch response
was ever sent.

kubelet therefore stayed on the device list it received at startup. On a
node that booted while the driver was still initialising, every device
stayed Unhealthy, the node advertised no allocatable devices, and Pods
failed admission with "Allocate failed due to no healthy devices
present" even though npu-smi reported all devices healthy. The same
condition kept a partially enumerated device list from ever growing.
Only restarting the Pod, which calls UpdateDevice() again in Start(),
recovered the node.

Refresh the cache on every iteration and publish a ListAndWatch update
whenever the device set changes, in either direction. A fingerprint over
the published device UUIDs and their health keeps the steady state free
of redundant updates. The send is bounded so that a missing ListAndWatch
consumer cannot stall the loop, which would also stop the HAMi node
registration that follows it, since healthCh is unbuffered.

Signed-off-by: 韩翔宇 <admin@pescn.cn>
@pescn
pescn force-pushed the fix/refresh-device-cache-every-tick branch from 8e4a50f to 8d6bf6e Compare August 19, 2026 15:25

@archlitchi archlitchi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

/lgtm

@hami-robot

hami-robot Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: archlitchi, pescn

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@hami-robot hami-robot Bot added the approved label Aug 20, 2026
@hami-robot
hami-robot Bot merged commit 4b977f9 into Project-HAMi:main Aug 20, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved enhancement New feature or request lgtm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Devices that recover after startup are never republished to kubelet

2 participants