emqx/lib-ee/emqx_license/src/emqx_license_checker.erl

216 lines
6.8 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%--------------------------------------------------------------------
-module(emqx_license_checker).
-include("emqx_license.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-behaviour(gen_server).
-define(CHECK_INTERVAL, 5000).
-define(EXPIRY_ALARM_CHECK_INTERVAL, 24 * 60 * 60).
-define(OK(EXPR),
try
_ = begin
EXPR
end,
ok
catch
_:_ -> ok
end
).
-export([
start_link/1,
start_link/2,
update/1,
dump/0,
purge/0,
limits/0,
print_warnings/1
]).
%% gen_server callbacks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2
]).
-define(LICENSE_TAB, emqx_license).
%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------
-type limits() :: #{max_connections := non_neg_integer() | ?ERR_EXPIRED}.
-spec start_link(emqx_license_parser:license()) -> {ok, pid()}.
start_link(LicenseFetcher) ->
start_link(LicenseFetcher, ?CHECK_INTERVAL).
-spec start_link(emqx_license_parser:license(), timeout()) -> {ok, pid()}.
start_link(LicenseFetcher, CheckInterval) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [LicenseFetcher, CheckInterval], []).
-spec update(emqx_license_parser:license()) -> ok.
update(License) ->
gen_server:call(?MODULE, {update, License}, infinity).
-spec dump() -> [{atom(), term()}].
dump() ->
gen_server:call(?MODULE, dump, infinity).
-spec limits() -> {ok, limits()} | {error, any()}.
limits() ->
try ets:lookup(?LICENSE_TAB, limits) of
[{limits, Limits}] -> {ok, Limits};
_ -> {error, no_license}
catch
error:badarg ->
{error, no_license}
end.
%% @doc Force purge the license table.
-spec purge() -> ok.
purge() ->
gen_server:call(?MODULE, purge, infinity).
%%------------------------------------------------------------------------------
%% gen_server callbacks
%%------------------------------------------------------------------------------
init([LicenseFetcher, CheckInterval]) ->
case LicenseFetcher() of
{ok, License} ->
?LICENSE_TAB = ets:new(?LICENSE_TAB, [
set, protected, named_table, {read_concurrency, true}
]),
ok = print_warnings(check_license(License)),
State0 = ensure_check_license_timer(#{
check_license_interval => CheckInterval,
license => License
}),
State = ensure_check_expiry_timer(State0),
{ok, State};
{error, Reason} ->
{stop, Reason}
end.
handle_call({update, License}, _From, State) ->
ok = expiry_early_alarm(License),
{reply, check_license(License), State#{license => License}};
handle_call(dump, _From, #{license := License} = State) ->
{reply, emqx_license_parser:dump(License), State};
handle_call(purge, _From, State) ->
_ = ets:delete_all_objects(?LICENSE_TAB),
{reply, ok, State};
handle_call(_Req, _From, State) ->
{reply, unknown, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(check_license, #{license := License} = State) ->
#{} = check_license(License),
NewState = ensure_check_license_timer(State),
?tp(debug, emqx_license_checked, #{}),
{noreply, NewState};
handle_info(check_expiry_alarm, #{license := License} = State) ->
ok = expiry_early_alarm(License),
NewState = ensure_check_expiry_timer(State),
{noreply, NewState};
handle_info(_Msg, State) ->
{noreply, State}.
%%------------------------------------------------------------------------------
%% Private functions
%%------------------------------------------------------------------------------
ensure_check_license_timer(#{check_license_interval := CheckInterval} = State) ->
cancel_timer(State, timer),
State#{timer => erlang:send_after(CheckInterval, self(), check_license)}.
ensure_check_expiry_timer(State) ->
cancel_timer(State, expiry_alarm_timer),
Ref = erlang:send_after(?EXPIRY_ALARM_CHECK_INTERVAL, self(), check_expiry_alarm),
State#{expiry_alarm_timer => Ref}.
cancel_timer(State, Key) ->
_ =
case maps:find(Key, State) of
{ok, Ref} when is_reference(Ref) -> erlang:cancel_timer(Ref);
_ -> ok
end,
ok.
check_license(License) ->
DaysLeft = days_left(License),
IsOverdue = is_overdue(License, DaysLeft),
NeedRestriction = IsOverdue,
MaxConn = emqx_license_parser:max_connections(License),
Limits = limits(License, NeedRestriction),
true = apply_limits(Limits),
#{
warn_evaluation => warn_evaluation(License, NeedRestriction, MaxConn),
warn_expiry => {(DaysLeft < 0), -DaysLeft}
}.
warn_evaluation(License, false, MaxConn) ->
{emqx_license_parser:customer_type(License) == ?EVALUATION_CUSTOMER, MaxConn};
warn_evaluation(_License, _NeedRestrict, _Limits) ->
false.
limits(License, false) -> #{max_connections => emqx_license_parser:max_connections(License)};
limits(_License, true) -> #{max_connections => ?ERR_EXPIRED}.
days_left(License) ->
DateEnd = emqx_license_parser:expiry_date(License),
{DateNow, _} = calendar:universal_time(),
calendar:date_to_gregorian_days(DateEnd) - calendar:date_to_gregorian_days(DateNow).
is_overdue(License, DaysLeft) ->
CType = emqx_license_parser:customer_type(License),
Type = emqx_license_parser:license_type(License),
small_customer_overdue(CType, DaysLeft) orelse
non_official_license_overdue(Type, DaysLeft).
%% small customers overdue 90 days after license expiry date
small_customer_overdue(?SMALL_CUSTOMER, DaysLeft) -> DaysLeft < ?EXPIRED_DAY;
small_customer_overdue(_CType, _DaysLeft) -> false.
%% never restrict official license
non_official_license_overdue(?OFFICIAL, _) -> false;
non_official_license_overdue(_, DaysLeft) -> DaysLeft < 0.
apply_limits(Limits) ->
ets:insert(?LICENSE_TAB, {limits, Limits}).
expiry_early_alarm(License) ->
case days_left(License) < 30 of
true ->
{Y, M, D} = emqx_license_parser:expiry_date(License),
Date = iolist_to_binary(io_lib:format("~B~2..0B~2..0B", [Y, M, D])),
?OK(emqx_alarm:activate(license_expiry, #{expiry_at => Date}));
false ->
?OK(emqx_alarm:ensure_deactivated(license_expiry))
end.
print_warnings(Warnings) ->
ok = print_evaluation_warning(Warnings),
ok = print_expiry_warning(Warnings).
print_evaluation_warning(#{warn_evaluation := {true, MaxConn}}) ->
io:format(?EVALUATION_LOG, [MaxConn]);
print_evaluation_warning(_) ->
ok.
print_expiry_warning(#{warn_expiry := {true, Days}}) ->
io:format(?EXPIRY_LOG, [Days]);
print_expiry_warning(_) ->
ok.