test: cover Excache.StoreConsistencyTest

This commit is contained in:
dualfroz committed 2026-09-13 18:57:50 +02:00
1 parent 290c662fb2
commit bdf88ca1ca
1 file changed
+159
+159
View File
@@ -0,0 +1,159 @@
defmodule Excache.StoreConsistencyTest do
use Excache.Test.ServerCase, async: false
alias Excache.Aof
alias Excache.Config
alias Excache.Eviction.Tracker
alias Excache.Store
@ttlMs 200
@holdMs 400
setup do
Tracker.clear(Tracker)
on_exit(fn ->
Config.reset()
Tracker.clear(Tracker)
end)
:ok
end
describe "RENAME must not drop the source when the destination write is refused" do
test "an expired source is reported instead of being silently swallowed" do
source = "cons:rename:src"
dest = keyInOtherShard("cons:rename:dst-", source)
assert :ok = Store.set(source, "payload", px: @ttlMs)
assert {:error, :nokey} =
duringSuspendedShard(dest, fn -> Store.rename(source, dest) end)
assert Store.exists([dest]) == 0
assert Store.exists([source]) == 0
end
test "an existing destination is never reported as overwritten when it was not" do
source = "cons:rename:live-src"
dest = keyInOtherShard("cons:rename:live-dst-", source)
assert :ok = Store.set(dest, "stale", [])
assert :ok = Store.set(source, "fresh", px: @ttlMs)
assert {:error, :nokey} =
duringSuspendedShard(dest, fn -> Store.rename(source, dest) end)
assert {:ok, "stale"} = Store.get(dest)
end
test "a healthy rename still carries the value and the remaining ttl" do
source = "cons:rename:ok-src"
dest = keyInOtherShard("cons:rename:ok-dst-", source)
assert :ok = Store.set(source, "payload", px: 60_000)
assert {:ok, "OK"} = Store.rename(source, dest)
assert {:ok, "payload"} = Store.get(dest)
assert Store.exists([source]) == 0
remaining = Store.ttl(dest, :seconds)
assert remaining > 0 and remaining <= 60
end
end
describe "RENAMENX must not drop the source when the destination write is refused" do
test "an expired source is reported instead of counted as a rename" do
source = "cons:renamenx:src"
dest = keyInOtherShard("cons:renamenx:dst-", source)
assert :ok = Store.set(source, "payload", px: @ttlMs)
assert {:error, :nokey} =
duringSuspendedShard(dest, fn -> Store.renamenx(source, dest) end)
assert Store.exists([dest]) == 0
end
test "an occupied destination still refuses the rename and keeps both keys" do
source = "cons:renamenx:busy-src"
dest = keyInOtherShard("cons:renamenx:busy-dst-", source)
assert :ok = Store.set(source, "payload", [])
assert :ok = Store.set(dest, "taken", [])
assert {:ok, 0} = Store.renamenx(source, dest)
assert {:ok, "payload"} = Store.get(source)
assert {:ok, "taken"} = Store.get(dest)
end
end
describe "COPY must not destroy the destination when the write is refused" do
test "an expiring source leaves the existing destination intact" do
source = "cons:copy:src"
dest = keyInOtherShard("cons:copy:dst-", source)
assert :ok = Store.set(dest, "keep-me", [])
assert :ok = Store.set(source, "payload", px: @ttlMs)
assert {:ok, 0} =
duringSuspendedShard(dest, fn -> Store.copy(source, dest, true) end)
assert {:ok, "keep-me"} = Store.get(dest)
end
test "replacing a destination of a different type still overwrites it" do
source = "cons:copy:swap-src"
dest = keyInOtherShard("cons:copy:swap-dst-", source)
assert {:ok, 1} = Store.rpush(dest, ["old"])
assert :ok = Store.set(source, "fresh", [])
assert {:ok, 1} = Store.copy(source, dest, true)
assert {:ok, "fresh"} = Store.get(dest)
assert Store.type(dest) == "string"
end
test "copying onto an occupied destination without replace changes nothing" do
source = "cons:copy:keep-src"
dest = keyInOtherShard("cons:copy:keep-dst-", source)
assert :ok = Store.set(source, "fresh", [])
assert :ok = Store.set(dest, "untouched", [])
assert {:ok, 0} = Store.copy(source, dest, false)
assert {:ok, "untouched"} = Store.get(dest)
end
end
describe "SPOP must reach the log as the removal it actually performed" do
test "the replayed log rebuilds exactly the surviving members", %{conn: conn} do
key = "cons:pop:members"
members = Enum.map(1..20, &"member#{&1}")
assert {20, conn} = Client.command(conn, ["SADD", key | members])
{taken, conn} = Client.command(conn, ["SPOP", key, "5"])
assert length(taken) == 5
{single, conn} = Client.command(conn, ["SPOP", key])
assert single in members
{survivors, conn} = Client.command(conn, ["SMEMBERS", key])
assert length(survivors) == 14
log = flushedLog()
refute log =~ "SPOP"
assert log =~ "SREM"
conn = replayFromScratch(conn)
{replayed, _conn} = Client.command(conn, ["SMEMBERS", key])
assert Enum.sort(replayed) == Enum.sort(survivors)
end
test "draining the whole set replays as the key being gone", %{conn: conn} do
key = "cons:pop:drained"
assert {3, conn} = Client.command(conn, ["SADD", key, "a", "b", "c"])