From 35d0c2085cd07376157ac4bbe95c79db005e7a5c Mon Sep 17 00:00:00 2001 From: Siri Hansen Date: Tue, 9 Oct 2018 13:18:19 +0200 Subject: [logger] Further refactor built-in handlers --- lib/kernel/src/logger_disk_log_h.erl | 102 +++----- lib/kernel/src/logger_h_common.erl | 389 ++++++++++++---------------- lib/kernel/src/logger_std_h.erl | 160 ++++++------ lib/kernel/test/logger_disk_log_h_SUITE.erl | 29 ++- lib/kernel/test/logger_std_h_SUITE.erl | 24 +- 5 files changed, 306 insertions(+), 398 deletions(-) diff --git a/lib/kernel/src/logger_disk_log_h.erl b/lib/kernel/src/logger_disk_log_h.erl index 8c09dd071f..1e48e5b0a8 100644 --- a/lib/kernel/src/logger_disk_log_h.erl +++ b/lib/kernel/src/logger_disk_log_h.erl @@ -27,10 +27,8 @@ -export([info/1, filesync/1, reset/1]). %% logger_h_common callbacks --export([init/2, check_config/4, reset_state/1, - async_filesync/2, sync_filesync/2, - async_write/3, sync_write/3, - handle_info/2, terminate/3]). +-export([init/2, check_config/4, reset_state/2, + filesync/3, write/4, handle_info/3, terminate/3]). %% logger callbacks -export([log/2, adding_handler/1, removing_handler/1, changing_config/3, @@ -185,45 +183,27 @@ merge_default_logopts(Name, HConfig) -> type => Type}, maps:merge(Defaults, HConfig). -async_filesync(Name,State) -> - {_,State1} = disk_log_sync(Name,State), - State1. - -sync_filesync(Name,State) -> +filesync(Name,_Mode,State) -> disk_log_sync(Name,State). -async_write(Name, Bin, State) -> - {_,State1} = disk_log_write(Name, Bin, State), - State1. - -sync_write(Name, Bin, State) -> +write(Name, _Mode, Bin, State) -> disk_log_write(Name, Bin, State). -reset_state(State) -> +reset_state(_Name, State) -> State#{prev_log_result => ok, prev_sync_result => ok, prev_disk_log_info => undefined}. %% The disk log owner must handle status messages from disk_log. -handle_info({disk_log, _Node, _Log, {wrap,_NoLostItems}}, State) -> - State; -handle_info({disk_log, _Node, Log, Info = {truncated,_NoLostItems}}, - State = #{id := Name, prev_disk_log_info := PrevInfo}) -> - error_notify_new(Info, PrevInfo, {disk_log,Name,Log,Info}), - State#{prev_disk_log_info => Info}; -handle_info({disk_log, _Node, Log, Info = {blocked_log,_Items}}, - State = #{id := Name, prev_disk_log_info := PrevInfo}) -> - error_notify_new(Info, PrevInfo, {disk_log,Name,Log,Info}), - State#{prev_disk_log_info => Info}; -handle_info({disk_log, _Node, Log, full}, - State = #{id := Name, prev_disk_log_info := PrevInfo}) -> - error_notify_new(full, PrevInfo, {disk_log,Name,Log,full}), - State#{prev_disk_log_info => full}; -handle_info({disk_log, _Node, Log, Info = {error_status,_Status}}, - State = #{id := Name, prev_disk_log_info := PrevInfo}) -> - error_notify_new(Info, PrevInfo, {disk_log,Name,Log,Info}), - State#{prev_disk_log_info => Info}; -handle_info(_, State) -> +handle_info(Name, {disk_log, _Node, Log, Info={truncated,_NoLostItems}}, State) -> + maybe_notify_status(Name, Log, Info, prev_disk_log_info, State); +handle_info(Name, {disk_log, _Node, Log, Info = {blocked_log,_Items}}, State) -> + maybe_notify_status(Name, Log, Info, prev_disk_log_info, State); +handle_info(Name, {disk_log, _Node, Log, Info = full}, State) -> + maybe_notify_status(Name, Log, Info, prev_disk_log_info, State); +handle_info(Name, {disk_log, _Node, Log, Info = {error_status,_Status}}, State) -> + maybe_notify_status(Name, Log, Info, prev_disk_log_info, State); +handle_info(_, _, State) -> State. terminate(Name, _Reason, _State) -> @@ -265,42 +245,28 @@ close_disk_log(Name, _) -> ok. disk_log_write(Name, Bin, State) -> - case ?disk_log_blog(Name, Bin) of - ok -> - {ok,State#{prev_log_result => ok, last_op => write}}; - LogError -> - _ = case maps:get(prev_log_result, State) of - LogError -> - %% don't report same error twice - ok; - _ -> - LogOpts = maps:get(log_opts, State), - logger_h_common:error_notify({Name,log, - LogOpts, - LogError}) - end, - {LogError,State#{prev_log_result => LogError}} - end. + Result = ?disk_log_blog(Name, Bin), + maybe_notify_error(Name, log, Result, prev_log_result, State). disk_log_sync(Name, State) -> - case ?disk_log_sync(Name) of - ok -> - {ok,State#{prev_sync_result => ok, last_op => sync}}; - SyncError -> - _ = case maps:get(prev_sync_result, State) of - SyncError -> - %% don't report same error twice - ok; - _ -> - LogOpts = maps:get(log_opts, State), - logger_h_common:error_notify({Name,filesync, - LogOpts, - SyncError}) - end, - {SyncError,State#{prev_sync_result => SyncError}} - end. + Result = ?disk_log_sync(Name), + maybe_notify_error(Name, filesync, Result, prev_sync_result, State). -error_notify_new(Info,Info, _Term) -> +%%%----------------------------------------------------------------- +%%% Print error messages, but don't repeat the same message +maybe_notify_error(Name, Op, Result, Key, #{log_opts:=LogOpts}=State) -> + {Result,error_notify_new({Name, Op, LogOpts, Result}, Result, Key, State)}. + +maybe_notify_status(Name, Log, Info, Key, State) -> + error_notify_new({disk_log, Name, Log, Info}, Info, Key, State). + +error_notify_new(Term, What, Key, State) -> + error_notify_new(What, maps:get(Key,State), Term), + State#{Key => What}. + +error_notify_new(ok,_Prev,_Term) -> + ok; +error_notify_new(Same,Same,_Term) -> ok; -error_notify_new(_Info0,_Info1, Term) -> +error_notify_new(_New,_Prev,Term) -> logger_h_common:error_notify(Term). diff --git a/lib/kernel/src/logger_h_common.erl b/lib/kernel/src/logger_h_common.erl index d01851b2b1..d290f51e34 100644 --- a/lib/kernel/src/logger_h_common.erl +++ b/lib/kernel/src/logger_h_common.erl @@ -24,7 +24,7 @@ -include("logger_internal.hrl"). %% API --export([start_link/3, info/2, filesync/2, reset/2]). +-export([start_link/1, info/2, filesync/2, reset/2]). %% gen_server and proc_lib callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -52,65 +52,37 @@ -define(READ_ONLY_KEYS,[handler_pid,mode_tab]). %%%----------------------------------------------------------------- -%%% -filesync(Module, Name) when is_atom(Name) -> - try - gen_server:call(?name_to_reg_name(Module,Name), - filesync, ?DEFAULT_CALL_TIMEOUT) - catch - _:{timeout,_} -> {error,handler_busy} - end; -filesync(_, Name) -> - {error,{badarg,{filesync,[Name]}}}. +%%% API -info(Module, Name) when is_atom(Name) -> - try - gen_server:call(?name_to_reg_name(Module,Name), - info, ?DEFAULT_CALL_TIMEOUT) - catch - _:{timeout,_} -> {error,handler_busy} - end; -info(_, Name) -> - {error,{badarg,{info,[Name]}}}. +%% This function is called by the logger_sup supervisor +start_link(Args) -> + proc_lib:start_link(?MODULE,init,[Args]). -reset(Module, Name) when is_atom(Name) -> - try - gen_server:call(?name_to_reg_name(Module,Name), - reset, ?DEFAULT_CALL_TIMEOUT) - catch - _:{timeout,_} -> {error,handler_busy} - end; -reset(_, Name) -> - {error,{badarg,{reset,[Name]}}}. +filesync(Module, Name) -> + call(Module, Name, filesync). +info(Module, Name) -> + call(Module, Name, info). +reset(Module, Name) -> + call(Module, Name, reset). %%%----------------------------------------------------------------- %%% Handler being added adding_handler(#{id:=Name,module:=Module}=Config) -> - HConfig = maps:get(config, Config, #{}), - HandlerConfig0 = maps:without(?CONFIG_KEYS,HConfig), + HConfig0 = maps:get(config, Config, #{}), + HandlerConfig0 = maps:without(?CONFIG_KEYS,HConfig0), case Module:check_config(Name,set,undefined,HandlerConfig0) of {ok,HandlerConfig} -> ModifiedCommon = maps:with(?CONFIG_KEYS,HandlerConfig), - CommonConfig0 = maps:with(?CONFIG_KEYS,HConfig), + CommonConfig0 = maps:with(?CONFIG_KEYS,HConfig0), CommonConfig = maps:merge( maps:merge(get_default_config(), CommonConfig0), ModifiedCommon), case check_config(CommonConfig) of ok -> - State = maps:merge(get_init_state(), CommonConfig), - HConfig1 = maps:merge(CommonConfig,HandlerConfig), - Config1 = Config#{config=>HConfig1}, - case overload_levels_ok(State) of - true -> - start(Name, Config1, State); - false -> - #{sync_mode_qlen := SMQL, - drop_mode_qlen := DMQL, - flush_qlen := FQL} = State, - {error,{invalid_levels,{SMQL,DMQL,FQL}}} - end; + HConfig = maps:merge(CommonConfig,HandlerConfig), + start(Config#{config => HConfig}); {error,Faulty} -> {error,{invalid_config,Module,Faulty}} end; @@ -141,7 +113,7 @@ removing_handler(#{id:=Name, module:=Module}) -> %%%----------------------------------------------------------------- %%% Updating handler config changing_config(SetOrUpdate, - OldConfig=#{id:=Name,config:=OldHConfig,module:=Module}, + #{id:=Name,config:=OldHConfig,module:=Module}, NewConfig0) -> NewHConfig0 = maps:get(config, NewConfig0, #{}), OldHandlerConfig = maps:without(?CONFIG_KEYS++?READ_ONLY_KEYS,OldHConfig), @@ -169,12 +141,9 @@ changing_config(SetOrUpdate, ReadOnly), NewConfig = NewConfig0#{config=>NewHConfig}, HPid = maps:get(handler_pid,OldHConfig), - try gen_server:call(HPid, {change_config,OldConfig,NewConfig}, - ?DEFAULT_CALL_TIMEOUT) of + case call(HPid, {change_config,NewConfig}) of ok -> {ok,NewConfig}; Error -> Error - catch - _:{timeout,_} -> {error,handler_busy} end; {error,Faulty} -> {error,{invalid_config,Module,Faulty}} @@ -204,61 +173,35 @@ filter_config(#{config:=HConfig}=Config) -> Config#{config=>maps:without(?READ_ONLY_KEYS,HConfig)}. %%%----------------------------------------------------------------- -%%% Add a standard handler to the logger. -%%% This starts a dedicated handler process which should always -%%% exist if the handler is registered with logger (and should not -%%% exist if the handler is not registered). -%%% -%%% Handler specific config should be provided with a sub map associated -%%% with a key named 'config', e.g: +%%% Start the handler process %%% -%%% Config = #{config => #{sync_mode_qlen => 50} +%%% The process must always exist if the handler is registered with +%%% logger (and must not exist if the handler is not registered). %%% -%%% The standard handler process is linked to logger_sup, which is -%%% part of the kernel application's supervision tree. -start(Name, Config, HandlerState) -> +%%% The handler process is linked to logger_sup, which is part of the +%%% kernel application's supervision tree. +start(#{id := Name} = Config0) -> ChildSpec = #{id => Name, - start => {?MODULE, start_link, [Name,Config,HandlerState]}, + start => {?MODULE, start_link, [Config0]}, restart => temporary, shutdown => 2000, type => worker, modules => [?MODULE]}, case supervisor:start_child(logger_sup, ChildSpec) of - {ok,Pid,Config1} -> + {ok,Pid,Config} -> ok = logger_handler_watcher:register_handler(Name,Pid), - {ok,Config1}; + {ok,Config}; Error -> Error end. -%%%----------------------------------------------------------------- -%%% Start a standard handler process and link to caller. -%%% This function is called by the kernel supervisor when this -%%% handler process gets added --spec start_link(Name, Config, HandlerState) -> {ok,Pid} | {error,Reason} when - Name :: atom(), - Config :: logger:handler_config(), - HandlerState :: map(), - Pid :: pid(), - Reason :: term(). - -start_link(Name, Config, HandlerState) -> - proc_lib:start_link(?MODULE,init,[[Name,Config,HandlerState]]). - -%%%----------------------------------------------------------------- -%%% -get_init_state() -> - #{ctrl_sync_int => ?CONTROLLER_SYNC_INTERVAL, - filesync_ok_qlen => ?FILESYNC_OK_QLEN}. - %%%=================================================================== %%% gen_server callbacks %%%=================================================================== -init([Name, Config = #{config := HConfig, module := Module}, - State = #{filesync_repeat_interval := FSyncInt, - ctrl_sync_int := CtrlSyncInt}]) -> +init(#{id := Name, module := Module, + formatter := Formatter, config := HConfig0} = Config0) -> RegName = ?name_to_reg_name(Module,Name), register(RegName, self()), process_flag(trap_exit, true), @@ -267,43 +210,36 @@ init([Name, Config = #{config := HConfig, module := Module}, ?init_test_hooks(), ?start_observation(Name), - case Module:init(Name, HConfig) of + case Module:init(Name, HConfig0) of {ok,HState} -> try ets:new(Name, [public]) of ModeTab -> ?set_mode(ModeTab, async), T0 = ?timestamp(), - State1 = - ?merge_with_stats(State#{id => Name, - module => Module, - mode_tab => ModeTab, - mode => async, - ctrl_sync_count => CtrlSyncInt, - last_qlen => 0, - last_log_ts => T0, - last_op => sync, - burst_win_ts => T0, - burst_msg_count => 0, - handler_state => HState}), - Config1 = - Config#{config => HConfig#{handler_pid => self(), - mode_tab => ModeTab}}, - proc_lib:init_ack({ok,self(),Config1}), - if is_integer(FSyncInt) -> - gen_server:cast(self(), repeated_filesync); - true -> - ok - end, - case unset_restart_flag(Name, Module) of - true -> - %% inform about restart - gen_server:cast(self(), {log_handler_info, - "Handler ~p restarted", - [Name]}); - false -> - %% initial start - ok - end, + HConfig = HConfig0#{handler_pid => self(), + mode_tab => ModeTab}, + Config = Config0#{config => HConfig}, + proc_lib:init_ack({ok,self(),Config}), + %% Storing common config in state to avoid copying + %% (sending) the config data for each log message + CommonConfig = maps:with(?CONFIG_KEYS,HConfig), + State = + ?merge_with_stats( + CommonConfig#{id => Name, + module => Module, + mode_tab => ModeTab, + mode => async, + ctrl_sync_count => + ?CONTROLLER_SYNC_INTERVAL, + last_qlen => 0, + last_log_ts => T0, + last_op => sync, + burst_win_ts => T0, + burst_msg_count => 0, + formatter => Formatter, + handler_state => HState}), + State1 = set_repeated_filesync(State), + unset_restart_flag(State1), gen_server:enter_loop(?MODULE, [], State1) catch _:Error -> @@ -326,47 +262,33 @@ handle_call({log, Bin}, _From, State) -> handle_call(filesync, _From, State = #{id := Name, module := Module, handler_state := HandlerState}) -> - {Result,HandlerState1} = Module:sync_filesync(Name,HandlerState), + {Result,HandlerState1} = Module:filesync(Name,sync,HandlerState), {reply, Result, State#{handler_state=>HandlerState1, last_op=>sync}}; -handle_call({change_config,_OldConfig,NewConfig}, _From, +handle_call({change_config, #{formatter:=Formatter, config:=NewHConfig}}, _From, State = #{filesync_repeat_interval := FSyncInt0}) -> - HConfig = maps:get(config, NewConfig, #{}), - State1 = maps:merge(State, HConfig), - case overload_levels_ok(State1) of - true -> - _ = - case maps:get(filesync_repeat_interval, HConfig, undefined) of - undefined -> - ok; - no_repeat -> - _ = logger_h_common:cancel_timer(maps:get(rep_sync_tref, - State, - undefined)); - FSyncInt0 -> - ok; - _FSyncInt1 -> - _ = logger_h_common:cancel_timer(maps:get(rep_sync_tref, - State, - undefined)), - gen_server:cast(self(), repeated_filesync) - end, - {reply, ok, State1}; - false -> - #{sync_mode_qlen := SMQL, - drop_mode_qlen := DMQL, - flush_qlen := FQL} = State1, - {reply, {error,{invalid_levels,{SMQL,DMQL,FQL}}}, State} - end; + %% In the future, if handler_state must be updated due to config + %% change, then we need to add a callback to Module here. + CommonConfig = maps:with(?CONFIG_KEYS,NewHConfig), + State1 = maps:merge(State, CommonConfig), + State2 = + case maps:get(filesync_repeat_interval, NewHConfig) of + FSyncInt0 -> + State1; + _FSyncInt1 -> + set_repeated_filesync(cancel_repeated_filesync(State1)) + end, + {reply, ok, State2#{formatter:=Formatter}}; handle_call(info, _From, State) -> {reply, State, State}; -handle_call(reset, _From, #{module:=Module,handler_state:=HandlerState}=State) -> +handle_call(reset, _From, + #{id:=Name,module:=Module,handler_state:=HandlerState}=State) -> State1 = ?merge_with_stats(State), {reply, ok, State1#{last_qlen => 0, last_log_ts => ?timestamp(), - handler_state => Module:reset_state(HandlerState)}}; + handler_state => Module:reset_state(Name,HandlerState)}}; handle_call(stop, _From, State) -> {stop, {shutdown,stopped}, ok, State}. @@ -376,41 +298,37 @@ handle_cast({log, Bin}, State) -> {_,State1} = do_log(Bin, cast, State), {noreply, State1}; -handle_cast({log_handler_info, Format, Args}, State = #{id:=Name}) -> - log_handler_info(Name, Format, Args, State), - {noreply, State}; - %% If FILESYNC_REPEAT_INTERVAL is set to a millisec value, this %% clause gets called repeatedly by the handler. In order to %% guarantee that a filesync *always* happens after the last log %% event, the repeat operation must be active! handle_cast(repeated_filesync,State = #{filesync_repeat_interval := no_repeat}) -> + %% This clause handles a race condition which may occur when + %% config changes filesync_repeat_interval from an integer value + %% to no_repeat. {noreply,State}; handle_cast(repeated_filesync, State = #{id := Name, module := Module, handler_state := HandlerState, - filesync_repeat_interval := FSyncInt, last_op := LastOp}) -> - HandlerState1 = + State1 = if LastOp == sync -> - HandlerState; + State; true -> - Module:async_filesync(Name,HandlerState) + {_,HS} = Module:filesync(Name, async, HandlerState), + State#{handler_state => HS, last_op => sync} end, - {ok,TRef} = timer:apply_after(FSyncInt, gen_server,cast, - [self(),repeated_filesync]), - {noreply,State#{handler_state=>HandlerState1, - rep_sync_tref => TRef, - last_op => sync}}. + {noreply,set_repeated_filesync(State1)}. -handle_info(Info, #{module := Module, handler_state := HandlerState} = State) -> - {noreply,State#{handler_state => Module:handle_info(Info,HandlerState)}}. +handle_info(Info, #{id := Name, module := Module, + handler_state := HandlerState} = State) -> + {noreply,State#{handler_state => Module:handle_info(Name,Info,HandlerState)}}. terminate(Reason, State = #{id := Name, module := Module, handler_state := HandlerState}) -> - _ = cancel_timer(maps:get(rep_sync_tref, State, undefined)), + _ = cancel_repeated_filesync(State), _ = Module:terminate(Name, Reason, HandlerState), ok = stop_or_restart(Name, Reason, State), unregister(?name_to_reg_name(Module, Name)), @@ -420,10 +338,24 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. +%%%----------------------------------------------------------------- +%%% Internal functions +call(Module, Name, Op) when is_atom(Name) -> + call(?name_to_reg_name(Module,Name), Op); +call(_, Name, Op) -> + {error,{badarg,{Op,[Name]}}}. + +call(Server, Msg) -> + try + gen_server:call(Server, Msg, ?DEFAULT_CALL_TIMEOUT) + catch + _:{timeout,_} -> {error,handler_busy} + end. + %% check for overload between every event (and set Mode to async, %% sync or drop accordingly), but never flush the whole mailbox %% before LogWindowSize events have been handled -do_log(Bin, CallOrCast, State = #{id:=Name, module:=Module, mode:=Mode0}) -> +do_log(Bin, CallOrCast, State = #{id:=Name, mode:=Mode0}) -> T1 = ?timestamp(), %% check if the handler is getting overloaded, or if it's @@ -444,7 +376,7 @@ do_log(Bin, CallOrCast, State = #{id:=Name, module:=Module, mode:=Mode0}) -> end, %% kill the handler if it can't keep up with the load - kill_if_choked(Name, Module, QLen, Mem, State), + kill_if_choked(Name, QLen, Mem, State), if Mode1 == flush -> flush(Name, QLen, T1, State1); @@ -473,8 +405,9 @@ flush(Name, _QLen0, T1, State=#{last_log_ts := _T0, mode_tab := ModeTab}) -> ?observe(Name,{flushed,NewFlushed+1}), State1 = ?update_max_time(?diff_time(T1,_T0),State), + State2 = ?update_max_qlen(_QLen1,State1), {dropped,?update_other(flushed,FLUSHED,NewFlushed, - State1#{mode => ?set_mode(ModeTab,async), + State2#{mode => ?set_mode(ModeTab,async), last_qlen => 0, last_log_ts => T1})}. @@ -484,24 +417,23 @@ write(Name, Mode, T1, Bin, _CallOrCast, handler_state := HandlerState, mode_tab := ModeTab, ctrl_sync_count := CtrlSync, - ctrl_sync_int := CtrlSyncInt, last_qlen := LastQLen, last_log_ts := T0}) -> %% check if we need to limit the number of writes %% during a burst of log events {DoWrite,BurstWinT,BurstMsgCount} = limit_burst(State), - %% only log synhrounously every CtrlSyncInt time, to give the - %% handler time between writes so it can keep up with incoming - %% messages + %% only log synhrounously every ?CONTROLLER_SYNC_INTERVAL time, to + %% give the handler time between writes so it can keep up with + %% incoming messages {Result,LastQLen1,HandlerState1} = if DoWrite, CtrlSync == 0 -> ?observe(Name,{_CallOrCast,1}), - {_,HS1} = Module:sync_write(Name, Bin, HandlerState), + {_,HS1} = Module:write(Name, sync, Bin, HandlerState), {ok,element(2, process_info(self(), message_queue_len)),HS1}; DoWrite -> ?observe(Name,{_CallOrCast,1}), - HS1 = Module:async_write(Name, Bin, HandlerState), + {_,HS1} = Module:write(Name, async, Bin, HandlerState), {ok,LastQLen,HS1}; not DoWrite -> ?observe(Name,{flushed,1}), @@ -519,39 +451,37 @@ write(Name, Mode, T1, Bin, _CallOrCast, {Mode1,BurstMsgCount1,HandlerState2} = if (LastQLen1 < ?FILESYNC_OK_QLEN) andalso (Time > ?IDLE_DETECT_TIME_USEC) -> - HS2 = Module:async_filesync(Name,HandlerState), + {_,HS2} = Module:filesync(Name,async,HandlerState), {?change_mode(ModeTab, Mode, async),0,HS2}; true -> {Mode,BurstMsgCount,HandlerState1} end, - State1 = - ?update_calls_or_casts(_CallOrCast,1,State), - State2 = + State1 = ?update_calls_or_casts(_CallOrCast,1,State), + State2 = ?update_max_qlen(LastQLen1,State1), + State3 = ?update_max_time(Time, - State1#{mode => Mode1, + State2#{mode => Mode1, last_qlen := LastQLen1, last_log_ts => T1, last_op => write, burst_win_ts => BurstWinT, burst_msg_count => BurstMsgCount1, - ctrl_sync_count => if CtrlSync==0 -> CtrlSyncInt; - true -> CtrlSync-1 - end, + ctrl_sync_count => + if CtrlSync==0 -> ?CONTROLLER_SYNC_INTERVAL; + true -> CtrlSync-1 + end, handler_state => HandlerState2}), - {Result,State2}. + {Result,State3}. log_handler_info(Name, Format, Args, #{module:=Module, + formatter:=Formatter, handler_state:=HandlerState}) -> - Config = - case logger:get_handler_config(Name) of - {ok,Conf} -> Conf; - _ -> #{formatter=>{?DEFAULT_FORMATTER,?DEFAULT_FORMAT_CONFIG}} - end, + Config = #{formatter=>Formatter}, Meta = #{time=>erlang:system_time(microsecond)}, Bin = log_to_binary(#{level => notice, msg => {Format,Args}, meta => Meta}, Config), - _ = Module:async_write(Name, Bin, HandlerState), + _ = Module:write(Name, async, Bin, HandlerState), ok. %%%----------------------------------------------------------------- @@ -612,7 +542,20 @@ string_to_binary(String) -> %%%----------------------------------------------------------------- %%% Check that the configuration term is valid check_config(Config) when is_map(Config) -> - check_common_config(maps:to_list(Config)). + case check_common_config(maps:to_list(Config)) of + ok -> + case overload_levels_ok(Config) of + true -> + ok; + false -> + Faulty = maps:with([sync_mode_qlen, + drop_mode_qlen, + flush_qlen],Config), + {error,{invalid_levels,Faulty}} + end; + Error -> + Error + end. check_common_config([{sync_mode_qlen,N}|Config]) when is_integer(N) -> check_common_config(Config); @@ -667,15 +610,13 @@ call_cast_or_drop(_Name, HandlerPid, ModeTab, Bin) -> async -> gen_server:cast(HandlerPid, {log,Bin}); sync -> - try gen_server:call(HandlerPid, {log,Bin}, ?DEFAULT_CALL_TIMEOUT) of - %% if return value from call == dropped, the - %% message has been flushed by handler and should - %% therefore not be counted as dropped in stats - ok -> ok; - dropped -> ok - catch - _:{timeout,_} -> - ?observe(_Name,{dropped,1}) + case call(HandlerPid, {log,Bin}) of + ok -> + ok; + _Other -> + %% dropped or {error,handler_busy} + ?observe(_Name,{dropped,1}), + ok end; drop -> ?observe(_Name,{dropped,1}) @@ -686,10 +627,8 @@ call_cast_or_drop(_Name, HandlerPid, ModeTab, Bin) -> end, ok. -handler_exit(_Name, Reason) -> - exit(Reason). - -set_restart_flag(Name, Module) -> +set_restart_flag(#{id := Name, module := Module} = State) -> + log_handler_info(Name, "Handler ~p overloaded and stopping", [Name], State), Flag = list_to_atom(lists:concat([Module,"_",Name,"_restarting"])), spawn(fun() -> register(Flag, self()), @@ -697,14 +636,14 @@ set_restart_flag(Name, Module) -> end), ok. -unset_restart_flag(Name, Module) -> +unset_restart_flag(#{id := Name, module := Module} = State) -> Flag = list_to_atom(lists:concat([Module,"_",Name,"_restarting"])), case whereis(Flag) of undefined -> - false; + ok; Pid -> exit(Pid, kill), - true + log_handler_info(Name, "Handler ~p restarted", [Name], State) end. check_load(State = #{id:=_Name, mode_tab := ModeTab, mode := Mode, @@ -764,24 +703,17 @@ limit_burst(#{burst_win_ts := BurstWinT0, {true,BurstWinT0,BurstMsgCount+1} end. -kill_if_choked(Name, Module, QLen, Mem, - State = #{overload_kill_enable := KillIfOL, - overload_kill_qlen := OLKillQLen, - overload_kill_mem_size := OLKillMem}) -> +kill_if_choked(Name, QLen, Mem, State = #{overload_kill_enable := KillIfOL, + overload_kill_qlen := OLKillQLen, + overload_kill_mem_size := OLKillMem}) -> if KillIfOL andalso ((QLen > OLKillQLen) orelse (Mem > OLKillMem)) -> - log_handler_info(Name, - "Handler ~p overloaded and stopping", - [Name], State), - set_restart_flag(Name, Module), - handler_exit(Name, {shutdown,{overloaded,Name,QLen,Mem}}); + set_restart_flag(State), + exit({shutdown,{overloaded,Name,QLen,Mem}}); true -> ok end. -flush_log_events() -> - flush_log_events(-1). - flush_log_events(Limit) -> process_flag(priority, high), Flushed = flush_log_events(0, Limit), @@ -804,14 +736,27 @@ flush_log_events(N, Limit) -> 0 -> N end. -cancel_timer(TRef) when is_atom(TRef) -> ok; -cancel_timer(TRef) -> timer:cancel(TRef). - +set_repeated_filesync(#{filesync_repeat_interval:=FSyncInt} = State) + when is_integer(FSyncInt) -> + {ok,TRef} = timer:apply_after(FSyncInt, gen_server, cast, + [self(),repeated_filesync]), + State#{rep_sync_tref=>TRef}; +set_repeated_filesync(State) -> + State. + +cancel_repeated_filesync(State) -> + case maps:take(rep_sync_tref,State) of + {TRef,State1} -> + _ = timer:cancel(TRef), + State1; + error -> + State + end. stop_or_restart(Name, {shutdown,Reason={overloaded,_Name,_QLen,_Mem}}, #{overload_kill_restart_after := RestartAfter}) -> %% If we're terminating because of an overload situation (see - %% kill_if_choked/5), we need to remove the handler and set a + %% kill_if_choked/4), we need to remove the handler and set a %% restart timer. A separate process must perform this in order to %% avoid deadlock. HandlerPid = self(), diff --git a/lib/kernel/src/logger_std_h.erl b/lib/kernel/src/logger_std_h.erl index 95e7d17c0d..ebe741e331 100644 --- a/lib/kernel/src/logger_std_h.erl +++ b/lib/kernel/src/logger_std_h.erl @@ -29,10 +29,8 @@ -export([info/1, filesync/1, reset/1]). %% logger_h_common callbacks --export([init/2, check_config/4, reset_state/1, - async_filesync/2, sync_filesync/2, - async_write/3, sync_write/3, - handle_info/2, terminate/3]). +-export([init/2, check_config/4, reset_state/2, + filesync/3, write/4, handle_info/3, terminate/3]). %% logger callbacks -export([log/2, adding_handler/1, removing_handler/1, changing_config/3, @@ -71,21 +69,36 @@ reset(Name) -> logger_h_common:reset(?MODULE,Name). %%%=================================================================== -%%% logger callbacks +%%% logger callbacks - just forward to logger_h_common %%%=================================================================== %%%----------------------------------------------------------------- %%% Handler being added +-spec adding_handler(Config) -> {ok,Config} | {error,Reason} when + Config :: logger:handler_config(), + Reason :: term(). + adding_handler(Config) -> logger_h_common:adding_handler(Config). %%%----------------------------------------------------------------- %%% Updating handler config +-spec changing_config(SetOrUpdate, OldConfig, NewConfig) -> + {ok,Config} | {error,Reason} when + SetOrUpdate :: set | update, + OldConfig :: logger:handler_config(), + NewConfig :: logger:handler_config(), + Config :: logger:handler_config(), + Reason :: term(). + changing_config(SetOrUpdate, OldConfig, NewConfig) -> logger_h_common:changing_config(SetOrUpdate, OldConfig, NewConfig). %%%----------------------------------------------------------------- %%% Handler being removed +-spec removing_handler(Config) -> ok when + Config :: logger:handler_config(). + removing_handler(Config) -> logger_h_common:removing_handler(Config). @@ -100,6 +113,9 @@ log(LogEvent, Config) -> %%%----------------------------------------------------------------- %%% Remove internal fields from configuration +-spec filter_config(Config) -> Config when + Config :: logger:handler_config(). + filter_config(Config) -> logger_h_common:filter_config(Config). @@ -163,33 +179,29 @@ check_h_config([]) -> get_default_config() -> #{type => standard_io}. -async_filesync(_Name,#{type := Type}=State) when is_atom(Type) -> - State; -async_filesync(_Name,#{file_ctrl_pid := FileCtrlPid}=State) -> +filesync(_Name, _Mode, #{type := Type}=State) when is_atom(Type) -> + {ok,State}; +filesync(_Name, async, #{file_ctrl_pid := FileCtrlPid} = State) -> ok = file_ctrl_filesync_async(FileCtrlPid), - State. - -sync_filesync(_Name,#{type := Type}=State) when is_atom(Type) -> {ok,State}; -sync_filesync(_Name,#{file_ctrl_pid := FileCtrlPid}=State) -> +filesync(_Name, sync, #{file_ctrl_pid := FileCtrlPid} = State) -> Result = file_ctrl_filesync_sync(FileCtrlPid), {Result,State}. -async_write(_Name, Bin, #{file_ctrl_pid:=FileCtrlPid} = State) -> +write(_Name, async, Bin, #{file_ctrl_pid:=FileCtrlPid} = State) -> ok = file_write_async(FileCtrlPid, Bin), - State. - -sync_write(_Name, Bin, #{file_ctrl_pid:=FileCtrlPid} = State) -> + {ok,State}; +write(_Name, sync, Bin, #{file_ctrl_pid:=FileCtrlPid} = State) -> Result = file_write_sync(FileCtrlPid, Bin), {Result,State}. -reset_state(State) -> +reset_state(_Name, State) -> State. -handle_info({'EXIT',Pid,Why}, #{type := FileInfo, file_ctrl_pid := Pid}) -> +handle_info(_Name, {'EXIT',Pid,Why}, #{type := FileInfo, file_ctrl_pid := Pid}) -> %% file_ctrl_pid died, file error, terminate handler exit({error,{write_failed,FileInfo,Why}}); -handle_info(_, State) -> +handle_info(_, _, State) -> State. terminate(_Name, _Reason, #{file_ctrl_pid:=FWPid}) -> @@ -203,7 +215,8 @@ terminate(_Name, _Reason, #{file_ctrl_pid:=FWPid}) -> ok after ?DEFAULT_CALL_TIMEOUT -> - exit(FWPid, kill) + exit(FWPid, kill), + ok end; false -> ok @@ -273,18 +286,18 @@ file_write_async(Pid, Bin) -> ok. file_write_sync(Pid, Bin) -> - file_ctrl_call(Pid, {log,self(),Bin}). + file_ctrl_call(Pid, {log,Bin}). file_ctrl_filesync_async(Pid) -> Pid ! filesync, ok. file_ctrl_filesync_sync(Pid) -> - file_ctrl_call(Pid, {filesync,self()}). + file_ctrl_call(Pid, filesync). file_ctrl_call(Pid, Msg) -> MRef = monitor(process, Pid), - Pid ! {Msg,MRef}, + Pid ! {Msg,{self(),MRef}}, receive {MRef,Result} -> demonitor(MRef, [flush]), @@ -302,63 +315,40 @@ file_ctrl_init(HandlerName, FileInfo, Starter) when is_tuple(FileInfo) -> case do_open_log_file(FileInfo) of {ok,Fd} -> Starter ! {self(),ok}, - file_ctrl_loop(Fd, file, FileName, false, ok, ok, HandlerName); + file_ctrl_loop(Fd, FileName, false, ok, ok, HandlerName); {error,Reason} -> Starter ! {self(),{error,{open_failed,FileName,Reason}}} end; file_ctrl_init(HandlerName, StdDev, Starter) -> Starter ! {self(),ok}, - file_ctrl_loop(StdDev, standard_io, StdDev, false, ok, ok, HandlerName). + file_ctrl_loop(StdDev, StdDev, false, ok, ok, HandlerName). -file_ctrl_loop(Fd, Type, DevName, Synced, +file_ctrl_loop(Fd, DevName, Synced, PrevWriteResult, PrevSyncResult, HandlerName) -> receive %% asynchronous event {log,Bin} -> - Result = if Type == file -> - write_to_dev(Fd, Bin, DevName, - PrevWriteResult, HandlerName); - true -> - io:put_chars(Fd, Bin) - end, - file_ctrl_loop(Fd, Type, DevName, false, + Result = write_to_dev(Fd, Bin, DevName, PrevWriteResult, HandlerName), + file_ctrl_loop(Fd, DevName, false, Result, PrevSyncResult, HandlerName); %% synchronous event - {{log,From,Bin},MRef} -> - WResult = - if Type == file -> - %% check that file hasn't been deleted - CheckFile = - fun() -> {ok,_} = file:read_file_info(DevName) end, - spawn_link(CheckFile), - write_to_dev(Fd, Bin, DevName, - PrevWriteResult, HandlerName); - true -> - _ = io:put_chars(Fd, Bin), - ok - end, + {{log,Bin},{From,MRef}} -> + check_exist(Fd, DevName), + Result = write_to_dev(Fd, Bin, DevName, PrevWriteResult, HandlerName), From ! {MRef,ok}, - file_ctrl_loop(Fd, Type, DevName, false, - WResult, PrevSyncResult, HandlerName); + file_ctrl_loop(Fd, DevName, false, + Result, PrevSyncResult, HandlerName); - filesync when not Synced -> - Result = sync_dev(Fd, DevName, PrevSyncResult, HandlerName), - file_ctrl_loop(Fd, Type, DevName, true, + filesync -> + Result = sync_dev(Fd, DevName, Synced, PrevSyncResult, HandlerName), + file_ctrl_loop(Fd, DevName, true, PrevWriteResult, Result, HandlerName); - filesync -> - file_ctrl_loop(Fd, Type, DevName, true, - PrevWriteResult, PrevSyncResult, HandlerName); - - {{filesync,From},MRef} -> - Result = if not Synced -> - sync_dev(Fd, DevName, PrevSyncResult, HandlerName); - true -> - ok - end, + {filesync,{From,MRef}} -> + Result = sync_dev(Fd, DevName, Synced, PrevSyncResult, HandlerName), From ! {MRef,ok}, - file_ctrl_loop(Fd, Type, DevName, true, + file_ctrl_loop(Fd, DevName, true, PrevWriteResult, Result, HandlerName); stop -> @@ -366,26 +356,30 @@ file_ctrl_loop(Fd, Type, DevName, Synced, stopped end. +check_exist(DevName, DevName) when is_atom(DevName) -> + ok; +check_exist(_Fd, FileName) -> + _ = spawn_link(fun() -> {ok,_} = file:read_file_info(FileName) end), + ok. + +write_to_dev(DevName, Bin, _DevName, _PrevWriteResult, _HandlerName) + when is_atom(DevName) -> + io:put_chars(DevName, Bin); write_to_dev(Fd, Bin, FileName, PrevWriteResult, HandlerName) -> - case ?file_write(Fd, Bin) of - ok -> - ok; - PrevWriteResult -> - %% don't report same error twice - PrevWriteResult; - Error -> - logger_h_common:error_notify({HandlerName,write,FileName,Error}), - Error - end. + Result = ?file_write(Fd, Bin), + maybe_notify_error(write,Result,PrevWriteResult,FileName,HandlerName). -sync_dev(Fd, DevName, PrevSyncResult, HandlerName) -> - case ?file_datasync(Fd) of - ok -> - ok; - PrevSyncResult -> - %% don't report same error twice - PrevSyncResult; - Error -> - logger_h_common:error_notify({HandlerName,filesync,DevName,Error}), - Error - end. +sync_dev(_Fd, _FileName, true, PrevSyncResult, _HandlerName) -> + PrevSyncResult; +sync_dev(Fd, FileName, false, PrevSyncResult, HandlerName) -> + Result = ?file_datasync(Fd), + maybe_notify_error(filesync,Result,PrevSyncResult,FileName,HandlerName). + +maybe_notify_error(_Op, ok, _PrevResult, _FileName, _HandlerName) -> + ok; +maybe_notify_error(_Op, PrevResult, PrevResult, _FileName, _HandlerName) -> + %% don't report same error twice + PrevResult; +maybe_notify_error(Op, Error, _PrevResult, FileName, HandlerName) -> + logger_h_common:error_notify({HandlerName,Op,FileName,Error}), + Error. diff --git a/lib/kernel/test/logger_disk_log_h_SUITE.erl b/lib/kernel/test/logger_disk_log_h_SUITE.erl index 7bac4c2cb2..2bd49432b2 100644 --- a/lib/kernel/test/logger_disk_log_h_SUITE.erl +++ b/lib/kernel/test/logger_disk_log_h_SUITE.erl @@ -419,14 +419,19 @@ config_fail(_Config) -> filter_default=>log, formatter=>{?MODULE,self()}}), - {error,{handler_not_added,{invalid_levels,{_,1,_}}}} = + {error,{handler_not_added,{invalid_config,logger_disk_log_h, + {invalid_levels,#{drop_mode_qlen:=1}}}}} = logger:add_handler(?MODULE,logger_disk_log_h, #{config => #{drop_mode_qlen=>1}}), - {error,{handler_not_added,{invalid_levels,{43,42,_}}}} = + {error,{handler_not_added,{invalid_config,logger_disk_log_h, + {invalid_levels,#{sync_mode_qlen:=43, + drop_mode_qlen:=42}}}}} = logger:add_handler(?MODULE,logger_disk_log_h, #{config => #{sync_mode_qlen=>43, drop_mode_qlen=>42}}), - {error,{handler_not_added,{invalid_levels,{_,43,42}}}} = + {error,{handler_not_added,{invalid_config,logger_disk_log_h, + {invalid_levels,#{drop_mode_qlen:=43, + flush_qlen:=42}}}}} = logger:add_handler(?MODULE,logger_disk_log_h, #{config => #{drop_mode_qlen=>43, flush_qlen=>42}}), @@ -440,7 +445,7 @@ config_fail(_Config) -> #{max_no_files=>2}), %% incorrect values of OP params {ok,#{config := HConfig}} = logger:get_handler_config(?MODULE), - {error,{invalid_levels,_}} = + {error,{invalid_config,logger_disk_log_h,{invalid_levels,_}}} = logger:update_handler_config(?MODULE,config, HConfig#{sync_mode_qlen=>100, flush_qlen=>99}), @@ -672,9 +677,9 @@ sync(Config) -> SyncInt = 1000, WaitT = 4500, OneSync = {logger_h_common,handle_cast,repeated_filesync}, - %% receive 1 initial repeated_filesync, then 1 per sec + %% receive 1 repeated_filesync per sec start_tracer([{logger_h_common,handle_cast,2}], - [OneSync || _ <- lists:seq(1, 1 + trunc(WaitT/SyncInt))]), + [OneSync || _ <- lists:seq(1, trunc(WaitT/SyncInt))]), HConfig2 = HConfig#{filesync_repeat_interval => SyncInt}, ok = logger:update_handler_config(?MODULE, config, HConfig2), @@ -717,7 +722,7 @@ disk_log_wrap(Config) -> end, {ok,_} = dbg:tracer(process, {TraceFun, Tester}), {ok,_} = dbg:p(whereis(h_proc_name()), [c]), - {ok,_} = dbg:tp(logger_disk_log_h, handle_info, 2, []), + {ok,_} = dbg:tp(logger_disk_log_h, handle_info, 3, []), Text = [34 + rand:uniform(126-34) || _ <- lists:seq(1,MaxBytes)], ct:pal("String = ~p (~w)", [Text, erts_debug:size(Text)]), @@ -735,7 +740,7 @@ disk_log_wrap(Config) -> timer:sleep(1000), dbg:stop_clear(), Received = lists:flatmap(fun({trace,_M,handle_info, - [{disk_log,_Node,_Name,What},_]}) -> + [_,{disk_log,_Node,_Name,What},_]}) -> [{trace,What}]; ({log,_}) -> [] @@ -771,7 +776,7 @@ disk_log_full(Config) -> end, {ok,_} = dbg:tracer(process, {TraceFun, Tester}), {ok,_} = dbg:p(whereis(h_proc_name()), [c]), - {ok,_} = dbg:tp(logger_disk_log_h, handle_info, 2, []), + {ok,_} = dbg:tp(logger_disk_log_h, handle_info, 3, []), NoOfChars = 5, Text = [34 + rand:uniform(126-34) || _ <- lists:seq(1,NoOfChars)], @@ -781,7 +786,7 @@ disk_log_full(Config) -> timer:sleep(2000), dbg:stop_clear(), Received = lists:flatmap(fun({trace,_M,handle_info, - [{disk_log,_Node,_Name,What},_]}) -> + [_,{disk_log,_Node,_Name,What},_]}) -> [{trace,What}]; ({log,_}) -> [] @@ -820,14 +825,14 @@ disk_log_events(Config) -> end, {ok,_} = dbg:tracer(process, {TraceFun, Tester}), {ok,_} = dbg:p(whereis(h_proc_name()), [c]), - {ok,_} = dbg:tp(logger_disk_log_h, handle_info, 2, []), + {ok,_} = dbg:tp(logger_disk_log_h, handle_info, 3, []), [whereis(h_proc_name()) ! E || E <- Events], %% wait for trace messages timer:sleep(2000), dbg:stop_clear(), Received = lists:map(fun({trace,_M,handle_info, - [Got,_]}) -> Got + [_,Got,_]}) -> Got end, test_server:messages_get()), ct:pal("Trace =~n~p", [Received]), NoOfEvents = length(Events), diff --git a/lib/kernel/test/logger_std_h_SUITE.erl b/lib/kernel/test/logger_std_h_SUITE.erl index affa04d410..a1159f280c 100644 --- a/lib/kernel/test/logger_std_h_SUITE.erl +++ b/lib/kernel/test/logger_std_h_SUITE.erl @@ -319,14 +319,19 @@ config_fail(_Config) -> #{config => #{restart_type => bad}, filter_default=>log, formatter=>{?MODULE,self()}}), - {error,{handler_not_added,{invalid_levels,{_,1,_}}}} = + {error,{handler_not_added,{invalid_config,logger_std_h, + {invalid_levels,#{drop_mode_qlen:=1}}}}} = logger:add_handler(?MODULE,logger_std_h, #{config => #{drop_mode_qlen=>1}}), - {error,{handler_not_added,{invalid_levels,{43,42,_}}}} = + {error,{handler_not_added,{invalid_config,logger_std_h, + {invalid_levels,#{sync_mode_qlen:=43, + drop_mode_qlen:=42}}}}} = logger:add_handler(?MODULE,logger_std_h, #{config => #{sync_mode_qlen=>43, drop_mode_qlen=>42}}), - {error,{handler_not_added,{invalid_levels,{_,43,42}}}} = + {error,{handler_not_added,{invalid_config,logger_std_h, + {invalid_levels,#{drop_mode_qlen:=43, + flush_qlen:=42}}}}} = logger:add_handler(?MODULE,logger_std_h, #{config => #{drop_mode_qlen=>43, flush_qlen=>42}}), @@ -338,7 +343,7 @@ config_fail(_Config) -> logger:set_handler_config(?MODULE,config, #{type=>{file,"file"}}), - {error,{invalid_levels,_}} = + {error,{invalid_config,logger_std_h,{invalid_levels,_}}} = logger:set_handler_config(?MODULE,config, #{sync_mode_qlen=>100, flush_qlen=>99}), @@ -643,10 +648,8 @@ sync(Config) -> %% check repeated filesync happens start_tracer([{logger_std_h, write_to_dev, 5}, - {logger_std_h, sync_dev, 4}, {file, datasync, 1}], [{logger_std_h, write_to_dev, <<"first\n">>}, - {logger_std_h, sync_dev}, {file,datasync}]), logger:notice("first", ?domain), @@ -655,10 +658,8 @@ sync(Config) -> %% check that explicit filesync is only done once start_tracer([{logger_std_h, write_to_dev, 5}, - {logger_std_h, sync_dev, 4}, {file, datasync, 1}], [{logger_std_h, write_to_dev, <<"second\n">>}, - {logger_std_h, sync_dev}, {file,datasync}, {no_more,500} ]), @@ -679,13 +680,10 @@ sync(Config) -> %% triggered by the idle timeout between "thrid" and "fourth". timer:sleep(?IDLE_DETECT_TIME_MSEC*2), start_tracer([{logger_std_h, write_to_dev, 5}, - {logger_std_h, sync_dev, 4}, {file, datasync, 1}], [{logger_std_h, write_to_dev, <<"third\n">>}, - {logger_std_h, sync_dev}, {file,datasync}, {logger_std_h, write_to_dev, <<"fourth\n">>}, - {logger_std_h, sync_dev}, {file,datasync}]), logger:notice("third", ?domain), %% wait for automatic filesync @@ -698,9 +696,9 @@ sync(Config) -> SyncInt = 1000, WaitT = 4500, OneSync = {logger_h_common,handle_cast,repeated_filesync}, - %% receive 1 initial repeated_filesync, then 1 per sec + %% receive 1 repeated_filesync per sec start_tracer([{logger_h_common,handle_cast,2}], - [OneSync || _ <- lists:seq(1, 1 + trunc(WaitT/SyncInt))]), + [OneSync || _ <- lists:seq(1, trunc(WaitT/SyncInt))]), ok = logger:update_handler_config(?MODULE, config, #{filesync_repeat_interval => SyncInt}), -- cgit v1.2.3