Cachettl is an implementation of a periodic self-rehydrating TTL cache that resiliently
handles expensive data-processing ahead of time for fast access.
The cache mechanism generates 0-arity functions that embed inbound
data from store/3. Each function is registered under a unique key
along with a TTL("time to live").
Child processes are assigned to compute the functions at set intervals
and store the results. The cache is expected to provide the most recently
computed value whenever get/1 is called.
- Critical tasks are executed concurrently ensuring quality performance without race conditions.
- Child processes are free from their parent. Instead, they are linked to a chain of supervisors on the main application-- ensuring application-wide stability and resilience to runtime exceptions.
- Storage is optimized for concurrent read/write.
- Uses a single counter for Refresh Interval and TTL.
- Input data is high-frequency fast-changing queries.
- Data requires processing that is expensive to compute, therefore, data processing must start and be completed before it is needed, not when it is being requested for.
- Data is not frequently accessed, but fast access is guaranteed when needed.
Cachettl.get(key)Retrieve the value associated with the specified key.
If key exists in the cache and initial data associated with
key is available, {:ok, data} is returned. If data-prccessing
is in progress on first-run hence the data associated with key
has not been stored, then {:busy, reason} is returned.
If key is not present in the cache, {:error, reason} is returned.
Note: Client application calling Cachettl.get(key) should be
responsible for implementing a polling function with a timeout
mechanism. While this may be rarely needed, it should be
available in cases where the requested data does not yet exist
in the cache on initial run.
Cachettl.store(key, vaue, ttl // 3_600)Add or update existing value with its ttl in the cache under key.
ttl value is expected to be greater than the
refresh_interval (see Cachettl.Manager configuration).
It is recommended that ttl value is divisible by the refresh_interval.
if ttl is not given, it defaults to 3_600 seconds(1 hour).
Note: ttl should be specified in seconds, either in integer or decimal.
The provided value will convert to milliseconds internally.
Cachettl.store("HEL", %{}, 10)
# internal conversion
#=> Storage.sec_to_ms(10) == 10_000
#=> true
Cachettl.store("HEL", %{}, 10.50)
# internal conversion
#=> Storage.sec_to_ms(10.50) == 10_500
#=> true
# when ttl is not specified...
Cachettl.store("HEL", %{})
# internal conversion
#=> Storage.sec_to_ms(36_000)
#=> 36000To test-run and observer the performance, launch two terminals(Terminal-1 and Terminal-2) and the following lines in the project root directory:
# Terminal-1
/cachettl$ iex --sname server@localhost -S mix
#=> Erlang/OTP...
# Terminal-2
/cachettl$ iex --sname client@localhost -S mix
#=> Erlang/OTP...
# Treminal-2: connect to :server@localhost
iex(client@localhost)1> Node.connect(:server@localhost)
#=> true
# Terminal-1: verify connection
iex(server@localhost)1> Node.list()
#=> [:client@localhost]
# Terminal-2: verify connection
iex(client@localhost)2> Node.list()
#=> [:server@localhost]
# Terminal-1: use Observer to view performance and structures of running processes
iex(server@localhost)2> :observer.start()
#=> ...
#=> :ok
# Terminal-1
iex(server@localhost)3> Cachettl.MockWeather.loop_store()
# Terminal-2
iex(client@localhost)3> Node.spawn(:server@localhost, Cachettl.MockWeather, :loop_get, [])Expect to also get exceptions like this:
...[warning] Worker-LUX Terminated. Reason: {%RuntimeError{message: "faking an exception"}...
...[error] GenServer {Cachettl.WorkerRegistry, "LUX-worker", :fun_0_arity_processor} terminating
** (RuntimeError) faking an exceptionIt is a deliberate part of the test to verify how the Supervisor
handles child-process failure.
