%-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- % ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod: %%% %%%------------------------------------------------------------------------ %%% @doc %%% ==CloudI Availability Tracking Functionality== %%% @end %%% %%% MIT License %%% %%% Copyright (c) 2018-2023 Michael Truog %%% %%% Permission is hereby granted, free of charge, to any person obtaining a %%% copy of this software and associated documentation files (the "Software"), %%% to deal in the Software without restriction, including without limitation %%% the rights to use, copy, modify, merge, publish, distribute, sublicense, %%% and/or sell copies of the Software, and to permit persons to whom the %%% Software is furnished to do so, subject to the following conditions: %%% %%% The above copyright notice and this permission notice shall be included in %%% all copies or substantial portions of the Software. %%% %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %%% DEALINGS IN THE SOFTWARE. %%% %%% @author Michael Truog %%% @copyright 2018-2023 Michael Truog %%% @version 2.0.6 {@date} {@time} %%%------------------------------------------------------------------------ -module(cloudi_availability). -author('mjtruog at protonmail dot com'). %% external interface -export([durations_copy/2, durations_erase/2, durations_new/0, durations_size/1, durations_state/2, durations_store/3, durations_store_difference/4, durations_sum/2, durations_sum_with_view/5, nanoseconds_to_availability_day/1, nanoseconds_to_availability_day/3, nanoseconds_to_availability_week/1, nanoseconds_to_availability_week/3, nanoseconds_to_availability_month/1, nanoseconds_to_availability_month/3, nanoseconds_to_availability_year/1, nanoseconds_to_availability_year/3, nanoseconds_to_string_gt/2, nanoseconds_to_string_lt/2]). -include("cloudi_availability.hrl"). -include("cloudi_core_i_constants.hrl"). % when tracking the durations of downtime during the past year, limit the % number of durations to this max to keep memory consumption low % (if durations are discarded, the downtime result is considered approximate) -define(DURATIONS_YEAR_MAX, 366). -type nanoseconds() :: non_neg_integer(). -type duration() :: {T0 :: cloudi_timestamp:native_monotonic(), T1 :: cloudi_timestamp:native_monotonic()}. -type durations_state() :: {DurationCount :: non_neg_integer(), DurationList :: list(duration())}. %%%------------------------------------------------------------------------ %%% External interface functions %%%------------------------------------------------------------------------ -type durations(Key) :: #{Key := durations_state()} | #{}. -export_type([durations/1]). -spec durations_copy(KeyList :: list(), DurationsLookup :: durations(any())) -> durations(any()). durations_copy(KeyList, DurationsLookup) -> maps:with(KeyList, DurationsLookup). -spec durations_erase(Key :: any(), DurationsLookup :: durations(any())) -> durations(any()). durations_erase(Key, DurationsLookup) -> maps:remove(Key, DurationsLookup). -spec durations_new() -> #{}. durations_new() -> #{}. -spec durations_size(DurationsLookup :: durations(any())) -> non_neg_integer(). durations_size(DurationsLookup) -> maps:size(DurationsLookup). -spec durations_state(Key :: any(), DurationsLookup :: durations(any())) -> durations_state(). durations_state(Key, DurationsLookup) -> maps:get(Key, DurationsLookup, {0, []}). -spec durations_store(KeyList :: nonempty_list(), Duration :: duration(), DurationsLookup :: durations(any())) -> durations(any()). durations_store([_ | _] = KeyList, {_, T1} = Duration, DurationsLookup) -> duration_store(KeyList, T1 - ?NATIVE_TIME_IN_YEAR, Duration, DurationsLookup). -spec durations_store_difference(KeyList :: nonempty_list(), Duration :: duration(), DurationsAddLookup :: durations(any()), DurationsSubtractLookup :: durations(any())) -> durations(any()). durations_store_difference([_ | _] = KeyList, {_, T1} = Duration, DurationsAddLookup, DurationsSubtractLookup) -> duration_store_difference(KeyList, T1 - ?NATIVE_TIME_IN_YEAR, Duration, DurationsAddLookup, DurationsSubtractLookup). -spec durations_sum(durations_state(), T :: cloudi_timestamp:native_monotonic()) -> {boolean(), nanoseconds()}. durations_sum({DurationCount, DurationList}, T) -> durations_sum(DurationList, 0, DurationCount == ?DURATIONS_YEAR_MAX, T). -spec durations_sum_with_view(durations_state(), T :: cloudi_timestamp:native_monotonic(), TNow :: cloudi_timestamp:native_monotonic(), TPeriod :: year | month | week | day, TimeOffset :: integer()) -> {boolean(), nanoseconds(), nonempty_string()}. durations_sum_with_view({DurationCount, DurationList}, T, TNow, TPeriod, TimeOffset) -> {Tv, View} = durations_view_new(TNow, TPeriod, TimeOffset), durations_sum_with_view(DurationList, 0, DurationCount == ?DURATIONS_YEAR_MAX, View, Tv, T, TNow, TPeriod, TimeOffset). -spec nanoseconds_to_availability_day(NanoSecondsUptime :: nanoseconds()) -> cloudi_service_api:availability(). nanoseconds_to_availability_day(NanoSecondsUptime) -> availability_to_string(NanoSecondsUptime / ?NANOSECONDS_IN_DAY). -spec nanoseconds_to_availability_day(NanoSecondsUptime :: nanoseconds(), Approximate :: boolean(), NanoSecondsDowntime :: nanoseconds()) -> cloudi_service_api:availability_approx(). nanoseconds_to_availability_day(NanoSecondsUptime, Approximate, NanoSecondsDowntime) -> NanoSecondsDay = ?NANOSECONDS_IN_DAY, AvailabilityDay = (erlang:min(NanoSecondsUptime, NanoSecondsDay) - NanoSecondsDowntime) / NanoSecondsDay, availability_to_string(AvailabilityDay, Approximate). -spec nanoseconds_to_availability_week(NanoSecondsUptime :: nanoseconds()) -> cloudi_service_api:availability(). nanoseconds_to_availability_week(NanoSecondsUptime) -> availability_to_string(NanoSecondsUptime / ?NANOSECONDS_IN_WEEK). -spec nanoseconds_to_availability_week(NanoSecondsUptime :: nanoseconds(), Approximate :: boolean(), NanoSecondsDowntime :: nanoseconds()) -> cloudi_service_api:availability_approx(). nanoseconds_to_availability_week(NanoSecondsUptime, Approximate, NanoSecondsDowntime) -> NanoSecondsWeek = ?NANOSECONDS_IN_WEEK, AvailabilityWeek = (erlang:min(NanoSecondsUptime, NanoSecondsWeek) - NanoSecondsDowntime) / NanoSecondsWeek, availability_to_string(AvailabilityWeek, Approximate). -spec nanoseconds_to_availability_month(NanoSecondsUptime :: nanoseconds()) -> cloudi_service_api:availability(). nanoseconds_to_availability_month(NanoSecondsUptime) -> availability_to_string(NanoSecondsUptime / ?NANOSECONDS_IN_MONTH). -spec nanoseconds_to_availability_month(NanoSecondsUptime :: nanoseconds(), Approximate :: boolean(), NanoSecondsDowntime :: nanoseconds()) -> cloudi_service_api:availability_approx(). nanoseconds_to_availability_month(NanoSecondsUptime, Approximate, NanoSecondsDowntime) -> NanoSecondsMonth = ?NANOSECONDS_IN_MONTH, AvailabilityMonth = (erlang:min(NanoSecondsUptime, NanoSecondsMonth) - NanoSecondsDowntime) / NanoSecondsMonth, availability_to_string(AvailabilityMonth, Approximate). -spec nanoseconds_to_availability_year(NanoSecondsUptime :: nanoseconds()) -> cloudi_service_api:availability(). nanoseconds_to_availability_year(NanoSecondsUptime) -> availability_to_string(NanoSecondsUptime / ?NANOSECONDS_IN_YEAR). -spec nanoseconds_to_availability_year(NanoSecondsUptime :: nanoseconds(), Approximate :: boolean(), NanoSecondsDowntime :: nanoseconds()) -> cloudi_service_api:availability_approx(). nanoseconds_to_availability_year(NanoSecondsUptime, Approximate, NanoSecondsDowntime) -> NanoSecondsYear = ?NANOSECONDS_IN_YEAR, AvailabilityYear = (erlang:min(NanoSecondsUptime, NanoSecondsYear) - NanoSecondsDowntime) / NanoSecondsYear, availability_to_string(AvailabilityYear, Approximate). -spec nanoseconds_to_string_gt(NanoSeconds :: nanoseconds(), Approximate :: boolean()) -> cloudi_service_api:nanoseconds_string_approx_gt(). nanoseconds_to_string_gt(NanoSeconds, true) -> [$>, $ | cloudi_timestamp:nanoseconds_to_string(NanoSeconds)]; nanoseconds_to_string_gt(NanoSeconds, false) -> cloudi_timestamp:nanoseconds_to_string(NanoSeconds). -spec nanoseconds_to_string_lt(NanoSeconds :: nanoseconds(), Approximate :: boolean()) -> cloudi_service_api:nanoseconds_string_approx_lt(). nanoseconds_to_string_lt(NanoSeconds, true) -> [$<, $ | cloudi_timestamp:nanoseconds_to_string(NanoSeconds)]; nanoseconds_to_string_lt(NanoSeconds, false) -> cloudi_timestamp:nanoseconds_to_string(NanoSeconds). %%%------------------------------------------------------------------------ %%% Private functions %%%------------------------------------------------------------------------ duration_store_clear(Key, T, Duration, DurationsLookup) -> {DurationCount, DurationList} = durations_state(Key, DurationsLookup), {DurationCountNew, DurationListNew} = duration_clear(lists:reverse(DurationList), DurationCount, T), DurationsStateNew = {DurationCountNew + 1, [Duration | DurationListNew]}, maps:put(Key, DurationsStateNew, DurationsLookup). duration_store([], _, _, DurationsLookup) -> DurationsLookup; duration_store([Key | KeyList], T, Duration, DurationsLookup) -> duration_store(KeyList, T, Duration, duration_store_clear(Key, T, Duration, DurationsLookup)). duration_store_difference([], _, _, DurationsAddLookup, _) -> DurationsAddLookup; duration_store_difference([Key | KeyList], T, Duration, DurationsAddLookup, DurationsSubtractLookup) -> DurationsAddLookupNew = case maps:find(Key, DurationsSubtractLookup) of error -> duration_store_clear(Key, T, Duration, DurationsAddLookup); {ok, {0, []}} -> duration_store_clear(Key, T, Duration, DurationsAddLookup); {ok, {_, DurationSubtractList}} -> {DurationAddCount, DurationAddList} = durations_subtract(DurationSubtractList, Duration), {DurationCount, DurationList} = durations_state(Key, DurationsAddLookup), DurationCountNew = DurationAddCount + DurationCount, DurationListNew = DurationAddList ++ DurationList, DurationsStateNew = duration_clear(lists:reverse(DurationListNew), DurationCountNew, T), maps:put(Key, DurationsStateNew, DurationsAddLookup) end, duration_store_difference(KeyList, T, Duration, DurationsAddLookupNew, DurationsSubtractLookup). durations_subtract([], Duration, DurationList, DurationCount) -> {DurationCount + 1, lists:reverse([Duration | DurationList])}; durations_subtract([{SegmentT0, SegmentT1} | _], {T0, T1}, DurationList, DurationCount) when SegmentT0 =< T0, SegmentT1 >= T1 -> {DurationCount, lists:reverse(DurationList)}; durations_subtract([{_, SegmentT1} | DurationSubtractList], {T0, _} = Duration, DurationList, DurationCount) when SegmentT1 =< T0 -> durations_subtract(DurationSubtractList, Duration, DurationList, DurationCount); durations_subtract([{SegmentT0, _} | _], {_, T1} = Duration, DurationList, DurationCount) when SegmentT0 >= T1 -> {DurationCount + 1, lists:reverse([Duration | DurationList])}; durations_subtract([{SegmentT0, SegmentT1} | DurationSubtractList], {T0, T1}, DurationList, DurationCount) -> true = (SegmentT0 > T0) andalso (SegmentT1 < T1), durations_subtract(DurationSubtractList, {SegmentT1, T1}, [{T0, SegmentT0} | DurationList], DurationCount + 1). durations_subtract(DurationSubtractList, Duration) -> durations_subtract(DurationSubtractList, Duration, [], 0). duration_clear([] = DurationList, 0 = DurationCount, _) -> {DurationCount, DurationList}; duration_clear([{_, T1} | DurationList], DurationCount, T) when T1 < T -> duration_clear(DurationList, DurationCount - 1, T); duration_clear([_ | DurationList], DurationCount, T) when DurationCount >= ?DURATIONS_YEAR_MAX -> % duration list becomes approximate when it hits max size duration_clear(DurationList, DurationCount - 1, T); duration_clear(DurationList, DurationCount, _) -> {DurationCount, lists:reverse(DurationList)}. durations_sum([], NativeDowntime, Approximate, _) -> {Approximate, cloudi_timestamp:convert(NativeDowntime, native, nanosecond)}; durations_sum([{T0Old, T1} | DurationList], NativeDowntime, Approximate, T) -> T0 = duration_t0(T0Old, T1, T), if T0 =:= undefined -> % only older entries remain and their presence % implies the result is not approximate durations_sum([], NativeDowntime, false, T); is_integer(T0) -> durations_sum(DurationList, NativeDowntime + (T1 - T0), Approximate, T) end. durations_sum_with_view([], NativeDowntime, Approximate, View, _, _, TNow, TPeriod, TimeOffset) -> {Approximate, cloudi_timestamp:convert(NativeDowntime, native, nanosecond), durations_view_to_string(View, TNow, TPeriod, TimeOffset)}; durations_sum_with_view([{T0Old, T1} | DurationList], NativeDowntime, Approximate, View, Tv, T, TNow, TPeriod, TimeOffset) -> T0 = duration_t0(T0Old, T1, T), if T0 =:= undefined -> % only older entries remain and their presence % implies the result is not approximate durations_sum_with_view([], NativeDowntime, false, View, Tv, T, TNow, TPeriod, TimeOffset); is_integer(T0) -> Tv0 = duration_t0(T0Old, T1, Tv), ViewNew = if Tv0 =:= undefined -> % do not put an old duration in the current view character View; is_integer(Tv0) -> durations_view_incr(durations_view_index(TPeriod, Tv0, TimeOffset), durations_view_index(TPeriod, T1, TimeOffset), View) end, durations_sum_with_view(DurationList, NativeDowntime + (T1 - T0), Approximate, ViewNew, Tv, T, TNow, TPeriod, TimeOffset) end. duration_t0(T0, _, T) when T0 >= T -> T0; duration_t0(_, T1, T) when T1 > T -> T; duration_t0(_, _, _) -> undefined. durations_view_incr(I, I, View) -> durations_view_incr(I, View); durations_view_incr(I0, I1, View) -> durations_view_incr((I0 rem tuple_size(View)) + 1, I1, durations_view_incr(I0, View)). durations_view_incr(I, View) -> C0 = element(I, View), CN = if C0 == $ -> $1; C0 == $9; C0 == $X -> $X; C0 >= $1, C0 =< $8 -> C0 + 1 end, erlang:setelement(I, View, CN). durations_view_index(year, T, TimeOffset) -> % 1/3rd of a month per character {{DateYYYY, DateMM, DateDD}, _} = cloudi_timestamp:datetime_utc(T + TimeOffset), MonthFraction = (DateDD - 1) / (calendar:last_day_of_the_month(DateYYYY, DateMM) / 3), 1 + (DateMM - 1) * 3 + trunc(MonthFraction); durations_view_index(month, T, TimeOffset) -> % 1 day per character {{_, _, DateDD}, _} = cloudi_timestamp:datetime_utc(T + TimeOffset), DateDD; durations_view_index(week, T, TimeOffset) -> % 4 hours per character {{DateYYYY, DateMM, DateDD}, {TimeHH, _, _}} = cloudi_timestamp:datetime_utc(T + TimeOffset), % DayOfWeek = 1 for Monday with week starting on Monday (ISO 8601) DayOfWeek = calendar:day_of_the_week(DateYYYY, DateMM, DateDD), 1 + (DayOfWeek - 1) * 6 + TimeHH div 4; durations_view_index(day, T, TimeOffset) -> % 30 minutes per character {_, {TimeHH, TimeMM, _}} = cloudi_timestamp:datetime_utc(T + TimeOffset), 1 + TimeHH * 2 + TimeMM div 30. durations_view_new(TNow, TPeriod, TimeOffset) -> % Size does not include the | character {Size, TCharacter} = if TPeriod =:= year -> {36, ?NATIVE_TIME_IN_MONTH div 3}; % 1/3rd of a month per character TPeriod =:= month -> {31, ?NATIVE_TIME_IN_DAY}; % 1 day per character TPeriod =:= week -> {42, ?NATIVE_TIME_IN_DAY div 6}; % 4 hours per character TPeriod =:= day -> {48, ?NATIVE_TIME_IN_DAY div 48} % 30 minutes per character end, % TStart is the beginning of the current character in the view TStart = TNow - abs(TNow + TimeOffset) rem TCharacter, {TStart - (Size - 1) * TCharacter, erlang:list_to_tuple(lists:duplicate(Size, $ ))}. durations_view_to_string(View, TNow, TPeriod, TimeOffset) -> I = durations_view_index(TPeriod, TNow, TimeOffset), cloudi_lists:insert(I + 1, $|, erlang:tuple_to_list(View)). availability_to_string(Availability, true) -> case availability_to_string(Availability) of ?AVAILABILITY_ZERO = Zero -> Zero; AvailabilityStr -> [$<, $ | AvailabilityStr] end; availability_to_string(Availability, false) -> availability_to_string(Availability). % avoid erlang:float_to_list precision problems % and keep the formatting efficient availability_to_string(Availability) when Availability < 0.25 -> ?AVAILABILITY_ZERO; availability_to_string(Availability) when Availability < 1 / 3 -> "25 %"; availability_to_string(Availability) when Availability < 0.5 -> "33.3 %"; availability_to_string(Availability) when Availability < 2 / 3 -> "50 %"; availability_to_string(Availability) when Availability < 0.75 -> "66.6 %"; availability_to_string(Availability) when Availability < 0.9 -> "75 %"; availability_to_string(Availability) when Availability < 0.99 -> if % 1 nine Availability < 0.91 -> "90 %"; Availability < 0.92 -> "91 %"; Availability < 0.93 -> "92 %"; Availability < 0.94 -> "93 %"; Availability < 0.95 -> "94 %"; Availability < 0.96 -> "95 %"; Availability < 0.97 -> "96 %"; Availability < 0.98 -> "97 %"; true -> "98 %" end; availability_to_string(Availability) when Availability < 0.999 -> if % 2 nines Availability < 0.991 -> "99.0 %"; Availability < 0.992 -> "99.1 %"; Availability < 0.993 -> "99.2 %"; Availability < 0.994 -> "99.3 %"; Availability < 0.995 -> "99.4 %"; Availability < 0.996 -> "99.5 %"; Availability < 0.997 -> "99.6 %"; Availability < 0.998 -> "99.7 %"; true -> "99.8 %" end; availability_to_string(Availability) when Availability < 0.9999 -> if % 3 nines Availability < 0.9991 -> "99.9 %"; Availability < 0.9992 -> "99.91 %"; Availability < 0.9993 -> "99.92 %"; Availability < 0.9994 -> "99.93 %"; Availability < 0.9995 -> "99.94 %"; Availability < 0.9996 -> "99.95 %"; Availability < 0.9997 -> "99.96 %"; Availability < 0.9998 -> "99.97 %"; true -> "99.98 %" end; availability_to_string(Availability) when Availability < 0.99999 -> if % 4 nines Availability < 0.99991 -> "99.99 %"; Availability < 0.99992 -> "99.991 %"; Availability < 0.99993 -> "99.992 %"; Availability < 0.99994 -> "99.993 %"; Availability < 0.99995 -> "99.994 %"; Availability < 0.99996 -> "99.995 %"; Availability < 0.99997 -> "99.996 %"; Availability < 0.99998 -> "99.997 %"; true -> "99.998 %" end; availability_to_string(Availability) when Availability < 0.999999 -> if % 5 nines Availability < 0.999991 -> "99.999 %"; Availability < 0.999992 -> "99.9991 %"; Availability < 0.999993 -> "99.9992 %"; Availability < 0.999994 -> "99.9993 %"; Availability < 0.999995 -> "99.9994 %"; Availability < 0.999996 -> "99.9995 %"; Availability < 0.999997 -> "99.9996 %"; Availability < 0.999998 -> "99.9997 %"; true -> "99.9998 %" end; availability_to_string(Availability) when Availability < 0.9999999 -> if % 6 nines Availability < 0.9999991 -> "99.9999 %"; Availability < 0.9999992 -> "99.99991 %"; Availability < 0.9999993 -> "99.99992 %"; Availability < 0.9999994 -> "99.99993 %"; Availability < 0.9999995 -> "99.99994 %"; Availability < 0.9999996 -> "99.99995 %"; Availability < 0.9999997 -> "99.99996 %"; Availability < 0.9999998 -> "99.99997 %"; true -> "99.99998 %" end; availability_to_string(Availability) when Availability < 0.99999999 -> if % 7 nines Availability < 0.99999991 -> "99.99999 %"; Availability < 0.99999992 -> "99.999991 %"; Availability < 0.99999993 -> "99.999992 %"; Availability < 0.99999994 -> "99.999993 %"; Availability < 0.99999995 -> "99.999994 %"; Availability < 0.99999996 -> "99.999995 %"; Availability < 0.99999997 -> "99.999996 %"; Availability < 0.99999998 -> "99.999997 %"; true -> "99.999998 %" end; availability_to_string(Availability) when Availability < 0.999999999 -> if % 8 nines Availability < 0.999999991 -> "99.999999 %"; Availability < 0.999999992 -> "99.9999991 %"; Availability < 0.999999993 -> "99.9999992 %"; Availability < 0.999999994 -> "99.9999993 %"; Availability < 0.999999995 -> "99.9999994 %"; Availability < 0.999999996 -> "99.9999995 %"; Availability < 0.999999997 -> "99.9999996 %"; Availability < 0.999999998 -> "99.9999997 %"; true -> "99.9999998 %" end; availability_to_string(Availability) -> if % 9 nines Availability < 1.0 -> "99.9999999 %"; true -> "100 %" end.