Skip to main content

src/barrel_p2p_hlc.erl

%%% -*- erlang -*-
%%% Copyright (c) 2026 Benoit Chesneau
%%% SPDX-License-Identifier: Apache-2.0
%%%
-module(barrel_p2p_hlc).
-behaviour(gen_server).

%% HLC (Hybrid Logical Clock) wrapper for the hlc library.
%% Provides causal ordering with wall-clock approximation.

-include_lib("hlc/include/hlc.hrl").

%% API
-export([start_link/0]).
-export([now/0, update/1, compare/2]).
-export([to_binary/1, from_binary/1]).
-export([wall_time/1, logical/1]).
-export([pack/1]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, terminate/2]).

-define(SERVER, ?MODULE).

-type timestamp() :: #timestamp{}.
-export_type([timestamp/0]).

%%====================================================================
%% API
%%====================================================================

start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%% Get current HLC timestamp (advances clock)
-spec now() -> timestamp().
now() ->
    gen_server:call(?SERVER, now).

%% Update local clock with remote timestamp (for receiving messages)
-spec update(timestamp()) -> timestamp().
update(Remote) ->
    gen_server:call(?SERVER, {update, Remote}).

%% Compare two HLC timestamps
%% Returns: lt | eq | gt
-spec compare(timestamp(), timestamp()) -> lt | eq | gt.
compare(
    #timestamp{wall_time = WA, logical = LA},
    #timestamp{wall_time = WB, logical = LB}
) ->
    if
        WA =:= WB, LA =:= LB -> eq;
        WA < WB -> lt;
        WA > WB -> gt;
        LA < LB -> lt;
        true -> gt
    end.

%% Serialize HLC timestamp to binary
-spec to_binary(timestamp()) -> binary().
to_binary(#timestamp{wall_time = Wall, logical = Logical}) ->
    <<Wall:64/big, Logical:32/big>>.

%% Deserialize binary to HLC timestamp
-spec from_binary(binary()) -> timestamp().
from_binary(<<Wall:64/big, Logical:32/big>>) ->
    #timestamp{wall_time = Wall, logical = Logical}.

%% Extract wall time from timestamp
-spec wall_time(timestamp()) -> non_neg_integer().
wall_time(#timestamp{wall_time = Wall}) -> Wall.

%% Extract logical clock from timestamp
-spec logical(timestamp()) -> non_neg_integer().
logical(#timestamp{logical = Logical}) -> Logical.

%% Pack a timestamp into a single comparable, monotonic integer. Used
%% as an opaque fencing token / version id that external systems compare
%% with `>'. Higher wall time dominates; logical breaks ties within a
%% wall tick.
-spec pack(timestamp()) -> non_neg_integer().
pack(#timestamp{wall_time = Wall, logical = Logical}) ->
    (Wall bsl 32) bor (Logical band 16#FFFFFFFF).

%%====================================================================
%% gen_server callbacks
%%====================================================================

init([]) ->
    {ok, Pid} = hlc:start_link(),
    {ok, #{clock => Pid}}.

handle_call(now, _From, #{clock := Clock} = State) ->
    {reply, hlc:now(Clock), State};
handle_call({update, Remote}, _From, #{clock := Clock} = State) ->
    case hlc:update(Clock, Remote) of
        {ok, TS} -> {reply, TS, State};
        {timeahead, TS} -> {reply, TS, State}
    end;
handle_call(_Request, _From, State) ->
    {reply, {error, unknown_request}, State}.

handle_cast(_Msg, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.