4.1 KiB · text 5af55d9
defmodule GitGud.Workflows.CacheEvictionTest do
use GitGud.DataCase, async: false
alias GitGud.Repo
alias GitGud.Workflows.ActionsCache
alias GitGud.Workflows.CacheEntry
alias GitGud.Workflows.Workers.CacheEviction
import GitGud.ForgeFixtures
setup do
# Reset config between tests since the worker reads at runtime.
on_exit(fn -> Application.delete_env(:git_gud, CacheEviction) end)
{_user, repo} = repository_fixture()
{:ok, repo: repo}
end
defp finalize_with_size(entry, size_bytes) do
body = String.duplicate("x", size_bytes)
:ok = ActionsCache.append(entry, body)
{:ok, entry} = ActionsCache.finalize(entry, size_bytes)
entry
end
defp backdate(entry, days) do
ago = DateTime.utc_now() |> DateTime.add(-days * 86_400, :second) |> DateTime.truncate(:second)
{1, _} =
Repo.update_all(
from(c in CacheEntry, where: c.id == ^entry.id),
set: [last_used_at: ago, archived_at: ago]
)
Repo.get!(CacheEntry, entry.id)
end
test "prunes entries older than max_age_days", %{repo: repo} do
Application.put_env(:git_gud, CacheEviction,
max_age_days: 7,
per_repo_max_mb: nil
)
{:ok, fresh} = ActionsCache.reserve(repo, "fresh", "v", "refs/heads/main")
fresh = finalize_with_size(fresh, 100)
{:ok, stale} = ActionsCache.reserve(repo, "stale", "v", "refs/heads/main")
_ = finalize_with_size(stale, 100) |> backdate(30)
:ok = CacheEviction.run_now()
remaining = Repo.all(from c in CacheEntry, where: c.repository_id == ^repo.id)
assert Enum.map(remaining, & &1.id) == [fresh.id]
end
test "per_repo_max_mb evicts oldest by last_used_at first", %{repo: repo} do
# Cap at 1 MB; each entry is ~512KB; 3 entries → 1.5MB total →
# the oldest one gets evicted (LRU on last_used_at).
Application.put_env(:git_gud, CacheEviction,
max_age_days: nil,
per_repo_max_mb: 1
)
size = 512 * 1024
{:ok, oldest_e} = ActionsCache.reserve(repo, "a", "v", "refs/heads/main")
oldest = finalize_with_size(oldest_e, size) |> backdate(3)
{:ok, mid_e} = ActionsCache.reserve(repo, "b", "v", "refs/heads/main")
mid = finalize_with_size(mid_e, size) |> backdate(2)
{:ok, newest_e} = ActionsCache.reserve(repo, "c", "v", "refs/heads/main")
_newest = finalize_with_size(newest_e, size) |> backdate(1)
:ok = CacheEviction.run_now()
surviving =
Repo.all(from c in CacheEntry, where: c.repository_id == ^repo.id, select: c.key)
|> Enum.sort()
# `c` is newest, `b` keeps within cap, `a` exceeded → evicted
refute "a" in surviving
assert "b" in surviving
assert "c" in surviving
refute oldest.id in Enum.map(surviving, & &1)
refute mid.id in Enum.map(surviving, & &1)
end
test "dry_run logs but does not evict", %{repo: repo} do
Application.put_env(:git_gud, CacheEviction,
max_age_days: 1,
per_repo_max_mb: nil,
dry_run: true
)
{:ok, stale_e} = ActionsCache.reserve(repo, "old", "v", "refs/heads/main")
_ = finalize_with_size(stale_e, 100) |> backdate(30)
:ok = CacheEviction.run_now()
[row] = Repo.all(from c in CacheEntry, where: c.repository_id == ^repo.id)
assert row.key == "old"
end
test "nil knobs are a no-op", %{repo: repo} do
Application.put_env(:git_gud, CacheEviction,
max_age_days: nil,
per_repo_max_mb: nil
)
{:ok, e} = ActionsCache.reserve(repo, "k", "v", "refs/heads/main")
_ = finalize_with_size(e, 100) |> backdate(365)
:ok = CacheEviction.run_now()
assert [_] = Repo.all(from c in CacheEntry, where: c.repository_id == ^repo.id)
end
test "deletes storage object before the row", %{repo: repo} do
Application.put_env(:git_gud, CacheEviction,
max_age_days: 1,
per_repo_max_mb: nil
)
{:ok, e} = ActionsCache.reserve(repo, "k", "v", "refs/heads/main")
e = finalize_with_size(e, 100) |> backdate(30)
assert {:ok, _} = GitGud.Storage.get(ActionsCache.bucket(), e.storage_key)
:ok = CacheEviction.run_now()
assert {:error, _} = GitGud.Storage.get(ActionsCache.bucket(), e.storage_key)
assert is_nil(Repo.get(CacheEntry, e.id))
end
end