Cache
See how to cache data in Athenna.
Introduction
Some of the data retrieval or processing tasks performed by your application could be CPU intensive or take several seconds to complete. When this is the case, it is common to cache the retrieved data for a time so it can be retrieved quickly on subsequent requests for the same data. The cached data is usually stored in a very fast data store such as in memory or Redis.
Thankfully, Athenna provides an expressive, unified API for various cache backends, allowing you to take advantage of their blazing fast data retrieval and speed up your web application.
Installation
First of all you need to install @athenna/cache package and configure it. Artisan
provides a very simple command to install and configure the cache library in your project.
Simply run the following:
node artisan install @athenna/cache
The cache configurer will ask which stores you plan to use and then do the following operations in your project:
- Create the
cache.tsconfiguration file. - Add the cache provider in your
.athennarc.jsonfile. - Add cache environment variables to
.env,.env.testand.env.example. - Install the libraries of the selected stores (
lru-cacheformemoryandredisforredis). - Add a Redis service to your
docker-compose.ymlfile if you have selectedredis.
Configuration
Athenna's cache services may be configured via your application's
Path.config('cache.ts')./src/config/cache.ts
import { Env } from '@athenna/config'
export default {
default: Env('CACHE_STORE', 'memory'),
stores: {
memory: {
driver: 'memory'
},
redis: {
driver: 'redis',
url: Env('REDIS_URL', 'redis://localhost:6379?database=0')
}
}
}
Available cache drivers
Each store is powered by a "driver". The driver determines how and where the data will be cached. The following cache drivers are available in every Athenna application. An entry for each of these drivers is already present in your application's
Path.config('cache.ts')./src/config/cache.ts
| Driver name | Website |
|---|---|
memory | https://www.npmjs.com/package/lru-cache |
redis | https://redis.io |
Store options
The following options can be set in any store of your
Path.config('cache.ts')./src/config/cache.ts
| Option | Default | Description |
|---|---|---|
driver | - | The driver that will power the store. |
ttl | - | The default time to live in milliseconds of the items stored. |
enabled | true | When false, the store doesn't read or write anything. |
prefix | - | A prefix that will be added in front of your keys (redis driver only). |
maxItems | 1000 | The max number of items that could be stored (memory driver only). |
maxEntrySize | - | The max size of an item that could be stored, in bytes or as a string like '1MB' (memory driver only). |
The redis driver also accepts the connection options below. When
url is not set, the driver builds it from the other options:
| Option | Default | Description |
|---|---|---|
url | - | The connection URL of your Redis server. |
host | - | The host of your Redis server. |
port | 6379 | The port of your Redis server. |
username | - | The username of your Redis server. |
password | - | The password of your Redis server. |
database | 0 | The Redis database number. |
protocol | redis | The connection protocol (redis or rediss). |
socket | - | The socket options of the redis client. |
reconnectStrategy | see below | A function that defines how the client should reconnect. |
Reconnecting to Redis
By default, the redis driver never stops trying to reconnect when the
connection drops. It waits retries * 200ms between each attempt, up to
5 seconds, and logs a warning every 10 retries. You can define your own
strategy with the reconnectStrategy option. It receives the number of
retries so far and must return the delay in milliseconds before the next
attempt, or an Error to stop retrying:
redis: {
driver: 'redis',
url: Env('REDIS_URL'),
reconnectStrategy: (retries: number) => {
if (retries > 20) {
return new Error('Too many retries')
}
return 1000
}
}
Even when your strategy gives up, the next command sent to the store will try to reconnect the client, so your application recovers by itself once Redis is available again.
Storing in Cache
You may use the set() method on the Cache facade to store items in the cache:
import { Cache } from '@athenna/cache'
import { Parser } from '@athenna/common'
const options = { ttl: Parser.timeToMs('1d') }
await Cache.set('key', 'value', options)
If the ttl option is not passed to the set() method, the ttl of
your store configuration will be used. If your store doesn't have a ttl
configured, the item will be stored indefinitely:
await Cache.set('key', 'value')
The redis driver only stores strings. If you need to cache objects,
serialize them before calling set() and parse them after get():
await Cache.set('user:1', JSON.stringify(user))
const user = JSON.parse(await Cache.get('user:1'))
Retrieving from Cache
The Cache facade's get() method is used to retrieve items from the cache. If the item
does not exist in the cache, undefined will be returned. If you wish, you may pass a second
argument to the get method specifying the default value you wish to be returned if the
item doesn't exist:
const value = await Cache.get('key')
const value = await Cache.get('key', 'defaultValue')
Determining item existence
The has() method may be used to determine if an item exists in the cache. This method
will return false if the item doesn't exist or if its value is falsy (null, '',
0, ...):
if (await Cache.has('key')) {
// ...
}
Retrieve and store
Sometimes you may wish to retrieve an item from the cache, but also store a default value
if the requested item doesn't exist. For example, you may wish to retrieve all users from
the cache or, if they don't exist, retrieve them from the database and add them to the
cache. You may do this using the remember() method:
import { Database } from '@athenna/database'
const value = await Cache.remember('users', async () => {
return Database.table('users').findMany()
})
If the item does not exist in the cache, the closure passed to the remember() method will
be executed and its result will be placed in the cache.
You may set a third argument to remember() method to add the same options you set in set()
method:
import { Parser } from '@athenna/common'
import { Database } from '@athenna/database'
const options = { ttl: Parser.timeToMs('1d') }
const value = await Cache.remember('users', async () => {
return Database.table('users').findMany()
}, options)
Retrieve and delete
If you need to retrieve an item from the cache and then delete the item, you may use the pull()
method. Like the get() method, undefined will be returned if the item does not exist in the cache:
const value = await Cache.pull('key')
Deleting from Cache
You may remove items from the cache using the delete() method:
await Cache.delete('key')
You may clear the entire cache using the truncate() method:
await Cache.truncate()
The redis driver only removes the keys that start with your configured
prefix. If no prefix is configured, all entries of the Redis database
will be removed, so consider this carefully when clearing a cache which
is shared by other applications. The memory driver always removes
all of its entries.
Using multiple stores
The store() method may be used to access a store other than the
default one configured in your
Path.config('cache.ts')./src/config/cache.ts
await Cache.store('redis').set('key', 'value')
const value = await Cache.store('memory').get('key')
You may also override the store configuration in runtime using the
options property:
await Cache.store('redis', { options: { prefix: 'users', ttl: 60000 } })
.set('1', 'value')