Packages
macula
5.2.2
7.0.0
6.0.0
5.2.2
5.2.1
5.2.0
5.1.0
5.0.0
4.8.0
4.7.1
4.7.0
4.6.0
4.5.0
4.4.10
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.1
4.3.0
4.2.9
4.2.8
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.1
4.1.0
4.0.0
3.16.0
3.15.3
3.15.2
3.15.1
3.14.0
3.13.0
3.12.1
3.12.0
3.11.1
3.11.0
3.10.3
3.10.2
3.10.1
3.9.0
3.8.0
3.7.0
3.5.0
3.4.0
3.3.0
3.2.0
3.1.0
3.0.0
2.1.1
2.1.0
2.0.0
1.5.2
1.5.1
1.4.30
1.4.29
1.4.28
1.4.27
1.4.26
1.4.25
1.4.24
1.4.23
1.4.22
1.4.21
1.4.20
1.4.19
1.4.18
1.4.17
1.4.16
1.4.15
1.4.14
1.4.13
1.4.11
1.4.10
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.1
1.3.0
1.2.0
1.1.0
1.0.10
1.0.9
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.48.6
0.48.5
0.48.4
0.48.3
0.48.2
0.48.1
0.48.0
0.47.1
0.47.0
0.46.3
0.46.1
0.46.0
0.45.3
0.45.2
0.45.1
0.45.0
0.44.2
0.44.1
0.44.0
0.43.3
0.43.2
0.43.1
0.43.0
0.42.9
0.42.8
0.42.7
0.42.6
0.42.5
0.42.4
0.42.3
0.42.2
0.42.1
0.42.0
0.41.1
0.41.0
0.40.1
0.40.0
0.39.9
0.39.8
0.39.7
0.39.6
0.39.5
0.39.4
0.39.3
0.39.2
0.39.1
0.39.0
0.38.8
0.38.7
0.38.6
0.38.5
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.7
0.37.6
0.37.5
0.37.4
0.37.3
0.37.2
0.37.1
0.37.0
0.36.6
0.36.5
0.36.4
0.36.3
0.36.2
0.36.1
0.36.0
0.35.4
0.35.3
0.35.2
0.35.1
0.35.0
0.34.1
0.34.0
0.33.1
0.33.0
0.32.5
0.32.4
0.32.3
0.32.2
0.32.1
0.32.0
0.31.9
0.31.8
0.31.7
0.31.6
0.31.5
0.31.4
0.31.3
0.31.2
0.31.1
0.31.0
0.30.10
0.30.9
0.30.8
0.30.7
0.30.6
0.30.5
0.30.4
0.30.3
0.30.2
0.30.1
0.30.0
0.29.0
0.28.3
0.28.2
0.28.1
0.28.0
0.27.1
0.27.0
0.26.1
0.26.0
0.25.6
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.6
0.24.5
0.24.4
0.24.3
0.24.2
0.24.1
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.12
0.22.11
0.22.10
0.22.9
0.22.8
0.22.7
0.22.6
0.22.5
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.7
0.21.6
0.21.5
0.21.4
0.21.2
0.21.1
0.21.0
0.20.25
0.20.24
0.20.23
0.20.22
0.20.21
0.20.20
0.20.19
0.20.18
0.20.17
0.20.16
0.20.15
0.20.14
0.20.13
0.20.12
0.20.11
0.20.10
0.20.9
0.20.8
0.20.7
0.20.6
0.20.5
0.20.3
0.20.2
0.20.1
0.20.0
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.12.6
0.12.5
0.12.3
0.11.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.30
0.7.29
0.7.28
0.7.27
0.7.26
0.7.25
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.4
0.3.3
0.3.2
0.3.1
Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise
Current section
Files
Jump to
Current section
Files
src/cache_route/macula_cache_route.erl
%%%-------------------------------------------------------------------
%%% @doc TTL-bounded route cache for macula-net.
%%%
%%% Phase 2 (PLAN_MACULA_NET_PHASE2.md §4.3). Replaces Phase 1's
%%% static station table in {@link macula_route_packet}: route_packet
%%% consults this cache first, falls back to {@link
%%% macula_resolve_address} on miss, and inserts the result.
%%%
%%% Lookups are O(1) on an ETS set keyed by macula-net IPv6 address.
%%% A periodic sweep (gen_server timer) evicts entries whose
%%% `expires_at' has passed; lookups also expire-on-read so a stale
%%% entry never escapes.
%%% @end
%%%-------------------------------------------------------------------
-module(macula_cache_route).
-behaviour(gen_server).
-export([
start_link/0, start_link/1,
stop/0,
lookup/1,
insert/2,
invalidate/1,
sweep/0,
size/0
]).
%% gen_server
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export_type([route_entry/0, lookup_result/0]).
-define(SERVER, ?MODULE).
-define(TABLE, macula_cache_route_table).
-define(DEFAULT_SWEEP_MS, 30_000).
-type route_entry() :: #{
station_pubkey := <<_:256>>,
host := binary(),
port := 1..65535,
expires_at := pos_integer()
}.
-type lookup_result() :: {ok, route_entry()} | miss | expired.
-record(state, {
sweep_ms :: pos_integer(),
timer :: reference() | undefined
}).
%% =============================================================================
%% Public API
%% =============================================================================
-spec start_link() -> {ok, pid()} | {error, term()}.
start_link() ->
start_link(#{}).
-spec start_link(map()) -> {ok, pid()} | {error, term()}.
start_link(Opts) ->
gen_server:start_link({local, ?SERVER}, ?MODULE, Opts, []).
-spec stop() -> ok.
stop() ->
case whereis(?SERVER) of
undefined -> ok;
_ -> gen_server:stop(?SERVER)
end.
%% @doc Look up a route. Returns `{ok, Entry}' for a live cache hit,
%% `expired' if the entry was found but past its TTL (also evicted),
%% or `miss' if no entry was present.
-spec lookup(<<_:128>>) -> lookup_result().
lookup(Addr) when is_binary(Addr), byte_size(Addr) =:= 16 ->
case ets:info(?TABLE) of
undefined -> miss;
_ -> lookup_entry(Addr, ets:lookup(?TABLE, Addr))
end.
lookup_entry(Addr, [{_, Entry}]) -> check_expiry(Addr, Entry);
lookup_entry(_Addr, []) -> miss.
-spec insert(<<_:128>>, route_entry()) -> ok.
insert(Addr, #{expires_at := X} = Entry)
when is_binary(Addr), byte_size(Addr) =:= 16,
is_integer(X), X > 0 ->
ensure_table(),
true = ets:insert(?TABLE, {Addr, Entry}),
ok.
-spec invalidate(<<_:128>>) -> ok.
invalidate(Addr) when is_binary(Addr), byte_size(Addr) =:= 16 ->
case ets:info(?TABLE) of
undefined -> ok;
_ -> _ = ets:delete(?TABLE, Addr), ok
end.
%% @doc Force a sweep right now. Useful for tests.
-spec sweep() -> ok.
sweep() ->
case whereis(?SERVER) of
undefined -> sweep_table();
Pid -> gen_server:call(Pid, sweep)
end.
-spec size() -> non_neg_integer().
size() ->
case ets:info(?TABLE) of
undefined -> 0;
Info -> proplists:get_value(size, Info, 0)
end.
%% =============================================================================
%% gen_server callbacks
%% =============================================================================
init(Opts) ->
ensure_table(),
SweepMs = maps:get(sweep_ms, Opts, ?DEFAULT_SWEEP_MS),
Timer = erlang:send_after(SweepMs, self(), sweep_tick),
{ok, #state{sweep_ms = SweepMs, timer = Timer}}.
handle_call(sweep, _From, State) ->
sweep_table(),
{reply, ok, State};
handle_call(_Other, _From, State) ->
{reply, {error, unknown_call}, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(sweep_tick, #state{sweep_ms = Ms} = State) ->
sweep_table(),
Timer = erlang:send_after(Ms, self(), sweep_tick),
{noreply, State#state{timer = Timer}};
handle_info(_Other, State) ->
{noreply, State}.
terminate(_Reason, #state{timer = Timer}) ->
cancel(Timer),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% =============================================================================
%% Internals
%% =============================================================================
ensure_table() ->
case ets:info(?TABLE) of
undefined ->
_ = ets:new(?TABLE, [named_table, public, set,
{read_concurrency, true}]),
ok;
_ ->
ok
end.
check_expiry(Addr, #{expires_at := X} = Entry) ->
Now = erlang:system_time(millisecond),
case Now < X of
true -> {ok, Entry};
false ->
_ = ets:delete(?TABLE, Addr),
expired
end.
%% Walk the table, drop entries whose expires_at has passed.
sweep_table() ->
case ets:info(?TABLE) of
undefined -> ok;
_ -> sweep_now(erlang:system_time(millisecond))
end.
sweep_now(Now) ->
ets:foldl(fun(E, Acc) -> sweep_entry(E, Now, Acc) end, ok, ?TABLE).
sweep_entry({Addr, #{expires_at := X}}, Now, _Acc) when X =< Now ->
_ = ets:delete(?TABLE, Addr),
ok;
sweep_entry(_, _Now, _Acc) ->
ok.
cancel(undefined) -> ok;
cancel(T) when is_reference(T) ->
_ = erlang:cancel_timer(T),
ok.