Skip to content

Commit 0931df5

Browse files
authored
client: apply partial results from a failed poll attempt (#155)
When updating multiple secrets during a refresh, it is possible some may fail, for example if a secret that was previously declared is now gone. Before reporting this error to the caller, apply any updates that succeeded, so that the store will not get "stuck" by a single failure. Updates tailscale/corp#35650
1 parent e6ea558 commit 0931df5

File tree

2 files changed

+53
-2
lines changed

2 files changed

+53
-2
lines changed

client/setec/store.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,12 +299,20 @@ func (s *Store) Refresh(ctx context.Context) error {
299299
s.countPolls.Add(1)
300300
s.latestPoll.Set(float64(time.Now().UTC().UnixMilli()) / 1000)
301301
updates := make(map[string]*cachedSecret)
302-
if err := s.poll(ctx, updates); err != nil {
302+
303+
// Count errors from polling, but do not report them until we have
304+
// applied any updates that might have succeeded.
305+
perr := s.poll(ctx, updates)
306+
if perr != nil {
303307
s.countPollErrors.Add(1)
304-
return nil, fmt.Errorf("[store] update poll failed: %w", err)
305308
}
306309
if err := s.applyUpdates(updates); err != nil {
307310
return nil, fmt.Errorf("[store] applying updates failed: %w", err)
311+
} else if perr != nil {
312+
if len(updates) != 0 {
313+
return nil, fmt.Errorf("[store] update poll partially failed: %w", perr)
314+
}
315+
return nil, fmt.Errorf("[store] update poll failed: %w", perr)
308316
}
309317
return nil, nil
310318
})

client/setec/store_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,49 @@ func TestNewFileCache(t *testing.T) {
783783
})
784784
}
785785

786+
func TestPartialRefresh(t *testing.T) {
787+
d := setectest.NewDB(t, nil)
788+
d.MustPut(d.Superuser, "small", "1")
789+
d.MustPut(d.Superuser, "large", "500")
790+
791+
ts := setectest.NewServer(t, d, nil)
792+
hs := httptest.NewServer(ts.Mux)
793+
defer hs.Close()
794+
795+
st, err := setec.NewStore(t.Context(), setec.StoreConfig{
796+
Client: setec.Client{Server: hs.URL, DoHTTP: hs.Client().Do},
797+
Secrets: []string{"small", "large"},
798+
})
799+
if err != nil {
800+
t.Fatalf("NewStore: unexpected error: %v", err)
801+
}
802+
defer st.Close()
803+
804+
small := st.Secret("small")
805+
if small.GetString() != "1" {
806+
t.Errorf("Value of small before: got %q, want 1", small.GetString())
807+
}
808+
809+
// Update small to a new value, and delete large.
810+
// This means a refresh will partially fail (since "large" is now gone).
811+
// But the update to "small" should still be observed.
812+
d.MustActivate(d.Superuser, "small", d.MustPut(d.Superuser, "small", "2"))
813+
if err := d.Actual.Delete(d.Superuser, "large"); err != nil {
814+
t.Fatalf("Delete large: unexpected error: %v", err)
815+
}
816+
817+
// Refreshing should still report an error.
818+
if err := st.Refresh(t.Context()); err == nil {
819+
t.Error("Refresh should have reported an error")
820+
} else {
821+
t.Logf("Refresh reported (expected) error: %v", err)
822+
}
823+
824+
if small.GetString() != "2" {
825+
t.Errorf("Value of small after: got %q, want 2", small.GetString())
826+
}
827+
}
828+
786829
func TestNilSecret(t *testing.T) {
787830
var s setec.Secret
788831

0 commit comments

Comments
 (0)