Skip to content

Commit 5515c26

Browse files
authored
Merge pull request #491 from bootjp/feat/jepsen-schedule-enhance
fix: remove HLC from snapshotTS to prevent lost-update on concurrent writes
2 parents 316a2cf + 0ef2c9a commit 5515c26

2 files changed

Lines changed: 58 additions & 10 deletions

File tree

adapter/ts.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,18 @@ import (
55
"github.com/bootjp/elastickv/store"
66
)
77

8-
// snapshotTS picks a safe snapshot timestamp:
9-
// - uses the store's last commit watermark if available,
10-
// - otherwise the coordinator's HLC current value,
11-
// - and falls back to MaxUint64 if neither is set.
12-
func snapshotTS(clock *kv.HLC, st store.MVCCStore) uint64 {
8+
// snapshotTS picks a safe snapshot timestamp based solely on the store's
9+
// last committed watermark. The HLC must NOT be used here because
10+
// clock.Current() can advance ahead of the committed state (e.g. when a
11+
// concurrent write obtains its commitTS via clock.Next()). Using the HLC
12+
// would give a startTS that does not reflect what was actually read,
13+
// allowing the write-conflict check (latestTS > startTS) to miss
14+
// conflicts when startTS == a concurrent transaction's commitTS.
15+
func snapshotTS(_ *kv.HLC, st store.MVCCStore) uint64 {
1316
ts := uint64(0)
1417
if st != nil {
1518
ts = st.LastCommitTS()
1619
}
17-
if clock != nil {
18-
if cur := clock.Current(); cur > ts {
19-
ts = cur
20-
}
21-
}
2220
if ts == 0 {
2321
ts = ^uint64(0)
2422
}

adapter/ts_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package adapter
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/bootjp/elastickv/kv"
8+
"github.com/bootjp/elastickv/store"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestSnapshotTSDoesNotUseHLC(t *testing.T) {
14+
st := store.NewMVCCStore()
15+
require.NoError(t, st.PutAt(context.Background(), []byte("key"), []byte("v"), 10, 0))
16+
17+
clock := kv.NewHLC()
18+
clock.Observe(999) // Advance HLC far beyond LastCommitTS
19+
20+
ts := snapshotTS(clock, st)
21+
assert.Equal(t, st.LastCommitTS(), ts,
22+
"snapshotTS must return LastCommitTS, not HLC.Current()")
23+
assert.Less(t, ts, uint64(999),
24+
"snapshotTS must not advance to HLC value")
25+
}
26+
27+
func TestSnapshotTSFallbackWhenStoreEmpty(t *testing.T) {
28+
clock := kv.NewHLC()
29+
st := store.NewMVCCStore()
30+
31+
ts := snapshotTS(clock, st)
32+
assert.Equal(t, ^uint64(0), ts,
33+
"snapshotTS must return MaxUint64 when store is empty")
34+
}
35+
36+
func TestSnapshotTSNilStore(t *testing.T) {
37+
clock := kv.NewHLC()
38+
ts := snapshotTS(clock, nil)
39+
assert.Equal(t, ^uint64(0), ts,
40+
"snapshotTS must return MaxUint64 when store is nil")
41+
}
42+
43+
func TestSnapshotTSNilClock(t *testing.T) {
44+
st := store.NewMVCCStore()
45+
require.NoError(t, st.PutAt(context.Background(), []byte("k"), []byte("v"), 42, 0))
46+
47+
ts := snapshotTS(nil, st)
48+
assert.Equal(t, uint64(42), ts,
49+
"snapshotTS must work with nil clock")
50+
}

0 commit comments

Comments
 (0)