Skip to content

backup: add admin version API scaffolding#1059

Open
bootjp wants to merge 6 commits into
design/live-backup-pin-substratefrom
design/live-backup-admin-api
Open

backup: add admin version API scaffolding#1059
bootjp wants to merge 6 commits into
design/live-backup-pin-substratefrom
design/live-backup-admin-api

Conversation

@bootjp

@bootjp bootjp commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • add the live-backup admin RPC surface for BeginBackup, RenewBackup, EndBackup, ListAdaptersAndScopes, and GetNodeVersion
  • expose the node build version through the gRPC Admin service and surface leader node versions in GetRaftGroups via an async cache
  • expose the Raft snapshot threshold through the engine interface so backup headroom checks can use the configured value

Base: #1056

Validation

  • go test ./adapter -run 'Test(GetNodeVersion|GetRaftGroups|GroupOrdering|AdminTokenAuth|GetClusterOverview)' -count=1
  • go test . -count=1
  • go test ./internal/raftengine/etcd -run 'Test(SnapshotEvery|EngineSnapshotEvery)' -count=1
  • go test ./kv -run 'Test(CoordinateDispatch|LeaderProxy|LeaseRead|ShardedCoordinator|ValidateReadOnlyShards|LeaseWarmup)' -count=1
  • go test ./internal/raftadmin -count=1
  • golangci-lint run --timeout=5m
  • go test ./... -run '^$' -count=1
  • git diff --check
  • git verify-commit HEAD

Author: bootjp

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 600cd239-c24e-4e7b-870b-a5d9f333eeb5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@bootjp

bootjp commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

Implemented the admin API/version scaffolding slice on top of #1056.

Validation:

  • go test ./adapter -run 'Test(GetNodeVersion|GetRaftGroups|GroupOrdering|AdminTokenAuth|GetClusterOverview)' -count=1\n- go test . -count=1\n- go test ./internal/raftengine/etcd -run 'Test(SnapshotEvery|EngineSnapshotEvery)' -count=1\n- go test ./kv -run 'Test(CoordinateDispatch|LeaderProxy|LeaseRead|ShardedCoordinator|ValidateReadOnlyShards|LeaseWarmup)' -count=1\n- go test ./internal/raftadmin -count=1\n- golangci-lint run --timeout=5m\n- go test ./... -run '^$' -count=1\n- git diff --check\n- git verify-commit HEAD\n\n@codex review

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces a new GetNodeVersion RPC to the Admin service and implements asynchronous leader version probing and caching within GetRaftGroups. It also adds a SnapshotEvery method to the AdminGroup interface and its implementations. The review feedback highlights a potential race condition in probeLeaderVersionAsync due to accessing the request context asynchronously inside a goroutine, and suggests passing a mockable clock time into leaderNodeVersion to improve testability and avoid redundant syscalls.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread adapter/admin_grpc.go Outdated
Comment on lines +563 to +578
func (s *AdminServer) probeLeaderVersionAsync(ctx context.Context, key, address string) {
probe := s.leaderVersionProbe
timeout := s.leaderVersionProbeTimeout
go func() {
probeCtx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if md, ok := metadata.FromIncomingContext(ctx); ok {
probeCtx = metadata.NewOutgoingContext(probeCtx, md.Copy())
}
version, err := probe(probeCtx, address)
if err != nil {
version = ""
}
s.versionCache.Store(key, versionCacheEntry{version: version, fetchedAt: time.Now()})
}()
}

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.

high

In probeLeaderVersionAsync, the incoming gRPC request context ctx is passed directly into a background goroutine, and metadata.FromIncomingContext(ctx) is called asynchronously inside that goroutine.

Since the background goroutine runs asynchronously, the parent GetRaftGroups request may have already completed and returned. Once the request returns, the gRPC server cancels and cleans up the request context. Accessing ctx asynchronously after the handler has returned can lead to race conditions, undefined behavior, or accessing recycled metadata.

To ensure safety and avoid race conditions, extract the metadata synchronously from ctx before spawning the goroutine.

