Skip to content

fix(protocol): keep model identity groups when a withdrawn llm service is restored - #72

Open
JeniaJitsev wants to merge 2 commits into
eth-easl:mainfrom
JeniaJitsev:fix/llm-restore-identity-groups
Open

JeniaJitsev wants to merge 2 commits into
eth-easl:mainfrom
JeniaJitsev:fix/llm-restore-identity-groups

Conversation

@JeniaJitsev

Copy link
Copy Markdown

Problem

registerLLMService builds an llm worker's identity groups from the backend's /v1/models (model=<id>). They are not part of the configuration. When StartServiceHealthRevalidation restores a service it had withdrawn, it re-advertises it with viper.GetStringSlice("service.identity_group") only. For a typical llm worker that list is empty. The worker comes back ready but carries no model groups, so model-based routing does not reach it, and nothing repairs that short of restarting the worker.

Observed

On 2026-11-06, on a private mesh at f740781, two workers fronted vLLM engines through an SSH-tunnelled loopback bridge. Both failed three consecutive health probes during a stretch of slow logins on the remote side, and both were withdrawn ("failed 3 consecutive health checks; withdrawing advertisement"). Once the backends answered again, the head's /v1/dnt/table listed both workers ready with an llm service whose identity_group was empty. The gateway's model list no longer contained their models. Restarting only the worker processes (backends untouched) re-registered them with model=<id> groups within about 30 s, and inference worked again.

Fix

  • The model fetch moves out of registerLLMService into fetchLLMIdentityGroups(port, attempts, interval). Behaviour there is unchanged: 10 attempts, 3 s apart, loud on final failure.
  • On restore, restoredService re-reads /v1/models once for an llm service (it has just passed a health check). If that read fails, it uses the identity groups the service held when it was withdrawn, recorded by the loop before removeLocalService. Configured groups are kept in both cases. Non-llm services behave as before.
  • The fetch sits behind a package-level variable (llmIdentityGroupsFetcher) so the restore path can be tested without a network.

Tests

go test ./internal/protocol/... and go vet ./internal/protocol/ pass with Go 1.27.3, and go build ./... succeeds. The baseline on upstream main (4b6e90f) passed before the change. New tests:

  • TestRestoredService: llm restore uses the fetched groups; configured groups are merged; a failed fetch falls back to the groups held at withdrawal; a non-llm service keeps its configured groups and is never probed.
  • TestFetchLLMIdentityGroups: parses /v1/models from an httptest server and returns an error once it is gone.
  • TestLocalServiceIdentityGroups: returns a copy, or nil for an unknown service.

Separate observation, not addressed here

After a worker restart, the head sometimes did not pick up the worker's node-table row for several minutes. The worker listed itself ready with its services, while the head's /v1/dnt/table lacked it, and stopping the worker for 20 s or 70 s before starting it did not help. A POST /v1/dnt/_node with body {} on the worker, which forces UpdateNodeTable and a fresh store.Put, made the row appear at the head within 5 s. I have not traced the cause; it may deserve its own issue.

🤖 Generated with Claude Code

JeniaJitsev and others added 2 commits November 6, 2026 17:50
…e is restored

registerLLMService takes an llm worker's identity groups from /v1/models
("model=<id>"); they are not in the configuration. When the health
revalidation loop restored a withdrawn service, it re-advertised it with
service.identity_group alone, so the worker came back ready but without
model groups, and model-based routing never reached it again until the
worker was restarted.

On restore, an llm service now reads /v1/models again (one attempt; it
just passed a health check). If that fails, the identity groups the
service held when it was withdrawn are used. Configured groups are kept
in both cases. The model fetch is factored out of registerLLMService
into fetchLLMIdentityGroups, unchanged in behaviour there.

Assisted by Claude Opus 5.5
… healthy

Restoring a withdrawn llm service read /v1/models once and never again. If
that read failed, the worker kept the groups it held at withdrawal, which can
name a model it no longer serves, or none at all. A model swap too quick to
trigger a withdrawal, or a failed read at startup, was also never corrected.

Each healthy tick now re-reads the model list and publishes on change. On a
failed read the current groups are kept.

Also:
- An empty model list is a failed read, so startup retries it during warm-up
  and logs the final failure instead of registering silently without groups.
- Restore replaces the local entry instead of merging into it, so a model
  re-added through POST /v1/dnt/_node while withdrawn does not outlive a
  successful read. Refresh and restore replace only model=<id> groups; other
  groups and model=* are kept.
- The service stays withdrawn until its new entry is in place, so a reannounce
  during the /v1/models read cannot publish the old groups.
- service.identity_group no longer applies to an llm service on restore. It
  never applied at startup, so a flap changed the worker's routing identity.
- removeLocalService returns the groups it removed, and the loop body moves to
  serviceRevalidator.tick so the withdraw and restore wiring is tested against
  a real HTTP backend. This replaces llmIdentityGroupsFetcher and
  localServiceIdentityGroups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RijVob4d6Y1reSaEVvNJJU
Entire-Checkpoint: 01M6S03AHVPTYZZZ65K59W06WA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants