Skip to content

Commit 9c185e7

Browse files
committed
admin: drop dup RoleStore + Retry-After in forward 503 (Codex P2)
Two changes on the AdminForward leader-side dispatcher: - Codex P2 on PR #635: forwarded 503 leader_unavailable lost the Retry-After: 1 header that the leader-direct HTTP path emits. Add retry_after_seconds to AdminForwardResponse so the follower's bridge can rebuild the same HTTP header (the bridge side wires it in the next PR; the proto + leader populate it here). Direct HTTP path is unaffected — the proto field is only consumed during forwarding. - Drop the duplicate RoleStore/MapRoleStore declarations from forward_server.go. They now live in role_store.go (added on PR #634 for HTTP-side role revalidation) so both surfaces share one definition. Tests: extend TestForwardServer_CreateTable_LeaderSteppedDownReturns503 to pin the retry_after_seconds=1 hint.
1 parent 55731ff commit 9c185e7

4 files changed

Lines changed: 47 additions & 30 deletions

File tree

internal/admin/forward_server.go

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -42,30 +42,6 @@ type ForwardServer struct {
4242
logger *slog.Logger
4343
}
4444

45-
// RoleStore is the access-key → role lookup the leader uses to
46-
// re-validate the inbound principal. Implementations should mirror
47-
// the admin server's `Roles` map; production passes a typed wrapper
48-
// around that map so tests can swap in an in-memory stub.
49-
type RoleStore interface {
50-
// LookupRole returns the role for an access key as understood
51-
// by the leader's view of cluster configuration. The bool is
52-
// false when the access key is not in the admin role index — a
53-
// follower that forwarded the principal should not be able to
54-
// "make up" an admin identity.
55-
LookupRole(accessKey string) (Role, bool)
56-
}
57-
58-
// MapRoleStore is the trivial in-memory implementation, sufficient
59-
// for tests and for the production wiring (which already keeps the
60-
// role map in memory).
61-
type MapRoleStore map[string]Role
62-
63-
// LookupRole implements RoleStore.
64-
func (m MapRoleStore) LookupRole(accessKey string) (Role, bool) {
65-
r, ok := m[accessKey]
66-
return r, ok
67-
}
68-
6945
// NewForwardServer wires a TablesSource and a RoleStore behind the
7046
// gRPC AdminForward service. logger may be nil; defaults to
7147
// slog.Default().
@@ -237,7 +213,14 @@ func forwardErrorResponse(op string, err error) *pb.AdminForwardResponse {
237213
// Should never happen on the leader path — the leader
238214
// just verified itself — but if a leadership transfer
239215
// races with the dispatch, surface it consistently.
240-
return mustForwardJSON(http.StatusServiceUnavailable, errorBody{Error: "leader_unavailable", Message: "leader stepped down mid-request"})
216+
// Carry retry_after_seconds=1 so the follower's bridge
217+
// translates it back into the same HTTP Retry-After
218+
// header the leader-direct path emits (Codex P2 on
219+
// PR #635 — without this the forwarded 503 would lose
220+
// its retry timing).
221+
resp := mustForwardJSON(http.StatusServiceUnavailable, errorBody{Error: "leader_unavailable", Message: "leader stepped down mid-request"})
222+
resp.RetryAfterSeconds = 1
223+
return resp
241224
case errors.Is(err, ErrTablesNotFound):
242225
return mustForwardJSON(http.StatusNotFound, errorBody{Error: "not_found", Message: "table does not exist"})
243226
case errors.Is(err, ErrTablesAlreadyExists):

internal/admin/forward_server_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,12 @@ func TestForwardServer_CreateTable_LeaderSteppedDownReturns503(t *testing.T) {
334334
require.NoError(t, err)
335335
require.Equal(t, int32(http.StatusServiceUnavailable), resp.GetStatusCode())
336336
require.Contains(t, string(resp.GetPayload()), "leader_unavailable")
337+
// Codex P2 on PR #635: the leader's 503 must carry the
338+
// retry-after hint over the wire so the follower's bridge can
339+
// rebuild the same HTTP Retry-After header a leader-direct
340+
// 503 would have produced. Without this the forwarded 503
341+
// silently strips the retry timing.
342+
require.Equal(t, int32(1), resp.GetRetryAfterSeconds())
337343
}
338344

339345
// TestForwardServer_DeleteTable_LeaderSteppedDownReturns503 mirrors

proto/admin_forward.pb.go

Lines changed: 23 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proto/admin_forward.proto

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,14 @@ message AdminForwardResponse {
7474
// practice always "application/json; charset=utf-8" but echoed
7575
// explicitly so the follower does not have to guess.
7676
string content_type = 3;
77+
// retry_after_seconds carries a Retry-After hint the follower's
78+
// bridge translates back into an HTTP Retry-After header. The
79+
// direct HTTP path sets Retry-After: 1 on 503 leader_unavailable
80+
// (mid-dispatch leadership churn); without this field the same
81+
// response shape would lose its retry timing once forwarded —
82+
// clients would see 503 with no Retry-After and fall back to
83+
// their default backoff. Codex P2 on PR #635 flagged the gap.
84+
// Zero means "no Retry-After header"; the field is only meaningful
85+
// for status codes that traditionally carry one (typically 503).
86+
int32 retry_after_seconds = 4;
7787
}

0 commit comments

Comments
 (0)