mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-06 13:39:07 +02:00
[management] Keep an established claim when its re-read is inconclusive
SaveProxy upserts on the proxy ID, so on a reconnect the row Connect just wrote is the claim the account has held since its first connect, and the session guard on DeleteProxy matches because the upsert wrote the new session. Withdrawing that row whenever the post-write re-read errored surrendered an established claim on a transient store error — a window in which any other account could take the address — where the pre-existing code left the row untouched. An inconclusive re-read still refuses the connect, but marks the session disconnected instead of deleting the row; only a conclusive answer that the address is claimed withdraws it. The write-then-re-read argument moves to the doc of the exported ErrClusterAddressUnavailable, where the API needs it, and both helpers point there instead of carrying it twice. Store-backed tests drive the re-read through the real queries — a reconnect keeps its row, the account's own pin is not a competing claim, another account's is — since the whole path now depends on the store excluding the account's own claims. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
e6c69f674d
commit
d80f0ff031
@@ -458,19 +458,28 @@ func TestConnect_WithdrawsClaimLostDuringRegistration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestConnect_WithdrawsClaimWhenRecheckFails pins fail-closed: a re-read that
|
||||
// cannot answer leaves the row withdrawn and the connect refused, rather than
|
||||
// letting a claim stand that was never confirmed. The error is the store's,
|
||||
// not ErrClusterAddressUnavailable — nothing established that the address is
|
||||
// taken.
|
||||
func TestConnect_WithdrawsClaimWhenRecheckFails(t *testing.T) {
|
||||
// TestConnect_KeepsClaimWhenRecheckFails pins what an inconclusive re-read
|
||||
// does: the connect is refused with the store's error, not
|
||||
// ErrClusterAddressUnavailable, since nothing established that the address is
|
||||
// taken — and the row is marked disconnected rather than deleted. SaveProxy
|
||||
// upserts on the proxy ID, so on a reconnect that row is the claim the account
|
||||
// has held since its first connect; a transient store error must not hand the
|
||||
// address to whoever asks next.
|
||||
func TestConnect_KeepsClaimWhenRecheckFails(t *testing.T) {
|
||||
accountID := "acc-1"
|
||||
var withdrawn int
|
||||
var disconnected []string
|
||||
s := &mockStore{
|
||||
hasGatewayPinnedByOtherAccountFunc: func(_ context.Context, _, _ string) (bool, error) {
|
||||
return false, errors.New("db unavailable")
|
||||
},
|
||||
deleteProxyFunc: func(_ context.Context, _, _ string) error { withdrawn++; return nil },
|
||||
disconnectProxyFunc: func(_ context.Context, proxyID, sessionID string) error {
|
||||
disconnected = append(disconnected, proxyID+"/"+sessionID)
|
||||
return nil
|
||||
},
|
||||
deleteProxyFunc: func(_ context.Context, proxyID, _ string) error {
|
||||
t.Fatalf("an inconclusive re-read must not withdraw the row, but proxy %s was deleted", proxyID)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
mgr := newTestManager(s)
|
||||
@@ -478,7 +487,26 @@ func TestConnect_WithdrawsClaimWhenRecheckFails(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.NotErrorIs(t, err, proxy.ErrClusterAddressUnavailable, "an inconclusive re-read is not a conflict")
|
||||
assert.ErrorContains(t, err, "db unavailable", "the store's error must be the one surfaced")
|
||||
assert.Equal(t, 1, withdrawn, "an unconfirmed claim must be withdrawn")
|
||||
assert.Equal(t, []string{"proxy-1/session-1"}, disconnected, "the refused session must not stay marked connected")
|
||||
}
|
||||
|
||||
// TestConnect_RefusesEvenWhenWithdrawalFails pins that a lost claim is
|
||||
// reported as lost whatever happens to the compensating delete: the caller
|
||||
// must never be told it holds an address another claim already has, and the
|
||||
// stale row is the reaper's problem, not a reason to lie.
|
||||
func TestConnect_RefusesEvenWhenWithdrawalFails(t *testing.T) {
|
||||
accountID := "acc-1"
|
||||
s := &mockStore{
|
||||
hasGatewayPinnedByOtherAccountFunc: func(_ context.Context, _, _ string) (bool, error) { return true, nil },
|
||||
deleteProxyFunc: func(_ context.Context, _, _ string) error {
|
||||
return errors.New("delete failed")
|
||||
},
|
||||
}
|
||||
|
||||
mgr := newTestManager(s)
|
||||
p, err := mgr.Connect(context.Background(), "proxy-1", "session-1", "gw.example.com", "10.0.0.1", &accountID, nil)
|
||||
require.ErrorIs(t, err, proxy.ErrClusterAddressUnavailable, "a failed withdrawal must not turn a lost claim into a held one")
|
||||
assert.Nil(t, p)
|
||||
}
|
||||
|
||||
// TestConnect_ConfirmedClaimKeepsRow is the common case: nothing landed in the
|
||||
|
||||
Reference in New Issue
Block a user