%%%------------------------------------------------------------------- %%% @doc Hybrid Logical Clock utilities for barrel_docdb %%% %%% HLC timestamps are used for ordering changes across distributed nodes. %%% They combine physical wall clock time with a logical counter to ensure %%% causality while maintaining a close relationship to real time. %%% %%% Format: {WallTime, Logical} where WallTime is in milliseconds and %%% Logical is a counter that increments when events happen in the %%% physical clock's future. %%% @end %%%------------------------------------------------------------------- -module(barrel_hlc). -compile({no_auto_import, [now/0]}). -include_lib("hlc/include/hlc.hrl"). %% Global HLC operations (use the node-global clock) -export([ now/0, update/1, timestamp/0, get_hlc/0, sync_hlc/1, new_hlc/0, maybe_sync_from_header/1 ]). %% Instance-based operations (for testing or custom clocks) -export([ start_link/0, start_link/1, stop/1, now/1, update/2, timestamp/1 ]). %% Encoding/Decoding -export([ encode/1, decode/1, to_binary/1, from_binary/1, to_string/1, from_string/1 ]). %% Comparison -export([ compare/2, less/2, equal/2 ]). %% Constants -export([ min/0, max/0 ]). %% Accessor functions -export([ wall_time/1 ]). %% Types -export_type([ clock/0, timestamp/0 ]). %%==================================================================== %% Types %%==================================================================== -type clock() :: hlc:clock(). %% HLC clock process reference. -type timestamp() :: #timestamp{}. %% HLC timestamp with wall_time and logical components. %% Global clock name (started by barrel_docdb_sup) -define(GLOBAL_CLOCK, barrel_hlc_clock). %%==================================================================== %% Global HLC Operations %%==================================================================== %% These functions operate on the node-global HLC clock that is started %% by the barrel_docdb supervisor. Use these for distributed ordering. %% @doc Get the current global HLC timestamp without advancing the clock. -spec get_hlc() -> timestamp(). get_hlc() -> timestamp(). %% @doc Synchronize with a remote HLC timestamp. %% Call this when receiving data from another node to maintain causality. %% Returns {ok, NewTimestamp} or {error, clock_skew}. -spec sync_hlc(timestamp()) -> {ok, timestamp()} | {error, clock_skew}. sync_hlc(RemoteTS) -> update(RemoteTS). %% @doc Generate a new global HLC timestamp, advancing the clock. %% Use this when generating a new event that needs to be ordered. -spec new_hlc() -> timestamp(). new_hlc() -> now(). %% @doc Sync HLC from a base64-encoded HTTP header value. %% Silently ignores undefined or invalid values. -spec maybe_sync_from_header(binary() | undefined) -> ok. maybe_sync_from_header(undefined) -> ok; maybe_sync_from_header(HlcBase64) -> barrel_lib:safe(fun() -> HlcBin = base64:decode(HlcBase64), Hlc = decode(HlcBin), _ = sync_hlc(Hlc), ok end, ok). %% @doc Generate a new timestamp for a local event using global clock. -spec now() -> timestamp(). now() -> hlc:now(?GLOBAL_CLOCK). %% @doc Update the global clock with a remote timestamp. %% Rejects non-timestamp input at the API boundary so a misuse cannot %% crash the global `barrel_hlc_clock' gen_server. -spec update(timestamp()) -> {ok, timestamp()} | {error, clock_skew}. update(#timestamp{} = RemoteTS) -> case hlc:update(?GLOBAL_CLOCK, RemoteTS) of {ok, NewTS} -> {ok, NewTS}; {timeahead, _} -> {error, clock_skew} end. %% @doc Get the current global timestamp without advancing the clock. -spec timestamp() -> timestamp(). timestamp() -> hlc:timestamp(?GLOBAL_CLOCK). %%==================================================================== %% Instance-based Clock Management %%==================================================================== %% These functions are for testing or when you need a custom clock %% instance separate from the global node clock. %% @doc Start a new HLC clock with default settings. %% Uses physical system clock and no offset limit. -spec start_link() -> {ok, clock()}. start_link() -> hlc:start_link(). %% @doc Start a new HLC clock with maximum offset limit. %% MaxOffset is the maximum allowed clock skew in milliseconds. %% A value of 0 disables offset checking. -spec start_link(MaxOffset :: non_neg_integer()) -> {ok, clock()}. start_link(MaxOffset) -> hlc:start_link(fun hlc:physical_clock/0, MaxOffset). %% @doc Stop the HLC clock. -spec stop(clock()) -> ok | {error, term()}. stop(Clock) -> hlc:stop(Clock). %% @doc Generate a new timestamp using a specific clock instance. -spec now(clock()) -> timestamp(). now(Clock) -> hlc:now(Clock). %% @doc Update a specific clock with a remote timestamp. -spec update(clock(), timestamp()) -> {ok, timestamp()} | {error, clock_skew}. update(Clock, RemoteTS) -> case hlc:update(Clock, RemoteTS) of {ok, NewTS} -> {ok, NewTS}; {timeahead, _} -> {error, clock_skew} end. %% @doc Get the current timestamp from a specific clock without advancing it. -spec timestamp(clock()) -> timestamp(). timestamp(Clock) -> hlc:timestamp(Clock). %%==================================================================== %% Encoding/Decoding %%==================================================================== %% @doc Encode timestamp to binary for storage. %% Uses big-endian encoding to maintain lexicographic sort order. %% Format: 12-byte binary with WallTime (64-bit) and Logical (32-bit). -spec encode(timestamp()) -> binary(). encode(#timestamp{wall_time = WallTime, logical = Logical}) -> <>. %% @doc Decode binary to timestamp. -spec decode(binary()) -> timestamp(). decode(<>) -> #timestamp{wall_time = WallTime, logical = Logical}; decode(_) -> erlang:error(badarg). %% @doc Alias for encode/1 for consistency with other modules. -spec to_binary(timestamp()) -> binary(). to_binary(TS) -> encode(TS). %% @doc Alias for decode/1 for consistency with other modules. -spec from_binary(binary()) -> timestamp(). from_binary(Bin) -> decode(Bin). %% @doc Convert timestamp to human-readable string. %% Format: "WallTime:Logical" (e.g., "1609459200000:42") -spec to_string(timestamp()) -> binary(). to_string(#timestamp{wall_time = WallTime, logical = Logical}) -> WallBin = integer_to_binary(WallTime), LogicalBin = integer_to_binary(Logical), <>. %% @doc Parse timestamp from string. -spec from_string(binary()) -> timestamp() | {error, invalid_timestamp}. from_string(Bin) when is_binary(Bin) -> case binary:split(Bin, <<":">>) of [WallBin, LogicalBin] -> barrel_lib:safe(fun() -> WallTime = binary_to_integer(WallBin), Logical = binary_to_integer(LogicalBin), #timestamp{wall_time = WallTime, logical = Logical} end, {error, invalid_timestamp}); _ -> {error, invalid_timestamp} end; from_string(_) -> {error, invalid_timestamp}. %%==================================================================== %% Comparison %%==================================================================== %% @doc Compare two timestamps. %% Returns `lt' if TS1 is before TS2, `eq' if equal, `gt' if TS1 is after TS2. -spec compare(timestamp(), timestamp()) -> lt | eq | gt. compare(#timestamp{wall_time = W1, logical = L1}, #timestamp{wall_time = W2, logical = L2}) -> if W1 < W2 -> lt; W1 > W2 -> gt; L1 < L2 -> lt; L1 > L2 -> gt; true -> eq end. %% @doc Check if TS1 is less than TS2. -spec less(timestamp(), timestamp()) -> boolean(). less(TS1, TS2) -> hlc:less(TS1, TS2). %% @doc Check if two timestamps are equal. -spec equal(timestamp(), timestamp()) -> boolean(). equal(TS1, TS2) -> hlc:equal(TS1, TS2). %%==================================================================== %% Constants %%==================================================================== %% @doc Minimum timestamp value (for range scans). -spec min() -> timestamp(). min() -> #timestamp{wall_time = 0, logical = 0}. %% @doc Maximum timestamp value (for range scans). -spec max() -> timestamp(). max() -> %% Use maximum values that fit in our encoding format #timestamp{wall_time = 16#FFFFFFFFFFFFFFFF, logical = 16#FFFFFFFF}. %%==================================================================== %% Accessor Functions %%==================================================================== %% @doc Extract wall_time (milliseconds since epoch) from a timestamp. -spec wall_time(timestamp()) -> non_neg_integer(). wall_time(#timestamp{wall_time = WallTime}) -> WallTime.