func (s *AdminServer) probeLeaderVersionAsync(ctx context.Context, key, address string) {
	probe := s.leaderVersionProbe
	timeout := s.leaderVersionProbeTimeout
	var md metadata.MD
	if incomingMd, ok := metadata.FromIncomingContext(ctx); ok {
		md = incomingMd.Copy()
	}
	go func() {
		probeCtx, cancel := context.WithTimeout(context.Background(), timeout)
		defer cancel()
		if md != nil {
			probeCtx = metadata.NewOutgoingContext(probeCtx, md)
		}
		version, err := probe(probeCtx, address)
		if err != nil {
			version = ""
		}
		s.versionCache.Store(key, versionCacheEntry{version: version, fetchedAt: time.Now()})
	}()
}

Comment thread adapter/admin_grpc.go Outdated
CommitIndex: st.CommitIndex,
AppliedIndex: st.AppliedIndex,
LastContactUnixMs: lastContactUnixMs,
LeaderNodeVersion: s.leaderNodeVersion(ctx, st.Leader),

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.

medium

Pass the mockable clock time now (computed on line 452 using s.now()) into leaderNodeVersion instead of calling time.Now() internally. This ensures consistency with the rest of the GetRaftGroups response (such as LastContactUnixMs) and allows deterministic testing of the cache TTL using the mock clock.

Suggested change
LeaderNodeVersion: s.leaderNodeVersion(ctx, st.Leader),
LeaderNodeVersion: s.leaderNodeVersion(ctx, st.Leader, now),

