Current section
Files
Jump to
Current section
Files
src/ftpfilez_backoff.erl
%% @doc Short-lived cache for connection/login failures.
%%
%% This prevents a queue with many jobs for the same FTP credentials from
%% opening many connections that are very likely to fail with the same error.
%% Only connection setup errors are cached; file operation errors are not.
%% @author Marc Worrell
%% @copyright 2026 Marc Worrell
%% @end
%% Copyright 2026 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
-module(ftpfilez_backoff).
-behaviour(gen_server).
-export([
start_link/0,
lookup/4,
remember/5,
forget/4
]).
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).
-define(SERVER, ?MODULE).
-define(TABLE, ?MODULE).
-define(DEFAULT_BACKOFF_MS, 3000).
-type key() :: {binary(), pos_integer(), binary(), binary()}.
-spec start_link() -> {ok, pid()} | ignore | {error, term()}.
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
-spec lookup(Host, Port, Username, Password) -> none | {error, term()} when
Host :: string() | binary(),
Port :: pos_integer(),
Username :: string() | binary(),
Password :: string() | binary().
lookup(Host, Port, Username, Password) ->
Key = key(Host, Port, Username, Password),
Now = erlang:monotonic_time(millisecond),
try ets:lookup(?TABLE, Key) of
[{Key, Until, Error}] when Until > Now ->
Error;
[{Key, _Until, _Error}] ->
ets:delete(?TABLE, Key),
none;
[] ->
none
catch
error:badarg ->
none
end.
-spec remember(Host, Port, Username, Password, Error) -> ok when
Host :: string() | binary(),
Port :: pos_integer(),
Username :: string() | binary(),
Password :: string() | binary(),
Error :: {error, term()}.
remember(Host, Port, Username, Password, {error, _} = Error) ->
BackoffMs = backoff_ms(),
case BackoffMs > 0 of
true ->
Key = key(Host, Port, Username, Password),
Until = erlang:monotonic_time(millisecond) + BackoffMs,
try ets:insert(?TABLE, {Key, Until, Error}) of
true -> ok
catch
error:badarg -> ok
end;
false ->
ok
end.
-spec forget(Host, Port, Username, Password) -> ok when
Host :: string() | binary(),
Port :: pos_integer(),
Username :: string() | binary(),
Password :: string() | binary().
forget(Host, Port, Username, Password) ->
Key = key(Host, Port, Username, Password),
try ets:delete(?TABLE, Key) of
true -> ok
catch
error:badarg -> ok
end.
init([]) ->
?TABLE = ets:new(?TABLE, [
named_table,
public,
set,
{read_concurrency, true},
{write_concurrency, true}
]),
{ok, #{}}.
handle_call(_Msg, _From, State) ->
{reply, ok, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVersion, State, _Extra) ->
{ok, State}.
-spec key(string() | binary(), pos_integer(), string() | binary(), string() | binary()) -> key().
key(Host, Port, Username, Password) ->
PasswordHash = erlang:md5(to_bin(Password)),
{to_bin(Host), Port, to_bin(Username), PasswordHash}.
-spec backoff_ms() -> non_neg_integer().
backoff_ms() ->
case application:get_env(ftpfilez, connect_error_backoff_ms) of
{ok, N} when is_integer(N), N >= 0 -> N;
undefined -> ?DEFAULT_BACKOFF_MS
end.
to_bin(B) when is_binary(B) -> B;
to_bin(L) when is_list(L) -> unicode:characters_to_binary(L);
to_bin(undefined) -> <<>>.