/__w/shurbej/shurbej/_build/test/cover/ct/shurbej_write_token.html

1 -module(shurbej_write_token).
2 -behaviour(gen_server).
3
4 %% Tracks Zotero-Write-Token headers for idempotent writes.
5 %% Tokens are kept for 12 hours then pruned.
6
7 -export([start_link/0, check/1, store/2, init/1, handle_call/3, handle_cast/2, handle_info/2]).
8
9 -define(TTL_MS, 43200000). %% 12 hours
10 -define(CLEANUP_INTERVAL, 3600000). %% 1 hour
11
12 start_link() ->
13 1 gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
14
15 %% Check if a write token has been seen before.
16 %% Returns new | {duplicate, StoredResponse}.
17 60 check(undefined) -> new;
18 4 check(Token) -> gen_server:call(?MODULE, {check, Token}).
19
20 %% Store a completed write token with its response for future dedup.
21 52 store(undefined, _Response) -> ok;
22 2 store(Token, Response) -> gen_server:call(?MODULE, {store, Token, Response}).
23
24 init([]) ->
25 1 Table = ets:new(shurbej_write_tokens, [set, protected]),
26 1 erlang:send_after(?CLEANUP_INTERVAL, self(), cleanup),
27 1 {ok, #{table => Table}}.
28
29 handle_call({check, Token}, _From, #{table := Table} = State) ->
30 4 case ets:lookup(Table, Token) of
31 [{_, {complete, StoredResponse}, _}] ->
32 3 {reply, {duplicate, StoredResponse}, State};
33 [{_, in_progress, _}] ->
34
:-(
{reply, in_progress, State};
35 [] ->
36 1 ets:insert(Table, {Token, in_progress, erlang:system_time(millisecond)}),
37 1 {reply, new, State}
38 end;
39
40 handle_call({store, Token, Response}, _From, #{table := Table} = State) ->
41 2 ets:insert(Table, {Token, {complete, Response}, erlang:system_time(millisecond)}),
42 2 {reply, ok, State};
43
44 handle_call(_Msg, _From, State) ->
45
:-(
{reply, {error, unknown}, State}.
46
47 handle_cast(_Msg, State) ->
48
:-(
{noreply, State}.
49
50 handle_info(cleanup, #{table := Table} = State) ->
51 1 Cutoff = erlang:system_time(millisecond) - ?TTL_MS,
52 1 ets:select_delete(Table, [{{'_', '_', '$1'}, [{'<', '$1', Cutoff}], [true]}]),
53 1 erlang:send_after(?CLEANUP_INTERVAL, self(), cleanup),
54 1 {noreply, State};
55 handle_info(_Msg, State) ->
56
:-(
{noreply, State}.
Line Hits Source