Comment thread adapter/admin_grpc.go Outdated
Comment on lines +488 to +496
func (s *AdminServer) leaderNodeVersion(ctx context.Context, leader raftengine.LeaderInfo) string {
if version, ok := s.localLeaderVersion(leader); ok {
return version
}
key := leaderVersionCacheKey(leader)
now := time.Now()
if version, ok := s.cachedLeaderVersion(key, now); ok {
return version
}

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.

medium

Update the signature of leaderNodeVersion to accept now time.Time from the caller. This avoids redundant time.Now() syscalls in the loop and allows deterministic testing of the cache TTL.

func (s *AdminServer) leaderNodeVersion(ctx context.Context, leader raftengine.LeaderInfo, now time.Time) string {
	if version, ok := s.localLeaderVersion(leader); ok {
		return version
	}
	key := leaderVersionCacheKey(leader)
	if version, ok := s.cachedLeaderVersion(key, now); ok {
		return version
	}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c44879386

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread main.go Outdated
token = loaded
}
srv := adapter.NewAdminServer(self, members)
srv := adapter.NewAdminServer(self, members, adapter.WithAdminNodeVersion(buildVersion()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Wire the leader-version probe into the node service

When a Raft group is led by another node, leaderNodeVersion returns an empty leader_node_version unless leaderVersionProbe is configured, but this production constructor only sets WithAdminNodeVersion and there is no non-test call wiring WithAdminLeaderVersionProbe. That means remote leaders will permanently show an unknown version instead of asynchronously fetching GetNodeVersion; please provide the probe here (or avoid advertising the field until it is wired).

Useful? React with 👍 / 👎.

Comment thread adapter/admin_grpc_test.go Outdated
srv := NewAdminServer(
NodeIdentity{NodeID: "n1", GRPCAddress: "10.0.0.11:50051"},
nil,
WithAdminLeaderVersionProbeTimeout(50*time.Millisecond),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Avoid timing out the async probe before release

This test depends on the goroutine not hitting its 50 ms timeout before the test closes releaseProbe; under a slow CI scheduler, the second GetRaftGroups call or a pause after probeStarted can take longer than that, causing the probe to cache an empty version for the 1 s TTL and waitForLeaderVersion to time out. Use a much larger timeout or release the probe before asserting cache coalescing so the test is not scheduler-sensitive.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

Addressed the current-head admin version review feedback.

Changes:

  • Wire the production leader-version probe through the shared Admin gRPC connection cache.
  • Copy inbound metadata before starting the async probe goroutine.
  • Reuse the GetRaftGroups mockable clock for leader-version cache checks.
  • Increase the async probe test timeout to avoid scheduler-sensitive failures.
  • Added a production wiring regression test that fetches a remote node version through ConfigureAdminService.

Verification:

  • go test ./adapter -run 'Test(GetNodeVersion|GetRaftGroups|GroupOrdering|AdminTokenAuth|GetClusterOverview)' -count=1\n- go test . -run 'TestConfigureAdminService|TestAdminMembersFromBootstrap|TestCanonicalSelfAddress|TestLoadAdminTokenFileRejectsOversize)' -count=1\n- go test ./internal/raftadmin -count=1\n- go test . -count=1\n- go test ./... -run '^$' -count=1\n- golangci-lint run --timeout=5m\n- git diff --check\n- git verify-commit HEAD\n\n@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8f78f4b228

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread adapter/admin_grpc.go Outdated
if err != nil {
version = ""
}
s.versionCache.Store(key, versionCacheEntry{version: version, fetchedAt: time.Now()})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Use AdminServer clock for version-cache stamps

When tests or callers install a custom clock with SetClock, the cache freshness checks compare entries against that injected clock, but async probe completions are stamped with wall time here. If the injected clock is behind real time, now.Sub(entry.fetchedAt) stays negative and cached empty/error versions do not expire on the configured TTL; if it is ahead, a just-fetched version can expire immediately. Stamp cache entries with the same clock used by GetRaftGroups so TTL behavior remains consistent.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

Addressed the current-head leader-version cache clock feedback.

Changes:

  • Stamp async leader-version probe completions with the AdminServer clock captured under GetRaftGroups instead of wall time.
  • Added regression coverage where the injected server clock advances past the cache TTL and forces a fresh probe.

Verification:

  • go test ./adapter -run 'TestGetRaftGroupsLeaderVersion(Async|ProbeUsesServerClockForCacheStamp|ProbePropagatesAuthMetadata)|TestGetNodeVersion|TestGetRaftGroups' -count=1 -timeout=240s\n- go test ./adapter -run '^$' -count=1 -timeout=120s\n- go test ./... -run '^$' -count=1 -timeout=300s\n- golangci-lint run ./adapter/... --timeout=5m\n- git diff --check\n- git verify-commit HEAD\n\n@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6ece87b2f6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread adapter/admin_grpc.go Outdated
if err != nil {
version = ""
}
s.versionCache.Store(key, versionCacheEntry{version: version, fetchedAt: now()})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard cache writes from stale probe completions

When a leader-version probe is still in flight after its reservation has expired (for example with a probe timeout longer than the cache TTL, or when the injected server clock advances past the TTL), a later GetRaftGroups call can replace the expired marker and start a fresh probe, but the older goroutine still unconditionally stores its result here. If that older probe then times out or returns an older value after the fresh probe has populated the cache, it overwrites the newer entry and suppresses another probe for a full TTL; use a generation/CAS-style check against the reservation before storing the completion.

Useful? React with 👍 / 👎.

Comment thread main.go
Comment thread adapter/admin_grpc.go
@bootjp

bootjp commented Jul 11, 2026

Copy link
Copy Markdown
Owner Author

Addressed the current-head admin version inline findings.

Changes:

  • Guarded async leader-version probe completion with a per-reservation generation and sync.Map CAS, so stale probes cannot overwrite fresher cache entries.
  • Scoped leader-version cache/reservation keys to leader ID + address when an address is known, so a failed listener does not poison alternate per-group listener addresses for the same node ID.
  • Made adminBuildVersion an ldflags-backed variable with VCS build-info fallback, and kept configureAdminService/GetNodeVersion plus the admin HTTP cluster version on the same buildVersion() source.

Caller audit:

  • rg shows the leader-version cache helpers are only reached from AdminServer.GetRaftGroups; the external semantic remains LeaderNodeVersion == "" while probing or during an error-cache TTL, and the probed/local version once available.
  • GetNodeVersion is consumed by adminLeaderVersionProbe; buildVersion() is also used by newClusterInfoSource for GET /admin/api/v1/cluster, so both admin surfaces now share the same build identifier.
  • No other caller relies on node-ID-only leader-version cache keys.

Validation:

  • go test ./adapter -run 'TestGetRaftGroupsLeaderVersion(StaleProbeCannotOverwriteFreshCache|TriesAlternateAddressForSameLeaderID)|TestGetRaftGroupsLeaderVersion(Async|ProbeUsesServerClockForCacheStamp|ProbePropagatesAuthMetadata)|TestGetNodeVersion' -count=1 -timeout=240s\n- go test . -run 'TestConfigureAdminService(UsesStampedBuildVersion|WiresLeaderVersionProbe)' -count=1 -timeout=240s\n- go test ./adapter -run '^$' -count=1 -timeout=120s\n- go test ./... -run '^$' -count=1 -timeout=300s\n- golangci-lint run ./adapter/... . --timeout=5m\n- git diff --check\n- git verify-commit HEAD (Good signature, commit ab741c2cead5e82b2ce7176e25b64b68bcad2675)\n\n@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: ab741c2cea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@bootjp

bootjp commented Jul 11, 2026

Copy link
Copy Markdown
Owner Author

@codex review

Updated HEAD 7aece2712ca9 addresses the remaining active threads:

  • GetNodeVersion is stamped from buildVersion(), which uses adminBuildVersion when release builds set it and otherwise falls back to Go build info vcs.revision before returning dev. Existing TestConfigureAdminServiceUsesStampedBuildVersion covers the Admin gRPC wiring.
  • Leader-version probing now builds candidate Admin listener addresses for the same leader node ID across registered Raft groups. If the primary listener is stale or unreachable, the async probe tries the alternate listeners before caching an empty failure for that group.

Caller audit:

  • leaderNodeVersion is only called from GetRaftGroups.
  • probeLeaderVersionAsync is only called from leaderNodeVersion.
  • GetNodeVersion/buildVersion wiring is limited to configureAdminService, HTTP cluster info, and the admin listener setup.

Validation run on this head:

  • go test ./adapter -run 'TestGetNodeVersionReturnsConfiguredVersion|TestGetRaftGroupsLeaderVersion(Async|ProbeUsesServerClockForCacheStamp|StaleProbeCannotOverwriteFreshCache|TriesAlternateAddressForSameLeaderID|ProbePropagatesAuthMetadata)' -count=1 -timeout=120s
  • go test . -run 'TestConfigureAdminServiceUsesStampedBuildVersion|TestAdminGRPCGetRaftGroupsLeaderVersionFromRemote' -count=1 -timeout=120s
  • golangci-lint run . ./adapter/... --timeout=5m
  • git diff --check

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7aece2712c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread adapter/admin_grpc.go Outdated
}
version := ""
for _, address := range candidates {
probed, err := probe(probeCtx, address)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound each leader-version candidate probe

When the first candidate is a stale listener that accepts no connection, the production probe path (adminLeaderVersionProbe via GRPCConnCache.ConnFor) uses a wait-for-ready gRPC call, so this call can consume the entire 500ms probeCtx before returning. The loop then breaks on the expired context and never tries the alternate listener addresses for the same leader ID, caching an empty version for the group even though a later candidate may be reachable. Please give each candidate its own bounded attempt or otherwise avoid spending the whole budget on the first address.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 11, 2026

Copy link
Copy Markdown
Owner Author

Addressed the current-head leader-version candidate probe finding.

Changes:

  • Bound each async leader-version candidate probe to a per-candidate timeout under the total probe budget.
  • Kept the total async probe context as the outer cap and preserved inbound metadata propagation per attempt.
  • Added a regression test where the first listener consumes its attempt budget and the alternate listener still succeeds.

Verification:

  • go test ./adapter -run 'TestGetRaftGroupsLeaderVersion' -count=1\n- go test ./adapter -run 'TestGetRaftGroups|TestGetNodeVersion|TestAdmin' -count=1\n- git diff --check\n- git verify-commit HEAD\n\n@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 5cb7ae0f00

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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.

1 participant