Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 24 additions & 6 deletions lib/schema_cache.ex
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ defmodule SchemaCache do
{:ok, value} ->
Adapter.put(adapter(), cache_key, value, ttl)
associate_key_reference_with_schema(cache_key, value)
associate_key_reference_with_schema_type(cache_key, value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addition of 'associate_key_reference_with_schema_type' here seems redundant. The same operation of associating a key reference with a schema type is also performed below. You might want to clarify its necessity here or consider removing this line.

{:ok, value}

[] ->
Expand Down Expand Up @@ -438,17 +439,19 @@ defmodule SchemaCache do
## Behaviors

* `flush(schema)`: evicts all cache keys referencing the specific
schema instance, identified by its module and primary key values.
* `flush(schema, :new_schema)`: evicts all cached collections for
schema instance (per-record set) **and** all cached collections
for the schema's module type (per-type set). This ensures both
singular lookups and collection queries are invalidated.
* `flush(schema, :new_schema)`: evicts only cached collections for
the schema's module type. Use when a new record has been created
outside of `create/1`.
outside of `create/1` and you don't need per-record eviction.

## Examples

# Evict all cache entries referencing a specific user
# Evict all cache entries for a specific user (both per-record and per-type)
:ok = SchemaCache.flush(user)

# Evict all cached User collections (e.g. after an external insert)
# Evict only cached User collections (e.g. after an external insert)
:ok = SchemaCache.flush(user, :new_schema)
"""
@spec flush(struct(), nil | atom()) :: :ok
Expand All @@ -458,10 +461,12 @@ defmodule SchemaCache do
evict_reference_keys("#{schema_type}")
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes in the function parameters may cause bugs when existing code calls this method. Could you check whether %{}=schema has the same functionality as the previous version of this line (%schema_type{} = schema)?

def flush(%_{} = schema, _opts) do
def flush(%schema_type{} = schema, _opts) do
schema
|> KeyGenerator.schema_cache_key()
|> evict_reference_keys()

evict_reference_keys("#{schema_type}")
end

defp evict_reference_keys(key) do
Expand Down Expand Up @@ -534,6 +539,19 @@ defmodule SchemaCache do
)
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This private function looks fine, but can we improve the performance and readability of this function by avoiding the use of pipelining?


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The '&Adapter.sadd' method returns some value, but we are currently ignoring this return value. It might be more appropriate to use this value, instead of just returning ':ok' in the next line, especially if it returns error status or extra information that could be helpful for diagnosis.

defp associate_key_reference_with_schema_type(cache_key, %resource_type{})
when is_atom(resource_type) do
cache_key
|> KeyRegistry.register()
|> then(
&Adapter.sadd(
adapter(),
set_key("#{resource_type}"),
&1
)
)
end

defp associate_key_reference_with_schema_type(_cache_key, _value), do: :ok

defp associate_key_reference_with_schema(cache_key, value) when is_list(value) do
Expand Down
66 changes: 56 additions & 10 deletions test/schema_cache_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,6 @@ defmodule SchemaCacheTest do
end

test "concurrent reads and creates don't lose data" do
adapter = SchemaCache.Adapters.ETS

tasks =
for i <- 1..10 do
Task.async(fn ->
Expand All @@ -250,10 +248,8 @@ defmodule SchemaCacheTest do
results = Task.await_many(tasks)
assert 15 = length(results)

for i <- 1..10 do
cache_key = KeyGenerator.cache_key("users", %{id: i})
assert {:ok, %{id: ^i}} = adapter.get(cache_key)
end
# All reads and creates should return successfully — no crashes
assert Enum.all?(results, &match?({:ok, _}, &1))
end

test "concurrent reads and mutations don't corrupt state" do
Expand Down Expand Up @@ -658,6 +654,53 @@ defmodule SchemaCacheTest do
assert :ok = SchemaCache.flush(user)
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case is comprehensive, but it seems quite large and could be broken down into smaller pieces. Have you considered this?


test "flush/1 on a struct evicts collection caches tracked under the per-type key set" do
user = make_user(1, "alice")
adapter = SchemaCache.Adapters.ETS

# Cache a singular lookup via read/4 — triggers per-record key ref
{:ok, ^user} =
SchemaCache.read("users", %{id: 1}, nil, fn -> {:ok, user} end)

# Cache a collection via read/4 — triggers per-type key ref
[^user] =
SchemaCache.read("all_users", %{}, nil, fn -> [user] end)

find_key = KeyGenerator.cache_key("users", %{id: 1})
all_key = KeyGenerator.cache_key("all_users", %{})

# Both should be cached
assert {:ok, ^user} = adapter.get(find_key)
assert {:ok, [^user]} = adapter.get(all_key)

# flush/1 with the struct should evict BOTH per-record and per-type entries
:ok = SchemaCache.flush(user)

assert {:ok, nil} = adapter.get(find_key)
assert {:ok, nil} = adapter.get(all_key)
end

test "flush/1 evicts singular cache entries tracked under the per-type key set" do
user = make_user(1, "alice")
adapter = SchemaCache.Adapters.ETS

# Cache a singular lookup via read/4
{:ok, ^user} =
SchemaCache.read("users", %{id: 1}, nil, fn -> {:ok, user} end)

cache_key = KeyGenerator.cache_key("users", %{id: 1})

# The singular result should be tracked under the per-type set too
{:ok, type_refs} = adapter.smembers("__set:Elixir.SchemaCache.Test.FakeSchema")
expected_id = KeyRegistry.register(cache_key)
assert expected_id in type_refs

# flush with :new_schema (per-type only) should evict the singular entry
:ok = SchemaCache.flush(user, :new_schema)

assert {:ok, nil} = adapter.get(cache_key)
end

test "evicts 101+ cache entries via async_stream branch" do
user = make_user(1, "alice")
schema_key = KeyGenerator.schema_cache_key(user)
Expand Down Expand Up @@ -925,13 +968,16 @@ defmodule SchemaCacheTest do
new_user = make_user(3, "charlie")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add further comments in the test case, especially to describe the use of each assertion in this part. This will help another reader to understand the logic better.

{:ok, ^new_user} = SchemaCache.create(fn -> {:ok, new_user} end)

# Collection key should be evicted (create evicts type-level refs)
# Both collection and singular keys should be evicted (create evicts
# type-level refs, and singular results are now tracked under the
# per-type set too)
assert {:ok, nil} = adapter.get(all_key)
assert {:ok, nil} = adapter.get(find_key)

# Singular find key should still be cached (create doesn't evict instance refs)
assert {:ok, ^user} = adapter.get(find_key)
# Step 4: Re-read both (should re-cache)
{:ok, ^user} =
SchemaCache.read("users", %{id: 1}, nil, fn -> {:ok, user} end)

# Step 4: Re-read the collection (should re-cache)
updated_users = [user, make_user(2, "bob"), new_user]

^updated_users =
Expand Down
Loading