Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a32263d
test(jepsen): add ZSet safety workload with model-based checker
bootjp Apr 19, 2026
2693a30
ci(jepsen): run ZSet safety workload in per-push and scheduled jobs
bootjp Apr 19, 2026
94be1bd
fix(jepsen-zset-safety): plug checker false positives + add unit tests
bootjp Apr 19, 2026
9bfcc13
fix(jepsen-zset-safety): no-op-ZREM-only member must not trigger :sco…
bootjp Apr 20, 2026
6d0b4c3
fix(jepsen-zset-safety): address CodeRabbit checker soundness issues
bootjp Apr 20, 2026
86d23dd
Merge branch 'main' into feat/jepsen-zset-safety
bootjp Apr 21, 2026
ecb3983
fix(jepsen): correct ZSet checker for infinity, stale reads, and :inf…
bootjp Apr 22, 2026
2a194a4
fix(jepsen): accept linearization of concurrent ops and uncertain mut…
bootjp Apr 22, 2026
49c5e0c
Merge branch 'main' into feat/jepsen-zset-safety
bootjp Apr 22, 2026
0c0efc4
fix(jepsen): keep strict score check when concurrent ZINCRBY score is…
bootjp Apr 22, 2026
e5dcc34
fix(jepsen): exclude :fail completions from concurrent mutation uncer…
bootjp Apr 22, 2026
02a8adf
fix(jepsen): restrict committed ZINCRBY candidates to linearization-c…
bootjp Apr 22, 2026
da85560
fix(jepsen): restrict unknown-score? to :info zincrby, not any concur…
bootjp Apr 23, 2026
502f64a
fix(jepsen): guard setup! and ZINCRBY response parsing against nil/mi…
bootjp Apr 23, 2026
33d59c5
fix(jepsen): restrict can-be-present? existence evidence to ZADD/ZINCRBY
bootjp Apr 23, 2026
9535ff3
fix(jepsen): correct clojure.tools.logging/warn call style in zset wo…
bootjp Apr 23, 2026
fde116c
fix(jepsen): decode Redis ZSET member bytes as UTF-8 explicitly
bootjp Apr 23, 2026
0c948a2
fix(jepsen-zset): hard-fail setup! when :conn-spec is missing
bootjp Apr 23, 2026
84989f1
fix(jepsen-zset): prepend test subcommand only when absent or an option
bootjp Apr 23, 2026
7a7a218
fix(jepsen-zset): document why :final-generator is overridden to nil
bootjp Apr 23, 2026
6219831
fix(jepsen-zset): guard ZREM nil reply to avoid NPE in invoke!
bootjp Apr 23, 2026
e67d29f
fix(jepsen-zset): return :valid? :unknown when no successful reads
bootjp Apr 23, 2026
623d5c2
fix(jepsen-zset): hard-fail setup! when cleanup DEL errors
bootjp Apr 23, 2026
22e41e1
fix(jepsen-zset): guard nil .getMessage on exception :error fields
bootjp Apr 23, 2026
9f5e958
docs(jepsen-zset): strip LLM reviewer artifact markers from comments
bootjp Apr 23, 2026
d0c8a03
fix(jepsen-zset): let inner workload's :final-generator pass through
bootjp Apr 23, 2026
559e83d
fix(jepsen-zset): reject odd-length WITHSCORES replies
bootjp Apr 23, 2026
1a9370f
fix(jepsen-zset): coerce ZREM count across Long / string / bytes
bootjp Apr 23, 2026
ad9079c
fix(jepsen-zset): include full :allowed set in missing-member-range
bootjp Apr 23, 2026
69db24e
fix(jepsen-zset): catch Throwable in invoke! so Errors don't crash wo…
bootjp Apr 23, 2026
d03672e
Merge branch 'main' into feat/jepsen-zset-safety
bootjp Apr 25, 2026
29e62ca
Merge branch 'main' into feat/jepsen-zset-safety
bootjp Apr 25, 2026
03ce992
jepsen: tighten zset safety checker
bootjp Jun 23, 2026
d31d8b9
jepsen: tighten zset safety checker
bootjp Jun 23, 2026
d094a85
jepsen: tighten zset safety linearization
bootjp Jun 23, 2026
c514791
jepsen: fix zset safety regressions
bootjp Jun 23, 2026
a2b57b8
jepsen: tighten zset prefix anchors
bootjp Jun 23, 2026
31f156c
Merge remote-tracking branch 'origin/main' into HEAD
bootjp Jun 25, 2026
d74cfed
adapter: fix zset range tie ordering
bootjp Jun 25, 2026
72e4c53
test: stabilize CI race checks
bootjp Jun 25, 2026
c3edbfb
adapter: fallback on capped zset range scans
bootjp Jun 25, 2026
a03c2cf
jepsen: fix bounded zset prefix checker
bootjp Jun 25, 2026
62e8fd6
Address zset safety review feedback
bootjp Jul 3, 2026
e704351
Fix zset range prefix anchoring
bootjp Jul 3, 2026
61273a8
Handle zset absence review cases
bootjp Jul 3, 2026
bda154b
Retry DynamoDB type setup on transient errors
bootjp Jul 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/jepsen-test-scheduled.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,16 @@ jobs:
--max-txn-length ${{ inputs.max-txn-length || '4' }} \
--ports 63791,63792,63793 \
--host 127.0.0.1
- name: Run Redis ZSet safety Jepsen workload against elastickv
working-directory: jepsen
timeout-minutes: 10
run: |
timeout 480 ~/lein run -m elastickv.redis-zset-safety-workload \
--time-limit ${{ inputs.time-limit || '150' }} \
--rate ${{ inputs.rate || '10' }} \
--concurrency ${{ inputs.concurrency || '8' }} \
--ports 63791,63792,63793 \
--host 127.0.0.1
- name: Run DynamoDB Jepsen workload against elastickv
working-directory: jepsen
timeout-minutes: 10
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/jepsen-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ jobs:
timeout-minutes: 3
run: |
timeout 120 ~/lein run -m elastickv.redis-workload --time-limit 5 --rate 5 --concurrency 5 --ports 63791,63792,63793 --host 127.0.0.1
- name: Run Redis ZSet safety Jepsen workload against elastickv
working-directory: jepsen
timeout-minutes: 3
run: |
timeout 120 ~/lein run -m elastickv.redis-zset-safety-workload --time-limit 5 --rate 5 --concurrency 5 --ports 63791,63792,63793 --host 127.0.0.1
- name: Run DynamoDB Jepsen workload against elastickv
working-directory: jepsen
timeout-minutes: 3
Expand Down
9 changes: 5 additions & 4 deletions adapter/redis_compat_commands_stream_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,20 +284,21 @@ func TestRedis_StreamXReadLatencyIsConstant(t *testing.T) {
total = 10_000
probes = 100
)
lastID := ""
for i := range total {
_, err := rdb.XAdd(ctx, &redis.XAddArgs{
id, err := rdb.XAdd(ctx, &redis.XAddArgs{
Stream: "stream-lat",
ID: fmt.Sprintf("%d-0", 1_000_000+i),
ID: "*",
Values: []string{"i", fmt.Sprint(i)},
}).Result()
require.NoError(t, err)
lastID = id
}

afterID := fmt.Sprintf("%d-0", 1_000_000+total-1)
measure := func() time.Duration {
start := time.Now()
streams, err := rdb.XRead(ctx, &redis.XReadArgs{
Streams: []string{"stream-lat", afterID},
Streams: []string{"stream-lat", lastID},
Count: 10,
Block: 10 * time.Millisecond,
}).Result()
Expand Down
48 changes: 31 additions & 17 deletions adapter/redis_lua_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -2623,7 +2623,10 @@ func (c *luaScriptContext) cmdZRangeByScoreSlow(key []byte, options luaZRangeByS
// the whole offset+limit budget on those filtered-out rows and miss
// the real matches at score > value.
//
// The score index is lex-sorted by (userKey, sortableScore, member).
// The score index is grouped by (userKey, sortableScore). Member bytes
// follow the score, but the MVCC timestamp suffix means equal-score
// member ordering is normalized by zsetRangeByScoreFast rather than
// trusted directly from physical scan order.
// Conventions:
//
// minBound = -Inf -> startKey = ZSetScoreScanPrefix(key)
Expand Down Expand Up @@ -3738,26 +3741,37 @@ func parseZScoreBound(raw string) (zScoreBound, error) {
return zScoreBound{kind: zBoundValue, score: score, inclusive: inclusive}, nil
}

func scoreInRange(score float64, minBound zScoreBound, maxBound zScoreBound) bool {
if minBound.kind == zBoundValue {
if minBound.inclusive {
if score < minBound.score {
return false
}
} else if score <= minBound.score {
return false
func scoreSatisfiesMinBound(score float64, bound zScoreBound) bool {
switch bound.kind {
case zBoundValue:
if bound.inclusive {
return score >= bound.score
}
return score > bound.score
case zBoundPosInf:
return math.IsInf(score, +1)
default:
return true
}
if maxBound.kind == zBoundValue {
if maxBound.inclusive {
if score > maxBound.score {
return false
}
} else if score >= maxBound.score {
return false
}

func scoreSatisfiesMaxBound(score float64, bound zScoreBound) bool {
switch bound.kind {
case zBoundNegInf:
return math.IsInf(score, -1)
case zBoundValue:
if bound.inclusive {
return score <= bound.score
}
return score < bound.score
default:
return true
}
return true
}

func scoreInRange(score float64, minBound zScoreBound, maxBound zScoreBound) bool {
return scoreSatisfiesMinBound(score, minBound) &&
scoreSatisfiesMaxBound(score, maxBound)
}

func cloneSetMembers(in map[string]struct{}) map[string]struct{} {
Expand Down
44 changes: 44 additions & 0 deletions adapter/redis_retry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,50 @@ func TestRedisExecLuaCompatRetriesWriteConflict(t *testing.T) {
require.Equal(t, 1.0, zset.Entries[0].Score)
}

func TestRedisZRemWideColumnRemovesMemberAndScoreIndex(t *testing.T) {
t.Parallel()

ctx := context.Background()
st := store.NewMVCCStore()
coord := newRetryOnceCoordinator(st)

srv := &RedisServer{
store: st,
coordinator: coord,
scriptCache: map[string]string{},
}

key := []byte("retry:zrem-wide")
addConn := &recordingConn{}
srv.zadd(addConn, redcon.Command{Args: [][]byte{
[]byte(cmdZAdd), key,
[]byte("55"), []byte("m8"),
[]byte("-49"), []byte("m6"),
}})
require.Empty(t, addConn.err)
require.Equal(t, int64(2), addConn.int)

remConn := &recordingConn{}
srv.zrem(remConn, redcon.Command{Args: [][]byte{
[]byte(cmdZRem), key, []byte("m8"),
}})
require.Empty(t, remConn.err)
require.Equal(t, int64(1), remConn.int)

readTS := snapshotTS(coord.clock, st)
zset, exists, err := srv.loadZSetAt(ctx, key, readTS)
require.NoError(t, err)
require.True(t, exists)
require.Equal(t, []redisZSetEntry{{Member: "m6", Score: -49}}, zset.Entries)

memberExists, err := st.ExistsAt(ctx, store.ZSetMemberKey(key, []byte("m8")), readTS)
require.NoError(t, err)
require.False(t, memberExists)
scoreExists, err := st.ExistsAt(ctx, store.ZSetScoreKey(key, 55, []byte("m8")), readTS)
require.NoError(t, err)
require.False(t, scoreExists)
}

func TestRedisEvalRetriesWriteConflict(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading