hb_cache_control.erl - Cache Control Logic
Overview
Purpose: Derive and apply cache settings for AO-Core resolver
Module: hb_cache_control
Pattern: HTTP-style cache control with precedence rules
Defaults: Store=false, Lookup=true
This module manages cache control logic for the AO-Core resolver, deriving settings from requests, responses, and node options with clear precedence rules.
Cache Control Directives
always: Always store and lookupstore: Enable storingno-store: Disable storingcache: Enable lookupno-cache: Disable lookuponly-if-cached: Return error if not cached
Precedence Order
- Opts (node operator final say)
- Msg3 (result message from device)
- Msg2 (user request message)
Dependencies
- HyperBEAM:
hb_cache,hb_opts,hb_maps,hb_ao,hb_path,hb_store
Public Functions
1. maybe_store/4
-spec maybe_store(Msg1, Msg2, Msg3, Opts) -> ok | {ok, Path} | not_caching | skip_caching.Description: Conditionally write Msg3 result to cache based on cache control settings from Msg2, Msg3, and Opts. Returns ok for async writes, {ok, Path} for sync writes, or not_caching if caching disabled.
maybe_store_enabled_test() ->
Store = hb_test_utils:test_store(),
hb_store:reset(Store),
Msg1 = #{<<"key">> => <<"value">>},
Msg2 = #{<<"path">> => <<"key">>},
Msg3 = <<"result">>,
%% <<"always">> directive enables both store and lookup
Opts = #{store => Store, cache_control => [<<"always">>]},
Result = hb_cache_control:maybe_store(Msg1, Msg2, Msg3, Opts),
?assertMatch({ok, _}, Result).
maybe_store_disabled_test() ->
Msg1 = #{},
Msg2 = #{<<"cache-control">> => [<<"no-store">>]},
Msg3 = <<"result">>,
Result = hb_cache_control:maybe_store(Msg1, Msg2, Msg3, #{}),
?assertEqual(not_caching, Result).2. maybe_lookup/3
-spec maybe_lookup(Msg1, Msg2, Opts) ->
{ok, Result} | {continue, Msg1, Msg2} | {error, Reason}.Description: Check cache for Msg1/Msg2 result. Returns cached result, continues to compute, or errors if only-if-cached misses.
%% Test cache hit using hb_ao:resolve with <<"always">> then <<"only-if-cached">>
maybe_lookup_hit_test() ->
Msg1 = #{<<"key">> => <<"cached-value">>},
Msg2 = <<"key">>,
%% First resolve with <<"always">> to cache the result
{ok, Res1} = hb_ao:resolve(Msg1, Msg2, #{cache_control => [<<"always">>]}),
?assertEqual(<<"cached-value">>, Res1),
%% Now lookup with <<"only-if-cached">> - should hit cache
{ok, Res2} = hb_ao:resolve(Msg1, Msg2, #{cache_control => [<<"only-if-cached">>]}),
?assertEqual(<<"cached-value">>, Res2).
maybe_lookup_miss_test() ->
Store = hb_test_utils:test_store(),
hb_store:reset(Store),
Opts = #{store => Store},
%% Use unique keys to ensure cache miss
UniqueKey = base64:encode(crypto:strong_rand_bytes(8)),
Msg1 = #{UniqueKey => <<"value">>},
Msg2 = #{<<"path">> => <<"nonexistent">>},
Result = hb_cache_control:maybe_lookup(Msg1, Msg2, Opts),
?assertMatch({continue, _, _}, Result).
maybe_lookup_only_if_cached_miss_test() ->
%% Request key that doesn't exist with only-if-cached
%% Using in-memory map with nonexistent key path
Msg1 = #{<<"exists">> => <<"value">>},
Msg2 = <<"nonexistent-key">>,
Result = hb_ao:resolve(Msg1, Msg2, #{cache_control => [<<"only-if-cached">>]}),
?assertMatch({error, _}, Result).Cache Control Settings
% Derive settings from sources
Settings = #{
<<"store">> => true | false,
<<"lookup">> => true | false,
<<"only-if-cached">> => true | undefined
}.
% Directives
Directives = [
<<"always">>, % Store and lookup
<<"store">>, % Enable store
<<"no-store">>, % Disable store
<<"cache">>, % Enable lookup
<<"no-cache">>, % Disable lookup
<<"only-if-cached">> % Error on miss
].Common Patterns
%% Store result if requested
Opts = #{cache_control => [<<"store">>]},
{ok, Result} = hb_ao:resolve(Msg1, Msg2, Opts),
hb_cache_control:maybe_store(Msg1, Msg2, Result, Opts).
%% Only use cached results
Opts = #{cache_control => [<<"only-if-cached">>]},
case hb_cache_control:maybe_lookup(Msg1, Msg2, Opts) of
{ok, Cached} -> {cached, Cached};
{error, #{<<"status">> := 504}} -> {error, not_cached}
end.
%% Force computation
Opts = #{cache_control => [<<"no-cache">>]},
{continue, Msg1, Msg2} = hb_cache_control:maybe_lookup(Msg1, Msg2, Opts).
%% Always cache
Opts = #{cache_control => [<<"always">>]},
{ok, Result} = hb_ao:resolve(Msg1, Msg2, Opts).
% Automatically stored
%% Async caching
Opts = #{async_cache => true},
hb_cache_control:maybe_store(Msg1, Msg2, Result, Opts).
% Returns immediately, caches in backgroundAsync Caching
% Enable async caching
Opts = #{async_cache => true}.
% Worker process spawned per-process
Worker = find_or_spawn_async_writer(Opts),
Worker ! {write, Msg1, Msg2, Msg3, Opts}.
% Caching happens in background
% Main process continues immediatelyReferences
- Cache System -
hb_cache.erl - HTTP Cache-Control - RFC 7234
- AO-Core Resolution -
hb_ao.erl
Notes
- Precedence: Opts > Msg3 > Msg2
- Defaults: Store=false, Lookup=true
- Hashpath Ignore: Prevents storage with incorrect hashpath
- Async Option: Background caching for performance
- only-if-cached: Returns 504 on miss
- Heuristics: Skip cache for explicit key lookups
- Binary Results: Stored at hashpath
- Map Results: Stored as full message
- Multi-Source: Merges settings from all sources
- HTTP-Compatible: Follows HTTP cache-control semantics
- Message Control: Via
cache-controlkey - Performance: Heuristics improve common cases