When the cache clears

Copy Markdown View Source

AshCache throws a cached result away when the data behind it may have changed. Most of the time you don't need to do anything. This page lists when it happens, and what to do in the cases AshCache can't see.

What clears the cache

A cached result is thrown away when:

  1. The resource is changed through Ash. Creating, updating or deleting any product throws away every cached product read and generic action, right away.
  2. A resource listed in clear_when_changed is changed through Ash. See Related resources.
  3. Its ttl runs out, even if nothing changed.
  4. You clear it by hand with AshCache.invalidate/1 or AshCache.clear_store/1. See Clear by hand.

Nothing else clears it. A change made without Ash, such as raw SQL, an import script or another service writing to the same table, shows up only when ttl runs out.

A write clears the whole resource

Any write to a resource clears every cached read of that resource, not only the entries that contain the changed record. Updating one product clears the cached lookup of every product.

AshCache can't do better than this: an entry is saved under the query that produced it, so a write can't tell which entries hold the record it changed. If a resource is written to all the time, most reads will miss. Measure before caching it.

Writes that fail don't clear anything

The cache is cleared after the database transaction commits. A write that fails or is rolled back leaves the cache as it was, so it can never leave wrong data cached.

Related records loaded with load, such as a post's comments, are read fresh on every request. They need nothing.

Values computed from another resource are different: a post's comment count, or a filter on its comments. They are saved with the cached post, so AshCache caches such a read only when the other resource clears it. Otherwise the read goes to the database every time. To cache it, list the comment resource on the post:

defmodule MyApp.Blog.Post do
  use Ash.Resource,
    domain: MyApp.Blog,
    extensions: [AshCache.Resource]

  cache do
    read :get_by_slug
    clear_when_changed [MyApp.Blog.Comment]
  end
end

defmodule MyApp.Blog.Comment do
  use Ash.Resource,
    domain: MyApp.Blog,
    extensions: [AshCache.Resource]
end

Now creating, updating or deleting any comment clears every cached post read, the same way a change to a post does.

Things to know:

  • The listed resource needs the extension, so AshCache sees its writes. It doesn't need a cache block. Without the extension, Post fails to compile.
  • It is per resource. A comment on one post clears the cached reads of every post.
  • It is not passed along. If Post lists Comment and Comment lists Author, an author change does not clear posts. List Author on Post too.
  • List the join resource for many-to-many. If posts and tags are linked through a PostTag resource, tagging a post writes a PostTag, not a Tag. List PostTag, and Tag too if tag names can change.
  • Deletes done by the database are not seen. If deleting a post makes the database delete its comments (ON DELETE CASCADE), and Comment caches its own reads, those keep the deleted comments. Add clear_when_changed [MyApp.Blog.Post] to Comment, so deleting a post clears them.
  • Foreign key names don't matter. Only the resource is listed, so a relationship on any column works the same.
  • Clearing the listed resource by hand clears the post too. AshCache.invalidate(MyApp.Blog.Comment) throws away cached post reads.
  • Each listed resource makes cached reads a little slower: one more value is looked up in the store per read.

The Examples page has more cases: products with prices, orders with items, teams with members.

Clear by hand

When data changes without Ash, for example after an import, clear the resource yourself:

MyApp.Import.run!()
AshCache.invalidate(MyApp.Shop.Product)

AshCache.invalidate/1 does exactly what a write through Ash does: it clears the resource's cached reads and those of any resource that lists it in clear_when_changed.

To empty a whole store, for every resource in it, use AshCache.clear_store(:ash_cache). To empty one resource's cache without touching the rest, give it its own store; see Storage.

On several servers

With the default store, each server keeps its own cache. A write, or a call to AshCache.invalidate/1, clears the cache only on the server where it ran. The other servers serve old results until ttl runs out. Sharing one cache between servers is experimental; see Running on several servers.

Fill the cache right after a write

After a write, the next read goes to the database. If that first read must be fast, refresh runs it as part of the write, so the new result is cached before anyone asks:

cache do
  read :get_by_sku
  refresh :get_by_sku, arguments: [sku: :sku]
end

arguments says which attribute of the saved record fills each argument of the read. Each refresh makes the write one read slower. If the read fails, the failure is logged and the write still succeeds.

How it works

How clearing works inside, with version values in every key, is explained in How AshCache works.