test: cover Excache.StoreConsistencyTest
This commit is contained in:
1 parent
290c662fb2
commit
bdf88ca1ca
1 file changed
+159
@@ -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"])
|
||||
|
||||
Reference in new issue
Block a user