Skip to content

Commit 97c5178

Browse files
committed
[raft/memstore] Add Checkpoint/Restore
1 parent 6b7c7a4 commit 97c5178

6 files changed

Lines changed: 187 additions & 0 deletions

File tree

pkg/aux_/store/memstore/store.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
auxmodels "github.com/interuss/dss/pkg/aux_/models"
88
"github.com/interuss/dss/pkg/aux_/repos"
99
"github.com/interuss/dss/pkg/memstore"
10+
"github.com/interuss/stacktrace"
1011
"go.uber.org/zap"
1112
)
1213

@@ -50,3 +51,33 @@ func Init(ctx context.Context, logger *zap.Logger) (*memstore.Store[repos.Reposi
5051
}
5152

5253
func (r *repo) GetRepo() repos.Repository { return r }
54+
55+
// clone returns a copy of s with independent maps and participant records.
56+
func (s state) clone() state {
57+
ps := make(map[string]*participant, len(s.Participants))
58+
for k, v := range s.Participants {
59+
cp := *v
60+
ps[k] = &cp
61+
}
62+
hb := make(map[heartbeatKey]auxmodels.Heartbeat, len(s.Heartbeats))
63+
for k, v := range s.Heartbeats {
64+
hb[k] = v
65+
}
66+
return state{Participants: ps, Heartbeats: hb}
67+
}
68+
69+
// Checkpoint returns a fast, restorable in-memory copy of the current state.
70+
func (r *repo) Checkpoint() any {
71+
return r.state.clone()
72+
}
73+
74+
// Restore replaces the current state with a checkpoint previously returned by
75+
// Checkpoint. The checkpoint is copied, so it stays reusable.
76+
func (r *repo) Restore(cp any) error {
77+
s, ok := cp.(state)
78+
if !ok {
79+
return stacktrace.NewError("Invalid checkpoint type %T", cp)
80+
}
81+
r.state = s.clone()
82+
return nil
83+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package memstore
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestCheckpointRestore(t *testing.T) {
11+
ctx := context.Background()
12+
r := newRepo()
13+
14+
require.NoError(t, r.SaveOwnMetadata(ctx, "dss-1", "https://example.com"))
15+
16+
cp := r.Checkpoint()
17+
18+
// Mutate after the checkpoint.
19+
require.NoError(t, r.SaveOwnMetadata(ctx, "dss-2", "https://other.example.com"))
20+
md, err := r.GetDSSMetadata(ctx)
21+
require.NoError(t, err)
22+
require.Len(t, md, 2)
23+
24+
// Restore drops dss-2.
25+
require.NoError(t, r.Restore(cp))
26+
md, err = r.GetDSSMetadata(ctx)
27+
require.NoError(t, err)
28+
require.Len(t, md, 1)
29+
require.Equal(t, "dss-1", md[0].Locality)
30+
}
31+
32+
func TestCheckpointIsolatesUpsert(t *testing.T) {
33+
ctx := context.Background()
34+
r := newRepo()
35+
36+
require.NoError(t, r.SaveOwnMetadata(ctx, "dss-1", "https://old.example.com"))
37+
38+
cp := r.Checkpoint()
39+
40+
require.NoError(t, r.SaveOwnMetadata(ctx, "dss-1", "https://new.example.com"))
41+
42+
require.NoError(t, r.Restore(cp))
43+
md, err := r.GetDSSMetadata(ctx)
44+
require.NoError(t, err)
45+
require.Len(t, md, 1)
46+
require.Equal(t, "https://old.example.com", md[0].PublicEndpoint)
47+
}
48+
49+
func TestRestoreInvalidType(t *testing.T) {
50+
require.Error(t, newRepo().Restore("not a checkpoint"))
51+
}

pkg/memstore/store.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ type MemRepo[R any] interface {
2222
GetRepo() R
2323
GetSnapshot() ([]byte, error)
2424
RestoreFromSnapshot([]byte) error
25+
Checkpoint() any
26+
Restore(any) error
2527
}
2628

2729
type Store[R any] struct {
@@ -62,6 +64,16 @@ func (s *Store[R]) Interact(_ context.Context) (R, error) {
6264
return s.memRepo.GetRepo(), nil
6365
}
6466

67+
// Checkpoint returns a fast, restorable in-memory copy of the current state.
68+
func (s *Store[R]) Checkpoint() any {
69+
return s.memRepo.Checkpoint()
70+
}
71+
72+
// Restore replaces the current state with a checkpoint returned by Checkpoint.
73+
func (s *Store[R]) Restore(cp any) error {
74+
return s.memRepo.Restore(cp)
75+
}
76+
6577
func (s *Store[R]) Close() error {
6678
return nil
6779
}

pkg/rid/store/memstore/store.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,37 @@ func cloneFloat32(f *float32) *float32 {
134134
v := *f
135135
return &v
136136
}
137+
138+
// clone returns a copy of s with independent maps and records. Cell slices and
139+
// time pointers are shared, as they are never mutated in place.
140+
func (s state) clone() state {
141+
isas := make(map[dssmodels.ID]*isaRecord, len(s.ISAs))
142+
for id, rec := range s.ISAs {
143+
cp := *rec
144+
isas[id] = &cp
145+
}
146+
subs := make(map[dssmodels.ID]*subscriptionRecord, len(s.Subscriptions))
147+
for id, rec := range s.Subscriptions {
148+
cp := *rec
149+
subs[id] = &cp
150+
}
151+
return state{ISAs: isas, Subscriptions: subs}
152+
}
153+
154+
// Checkpoint returns a fast, restorable in-memory copy of the current state.
155+
// Unlike GetSnapshot it does not serialize, so it is cheap but only valid
156+
// in-process.
157+
func (r *repo) Checkpoint() any {
158+
return r.state.clone()
159+
}
160+
161+
// Restore replaces the current state with a checkpoint previously returned by
162+
// Checkpoint. The checkpoint is copied, so it stays reusable.
163+
func (r *repo) Restore(cp any) error {
164+
s, ok := cp.(state)
165+
if !ok {
166+
return stacktrace.NewError("Invalid checkpoint type %T", cp)
167+
}
168+
r.state = s.clone()
169+
return nil
170+
}

pkg/rid/store/memstore/store_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,53 @@ func TestDatabaseEnsuresBeginsBeforeExpires(t *testing.T) {
4545
})
4646
require.Error(t, err)
4747
}
48+
49+
func TestCheckpointRestoreISA(t *testing.T) {
50+
ctx := context.Background()
51+
repo := setUpStore(t)
52+
53+
_, err := repo.InsertISA(ctx, serviceArea)
54+
require.NoError(t, err)
55+
56+
cp := repo.Checkpoint()
57+
58+
// Mutate after the checkpoint.
59+
isa, err := repo.GetISA(ctx, serviceArea.ID, false)
60+
require.NoError(t, err)
61+
_, err = repo.DeleteISA(ctx, isa)
62+
require.NoError(t, err)
63+
gone, err := repo.GetISA(ctx, serviceArea.ID, false)
64+
require.NoError(t, err)
65+
require.Nil(t, gone)
66+
67+
// Restore brings it back.
68+
require.NoError(t, repo.Restore(cp))
69+
back, err := repo.GetISA(ctx, serviceArea.ID, false)
70+
require.NoError(t, err)
71+
require.NotNil(t, back)
72+
}
73+
74+
func TestCheckpointIsolatesNotificationIndex(t *testing.T) {
75+
ctx := context.Background()
76+
repo := setUpStore(t)
77+
78+
sub, err := repo.InsertSubscription(ctx, subscriptionsPool[0].input)
79+
require.NoError(t, err)
80+
81+
cp := repo.Checkpoint()
82+
83+
// In-place notification-index bump must not leak into the checkpoint.
84+
updated, err := repo.UpdateNotificationIdxsInCells(ctx, sub.Cells)
85+
require.NoError(t, err)
86+
require.Len(t, updated, 1)
87+
require.Equal(t, sub.NotificationIndex+1, updated[0].NotificationIndex)
88+
89+
require.NoError(t, repo.Restore(cp))
90+
restored, err := repo.GetSubscription(ctx, sub.ID)
91+
require.NoError(t, err)
92+
require.Equal(t, sub.NotificationIndex, restored.NotificationIndex)
93+
}
94+
95+
func TestRestoreInvalidType(t *testing.T) {
96+
require.Error(t, setUpStore(t).Restore("not a checkpoint"))
97+
}

pkg/scd/store/memstore/store.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/interuss/dss/pkg/memstore"
77
"github.com/interuss/dss/pkg/scd/repos"
8+
"github.com/interuss/stacktrace"
89
"go.uber.org/zap"
910
)
1011

@@ -16,3 +17,11 @@ func Init(ctx context.Context, logger *zap.Logger) (*memstore.Store[repos.Reposi
1617
}
1718

1819
func (r *repo) GetRepo() repos.Repository { return r }
20+
21+
func (r *repo) Checkpoint() any {
22+
return nil
23+
}
24+
25+
func (r *repo) Restore(any) error {
26+
return stacktrace.NewError("Restore not yet implemented for scd")
27+
}

0 commit comments

Comments
 (0)