-
Notifications
You must be signed in to change notification settings - Fork 0
fix: flush/1 evicts both per-record and per-type key sets #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
| {:ok, value} | ||
|
|
||
| [] -> | ||
|
|
@@ -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 | ||
|
|
@@ -458,10 +461,12 @@ defmodule SchemaCache do | |
| evict_reference_keys("#{schema_type}") | ||
| end | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
@@ -534,6 +539,19 @@ defmodule SchemaCache do | |
| ) | ||
| end | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 -> | ||
|
|
@@ -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 | ||
|
|
@@ -658,6 +654,53 @@ defmodule SchemaCacheTest do | |
| assert :ok = SchemaCache.flush(user) | ||
| end | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
|
@@ -925,13 +968,16 @@ defmodule SchemaCacheTest do | |
| new_user = make_user(3, "charlie") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = | ||
|
|
||
There was a problem hiding this comment.
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.