| 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}. |