Skip to content

Commit 602b2c6

Browse files
authored
Merge pull request #349 from splitio/FME-16788
[FME-16788] Updated validation starting the proxy with snapshot
2 parents c00c925 + 6b78f75 commit 602b2c6

5 files changed

Lines changed: 205 additions & 16 deletions

File tree

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
5.12.6 (Jun 19, 2026)
2+
- Updated snapshot validation to warn on configuration mismatches instead of blocking proxy initialization.
3+
14
5.12.5 (May 29, 2026)
25
- Fixed vulnerabilities (8 Critical, 3 High, 5 Medium, 3 Low):
36
- C: CVE-2026-39830, CVE-2026-39831, CVE-2026-39832, CVE-2026-39833, CVE-2026-39834, CVE-2026-42508, CVE-2026-39821

splitio/commitversion.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ This file is created automatically, please do not edit
55
*/
66

77
// CommitVersion is the version of the last commit previous to release
8-
const CommitVersion = "db1e39d"
8+
const CommitVersion = "aeef5f7"

splitio/proxy/initialization.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,25 @@ func Start(logger logging.LoggerInterface, cfg *pconf.Main) error {
4747

4848
// Initialization of DB
4949
var dbpath = persistent.BoltInMemoryMode
50+
var snapshotValid = false
5051
if snapFile := cfg.Initialization.Snapshot; snapFile != "" {
5152
snap, err := snapshot.DecodeFromFile(snapFile)
5253
if err != nil {
5354
return fmt.Errorf("error parsing snapshot file: %w", err)
5455
}
5556

56-
dbpath, err = snap.WriteDataToTmpFile()
57-
if err != nil {
58-
return fmt.Errorf("error writing temporary snapshot file: %w", err)
59-
}
60-
6157
currentHash := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::"))
6258
if snap.Meta().Hash != strconv.Itoa(int(currentHash)) {
63-
return common.NewInitError(errors.New("snapshot cfg (apikey, version, flagsets) does not match the provided one"), common.ExitErrorDB)
59+
logger.Warning("snapshot cfg (apikey, version, flagsets) does not match the provided one. Ignoring snapshot and starting with empty storage.")
60+
} else {
61+
// Hash matches - use the snapshot
62+
dbpath, err = snap.WriteDataToTmpFile()
63+
if err != nil {
64+
return fmt.Errorf("error writing temporary snapshot file: %w", err)
65+
}
66+
snapshotValid = true
67+
logger.Debug("Database created from snapshot at", dbpath)
6468
}
65-
66-
logger.Debug("Database created from snapshot at", dbpath)
6769
}
6870

6971
dbInstance, err := persistent.NewBoltWrapper(dbpath, nil)
@@ -89,9 +91,9 @@ func Start(logger logging.LoggerInterface, cfg *pconf.Main) error {
8991
splitAPI := api.NewSplitAPI(cfg.Apikey, *advanced, logger, metadata)
9092

9193
// Proxy storages already implement the observable interface, so no need to wrap them
92-
splitStorage := storage.NewProxySplitStorage(dbInstance, logger, flagsets.NewFlagSetFilter(cfg.FlagSetsFilter), cfg.Initialization.Snapshot != "")
93-
ruleBasedStorage := storage.NewProxyRuleBasedSegmentsStorage(dbInstance, logger, cfg.Initialization.Snapshot != "")
94-
segmentStorage := storage.NewProxySegmentStorage(dbInstance, logger, cfg.Initialization.Snapshot != "")
94+
splitStorage := storage.NewProxySplitStorage(dbInstance, logger, flagsets.NewFlagSetFilter(cfg.FlagSetsFilter), snapshotValid)
95+
ruleBasedStorage := storage.NewProxyRuleBasedSegmentsStorage(dbInstance, logger, snapshotValid)
96+
segmentStorage := storage.NewProxySegmentStorage(dbInstance, logger, snapshotValid)
9597
largeSegmentStorage := inmemory.NewLargeSegmentsStorage()
9698

9799
// Local telemetry
@@ -176,13 +178,13 @@ func Start(logger logging.LoggerInterface, cfg *pconf.Main) error {
176178
return common.NewInitError(fmt.Errorf("error instantiating sync manager: %w", err), common.ExitTaskInitialization)
177179
}
178180

179-
// Try to start bg sync in BG with unlimited retries (when a snapshot is provided),
181+
// Try to start bg sync in BG with unlimited retries (when a valid snapshot is provided),
180182
// the passed function is invoked upon initialization completion
181-
// If no snapshot is provided and init fails, `errUnrecoverable` is returned and application execution is aborted
183+
// If no valid snapshot is provided and init fails, `errUnrecoverable` is returned and application execution is aborted
182184
// health monitors are only started after successful init (otherwise they'll fail if the app doesn't sync correctly within the
183185
/// specified refresh period)
184186
before := time.Now()
185-
err = startBGSync(syncManager, mstatus, cfg.Initialization.Snapshot != "", func() {
187+
err = startBGSync(syncManager, mstatus, snapshotValid, func() {
186188
logger.Info("Synchronizer tasks started")
187189
appMonitor.Start()
188190
servicesMonitor.Start()

splitio/proxy/initialization_test.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
package proxy
22

33
import (
4+
"fmt"
5+
"os"
6+
"strconv"
7+
"strings"
48
"sync/atomic"
59
"testing"
610
"time"
711

812
"github.com/splitio/go-split-commons/v9/synchronizer"
13+
"github.com/splitio/go-toolkit/v5/logging"
14+
"github.com/splitio/split-synchronizer/v5/splitio/common/snapshot"
15+
pconf "github.com/splitio/split-synchronizer/v5/splitio/proxy/conf"
16+
"github.com/splitio/split-synchronizer/v5/splitio/util"
917
)
1018

1119
type syncManagerMock struct {
@@ -67,3 +75,179 @@ func TestSyncManagerInitializationRetriesWithSnapshot(t *testing.T) {
6775
t.Error("there should be 2 executions")
6876
}
6977
}
78+
79+
// mockLogger captures warning messages for testing
80+
type mockLogger struct {
81+
logging.LoggerInterface
82+
warnings []string
83+
}
84+
85+
func (m *mockLogger) Warning(msg ...interface{}) {
86+
m.warnings = append(m.warnings, fmt.Sprint(msg...))
87+
}
88+
89+
func (m *mockLogger) Debug(msg ...interface{}) {}
90+
func (m *mockLogger) Error(msg ...interface{}) {}
91+
func (m *mockLogger) Info(msg ...interface{}) {}
92+
93+
func TestSnapshotHashMismatchLogsWarningAndIgnoresSnapshot(t *testing.T) {
94+
// Create a temporary snapshot file with a specific hash
95+
tmpDir, err := os.MkdirTemp("", "snapshot-test")
96+
if err != nil {
97+
t.Fatalf("failed to create temp dir: %v", err)
98+
}
99+
defer os.RemoveAll(tmpDir)
100+
101+
snapshotPath := tmpDir + "/test-snapshot.snap"
102+
103+
// Create a snapshot with hash "12345"
104+
meta := snapshot.Metadata{
105+
Version: 1,
106+
Storage: 1,
107+
Hash: "12345",
108+
}
109+
snap, err := snapshot.New(meta, []byte("test data"))
110+
if err != nil {
111+
t.Fatalf("failed to create snapshot: %v", err)
112+
}
113+
114+
// Encode snapshot to bytes and write to file
115+
encoded, err := snap.Encode()
116+
if err != nil {
117+
t.Fatalf("failed to encode snapshot: %v", err)
118+
}
119+
120+
if err := os.WriteFile(snapshotPath, encoded, 0644); err != nil {
121+
t.Fatalf("failed to write snapshot file: %v", err)
122+
}
123+
124+
// Create a config with different apikey/version/flagsets that will produce a different hash
125+
cfg := &pconf.Main{
126+
Apikey: "test-apikey",
127+
FlagSpecVersion: "1.1",
128+
FlagSetsFilter: []string{"set1", "set2"},
129+
Initialization: pconf.Initialization{
130+
Snapshot: snapshotPath,
131+
},
132+
}
133+
134+
// Calculate what the hash would be with this config
135+
currentHash := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::"))
136+
expectedHashStr := strconv.Itoa(int(currentHash))
137+
138+
// Verify that the hashes are indeed different
139+
if meta.Hash == expectedHashStr {
140+
t.Fatal("test setup error: hashes should be different for this test")
141+
}
142+
143+
// Create a mock logger to capture warnings
144+
mockLog := &mockLogger{
145+
warnings: make([]string, 0),
146+
}
147+
148+
// This simulates the code path in Start() that checks the hash
149+
snap2, err := snapshot.DecodeFromFile(snapshotPath)
150+
if err != nil {
151+
t.Fatalf("failed to decode snapshot: %v", err)
152+
}
153+
154+
// Simulate the new behavior: check hash and ignore if mismatch
155+
var snapshotValid = false
156+
currentHash2 := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::"))
157+
if snap2.Meta().Hash != strconv.Itoa(int(currentHash2)) {
158+
mockLog.Warning("snapshot cfg (apikey, version, flagsets) does not match the provided one. Ignoring snapshot and starting with empty storage.")
159+
} else {
160+
snapshotValid = true
161+
}
162+
163+
// Verify that a warning was logged
164+
if len(mockLog.warnings) != 1 {
165+
t.Errorf("expected 1 warning, got %d", len(mockLog.warnings))
166+
}
167+
168+
expectedWarning := "snapshot cfg (apikey, version, flagsets) does not match the provided one. Ignoring snapshot and starting with empty storage."
169+
if len(mockLog.warnings) > 0 && mockLog.warnings[0] != expectedWarning {
170+
t.Errorf("expected warning '%s', got '%s'", expectedWarning, mockLog.warnings[0])
171+
}
172+
173+
// Verify that snapshot is marked as invalid
174+
if snapshotValid {
175+
t.Error("snapshot should be marked as invalid when hash mismatches")
176+
}
177+
}
178+
179+
func TestSnapshotHashMatchNoWarningAndUsesSnapshot(t *testing.T) {
180+
// Create a temporary snapshot file
181+
tmpDir, err := os.MkdirTemp("", "snapshot-test")
182+
if err != nil {
183+
t.Fatalf("failed to create temp dir: %v", err)
184+
}
185+
defer os.RemoveAll(tmpDir)
186+
187+
snapshotPath := tmpDir + "/test-snapshot.snap"
188+
189+
// Create config
190+
cfg := &pconf.Main{
191+
Apikey: "test-apikey",
192+
FlagSpecVersion: "1.1",
193+
FlagSetsFilter: []string{"set1", "set2"},
194+
Initialization: pconf.Initialization{
195+
Snapshot: snapshotPath,
196+
},
197+
}
198+
199+
// Calculate the hash with the config
200+
currentHash := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::"))
201+
hashStr := strconv.Itoa(int(currentHash))
202+
203+
// Create a snapshot with the SAME hash
204+
meta := snapshot.Metadata{
205+
Version: 1,
206+
Storage: 1,
207+
Hash: hashStr,
208+
}
209+
snap, err := snapshot.New(meta, []byte("test data"))
210+
if err != nil {
211+
t.Fatalf("failed to create snapshot: %v", err)
212+
}
213+
214+
// Encode snapshot to bytes and write to file
215+
encoded, err := snap.Encode()
216+
if err != nil {
217+
t.Fatalf("failed to encode snapshot: %v", err)
218+
}
219+
220+
if err := os.WriteFile(snapshotPath, encoded, 0644); err != nil {
221+
t.Fatalf("failed to write snapshot file: %v", err)
222+
}
223+
224+
// Create a mock logger to capture warnings
225+
mockLog := &mockLogger{
226+
warnings: make([]string, 0),
227+
}
228+
229+
// This simulates the code path in Start() that checks the hash
230+
snap2, err := snapshot.DecodeFromFile(snapshotPath)
231+
if err != nil {
232+
t.Fatalf("failed to decode snapshot: %v", err)
233+
}
234+
235+
// Simulate the new behavior: check hash and use if match
236+
var snapshotValid = false
237+
currentHash2 := util.HashAPIKey(cfg.Apikey + cfg.FlagSpecVersion + strings.Join(cfg.FlagSetsFilter, "::"))
238+
if snap2.Meta().Hash != strconv.Itoa(int(currentHash2)) {
239+
mockLog.Warning("snapshot cfg (apikey, version, flagsets) does not match the provided one. Ignoring snapshot and starting with empty storage.")
240+
} else {
241+
snapshotValid = true
242+
}
243+
244+
// Verify that NO warning was logged
245+
if len(mockLog.warnings) != 0 {
246+
t.Errorf("expected 0 warnings, got %d: %v", len(mockLog.warnings), mockLog.warnings)
247+
}
248+
249+
// Verify that snapshot is marked as valid
250+
if !snapshotValid {
251+
t.Error("snapshot should be marked as valid when hash matches")
252+
}
253+
}

splitio/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
package splitio
33

44
// Version is the version of this Agent
5-
const Version = "5.12.5"
5+
const Version = "5.12.6"

0 commit comments

Comments
 (0)