Skip to content

Commit 6f0b160

Browse files
committed
[raft/memstore] Add snapshop capability to aux memstore
1 parent 2842148 commit 6f0b160

7 files changed

Lines changed: 150 additions & 18 deletions

File tree

pkg/aux_/store/memstore/dss.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,28 @@ func (r *repo) SaveOwnMetadata(_ context.Context, locality string, publicEndpoin
1818
return stacktrace.NewErrorWithCode(dsserr.BadRequest, "Public endpoint not set")
1919
}
2020

21-
r.participants[locality] = &participant{
22-
publicEndpoint: publicEndpoint,
23-
updatedAt: time.Now(),
21+
r.state.Participants[locality] = &participant{
22+
PublicEndpoint: publicEndpoint,
23+
UpdatedAt: time.Now().UTC(),
2424
}
2525
return nil
2626
}
2727

2828
func (r *repo) GetDSSMetadata(_ context.Context) ([]*auxmodels.DSSMetadata, error) {
29-
metadata := make([]*auxmodels.DSSMetadata, 0, len(r.participants))
30-
for locality, p := range r.participants {
31-
updatedAt := p.updatedAt
29+
metadata := make([]*auxmodels.DSSMetadata, 0, len(r.state.Participants))
30+
for locality, p := range r.state.Participants {
31+
updatedAt := p.UpdatedAt
3232
m := &auxmodels.DSSMetadata{
3333
Locality: locality,
34-
PublicEndpoint: p.publicEndpoint,
34+
PublicEndpoint: p.PublicEndpoint,
3535
UpdatedAt: &updatedAt,
3636
}
3737

3838
// Find the latest heartbeat across all sources for this locality.
3939
var latest auxmodels.Heartbeat
4040
found := false
41-
for key, hb := range r.heartbeats {
42-
if key.locality != locality {
41+
for key, hb := range r.state.Heartbeats {
42+
if key.Locality != locality {
4343
continue
4444
}
4545
if !found || hb.Timestamp.After(*latest.Timestamp) {
@@ -69,15 +69,15 @@ func (r *repo) RecordHeartbeat(_ context.Context, heartbeat auxmodels.Heartbeat)
6969
}
7070

7171
if heartbeat.Timestamp == nil {
72-
now := time.Now()
72+
now := time.Now().UTC()
7373
heartbeat.Timestamp = &now
7474
}
7575

7676
if heartbeat.NextHeartbeatExpectedBefore != nil && heartbeat.NextHeartbeatExpectedBefore.Before(*heartbeat.Timestamp) {
7777
return stacktrace.NewErrorWithCode(dsserr.BadRequest, "Cannot expect the timestamp of the next heartbeat before the timestamp of the new heartbeat")
7878
}
7979

80-
r.heartbeats[heartbeatKey{locality: heartbeat.Locality, source: heartbeat.Source}] = heartbeat
80+
r.state.Heartbeats[heartbeatKey{Locality: heartbeat.Locality, Source: heartbeat.Source}] = heartbeat
8181
return nil
8282
}
8383

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package memstore
2+
3+
import (
4+
"bytes"
5+
"encoding/gob"
6+
7+
"github.com/interuss/stacktrace"
8+
)
9+
10+
const snapshotVersion = 1
11+
12+
type snapshotEnvelope struct {
13+
Version int
14+
State state
15+
}
16+
17+
func (r *repo) GetSnapshot() ([]byte, error) {
18+
var buf bytes.Buffer
19+
if err := gob.NewEncoder(&buf).Encode(snapshotEnvelope{Version: snapshotVersion, State: r.state}); err != nil {
20+
return nil, stacktrace.Propagate(err, "Failed to encode memstore snapshot")
21+
}
22+
return buf.Bytes(), nil
23+
}
24+
25+
func (r *repo) RestoreFromSnapshot(data []byte) error {
26+
var env snapshotEnvelope
27+
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&env); err != nil {
28+
return stacktrace.Propagate(err, "Failed to decode memstore snapshot")
29+
}
30+
if env.Version != snapshotVersion {
31+
return stacktrace.NewError("Unsupported memstore snapshot version %d, expected %d", env.Version, snapshotVersion)
32+
}
33+
r.state = env.State
34+
return nil
35+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package memstore
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/gob"
7+
"testing"
8+
"time"
9+
10+
auxmodels "github.com/interuss/dss/pkg/aux_/models"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestSnapshotRoundTrip(t *testing.T) {
15+
ctx := context.Background()
16+
src := newRepo()
17+
require.NoError(t, src.SaveOwnMetadata(ctx, "dss-1", "https://example.com"))
18+
ts := time.Now().UTC()
19+
require.NoError(t, src.RecordHeartbeat(ctx, auxmodels.Heartbeat{Locality: "dss-1", Source: "source-1", Timestamp: &ts, Reporter: "uss-1"}))
20+
21+
data, err := src.GetSnapshot()
22+
require.NoError(t, err)
23+
24+
dst := newRepo()
25+
require.NoError(t, dst.RestoreFromSnapshot(data))
26+
27+
want, err := src.GetDSSMetadata(ctx)
28+
require.NoError(t, err)
29+
got, err := dst.GetDSSMetadata(ctx)
30+
require.NoError(t, err)
31+
require.Equal(t, want, got)
32+
}
33+
34+
func TestRestoreFromSnapshotReplacesState(t *testing.T) {
35+
ctx := context.Background()
36+
src := newRepo()
37+
require.NoError(t, src.SaveOwnMetadata(ctx, "dss-1", "https://example.com"))
38+
data, err := src.GetSnapshot()
39+
require.NoError(t, err)
40+
41+
dst := newRepo()
42+
require.NoError(t, dst.SaveOwnMetadata(ctx, "dss-2", "https://other.example.com"))
43+
require.NoError(t, dst.RestoreFromSnapshot(data))
44+
45+
md, err := dst.GetDSSMetadata(ctx)
46+
require.NoError(t, err)
47+
require.Len(t, md, 1)
48+
require.Equal(t, "dss-1", md[0].Locality)
49+
}
50+
51+
func TestRestoreFromSnapshotInvalidData(t *testing.T) {
52+
require.Error(t, newRepo().RestoreFromSnapshot([]byte("random value that is definitely not valid")))
53+
}
54+
55+
func TestRestoreFromSnapshotVersionMismatch(t *testing.T) {
56+
var buf bytes.Buffer
57+
require.NoError(t, gob.NewEncoder(&buf).Encode(snapshotEnvelope{Version: snapshotVersion + 1}))
58+
require.Error(t, newRepo().RestoreFromSnapshot(buf.Bytes()))
59+
}

pkg/aux_/store/memstore/store.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,37 @@ import (
1212

1313
// repo is a full implementation of aux_.repos.Repository for memory-based storage.
1414
type repo struct {
15+
state state
16+
}
17+
18+
// state is the serializable in-memory state.
19+
type state struct {
20+
// Participants holds pool participants metadata, keyed by locality.
21+
Participants map[string]*participant
22+
// Heartbeats holds the latest heartbeat per (locality, source).
23+
Heartbeats map[heartbeatKey]auxmodels.Heartbeat
1524
// participants holds pool participants metadata, keyed by locality.
1625
participants map[string]*participant
1726
// heartbeats holds the latest heartbeat per (locality, source).
1827
heartbeats map[heartbeatKey]auxmodels.Heartbeat
1928
}
2029

2130
type participant struct {
22-
publicEndpoint string
23-
updatedAt time.Time
31+
PublicEndpoint string
32+
UpdatedAt time.Time
2433
}
2534

2635
type heartbeatKey struct {
27-
locality string
28-
source string
36+
Locality string
37+
Source string
2938
}
3039

3140
func newRepo() *repo {
3241
return &repo{
33-
participants: map[string]*participant{},
34-
heartbeats: map[heartbeatKey]auxmodels.Heartbeat{},
35-
}
42+
state: state{
43+
Participants: map[string]*participant{},
44+
Heartbeats: map[heartbeatKey]auxmodels.Heartbeat{},
45+
}}
3646
}
3747

3848
func Init(ctx context.Context, logger *zap.Logger) (*memstore.Store[repos.Repository], error) {

pkg/memstore/store.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020

2121
type MemRepo[R any] interface {
2222
GetRepo() R
23+
GetSnapshot() ([]byte, error)
24+
RestoreFromSnapshot([]byte) error
2325
}
2426

2527
type Store[R any] struct {

pkg/rid/store/memstore/snapshot.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package memstore
2+
3+
import (
4+
"github.com/interuss/stacktrace"
5+
)
6+
7+
func (r *repo) GetSnapshot() ([]byte, error) {
8+
return nil, stacktrace.NewError("GetSnapshot not yet implemented for rid")
9+
}
10+
11+
func (r *repo) RestoreFromSnapshot(data []byte) error {
12+
return stacktrace.NewError("RestoreFromSnapshot not yet implemented for rid")
13+
}

pkg/scd/store/memstore/snapshot.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package memstore
2+
3+
import (
4+
"github.com/interuss/stacktrace"
5+
)
6+
7+
func (r *repo) GetSnapshot() ([]byte, error) {
8+
return nil, stacktrace.NewError("GetSnapshot not yet implemented for rid")
9+
}
10+
11+
func (r *repo) RestoreFromSnapshot(data []byte) error {
12+
return stacktrace.NewError("RestoreFromSnapshot not yet implemented for rid")
13+
}

0 commit comments

Comments
 (0)