ar_timestamp.erl - Arweave Timestamp Cache Server
Overview
Purpose: Cached Arweave network timestamp with periodic refresh
Module: ar_timestamp
Pattern: Gen-server-like caching server
Refresh Interval: 15 seconds (15,000 ms)
This module implements a simple caching server that maintains the current Arweave network timestamp and refreshes it periodically. It avoids repeated network calls by caching the timestamp and serving it from memory.
Dependencies
- Erlang/OTP:
timer - Arweave:
hb_client(forarweave_timestamp/0) - Includes:
include/hb.hrl
Public Functions Overview
%% Server Management
-spec start() -> PID
when PID :: pid().
%% Timestamp Retrieval
-spec get() -> Timestamp
when Timestamp :: non_neg_integer().Architecture
Server Model
+---------------------------------------------+
| ar_timestamp (Registered Process) |
| |
| +------------+ +---------------+ |
| | Cache |<---------| Refresher | |
| | Process | | Process | |
| +-----+------+ +-------+-------+ |
| | | |
| | Serves cached | Updates |
| | timestamp | every |
| | | 15s |
+--------+-------------------------+----------+
| |
v v
Client Requests hb_client:arweave_timestamp()Process Communication
Client Cache Process Refresher Process
| | |
+--{get, self()}-------->| |
| | |
|<--{timestamp, TS}------+ |
| | |
| |<--{refresh, NewTS}----+
| | |
| | (every 15 seconds) |Public Functions
1. start/0
-spec start() -> PID
when
PID :: pid().Description: Start the timestamp cache server. If the server is already running, returns the existing PID. If not running or dead, spawns a new server.
Behavior:- Check if process is registered as
ar_timestamp - If not registered → spawn new server
- If registered but dead → spawn new server
- If registered and alive → return existing PID
-module(ar_timestamp_start_test).
-include_lib("eunit/include/eunit.hrl").
start_creates_process_test() ->
% Ensure clean state
case whereis(ar_timestamp) of
undefined -> ok;
OldPID ->
exit(OldPID, kill),
timer:sleep(10)
end,
PID = ar_timestamp:start(),
?assert(is_pid(PID)),
?assert(is_process_alive(PID)),
?assertEqual(PID, whereis(ar_timestamp)).
start_idempotent_test() ->
PID1 = ar_timestamp:start(),
PID2 = ar_timestamp:start(),
?assertEqual(PID1, PID2),
?assert(is_process_alive(PID1)).
start_after_kill_test() ->
PID1 = ar_timestamp:start(),
?assert(is_process_alive(PID1)),
% Kill the process - registration is automatically removed
exit(PID1, kill),
timer:sleep(10),
% Verify process is dead and unregistered
?assertEqual(false, is_process_alive(PID1)),
?assertEqual(undefined, whereis(ar_timestamp)),
% Start should spawn a new process
PID2 = ar_timestamp:start(),
?assert(is_process_alive(PID2)),
?assertNotEqual(PID1, PID2).2. get/0
-spec get() -> Timestamp
when
Timestamp :: non_neg_integer().Description: Get the current cached Arweave timestamp. Automatically starts the server if not running. Returns Unix timestamp in seconds.
Behavior:- Ensure server is started (calls
start/0) - Send
{get, self()}message to cache process - Wait for
{timestamp, Timestamp}response - Return timestamp
Blocking: This function blocks until it receives the timestamp response.
Test Code:-module(ar_timestamp_get_test).
-include_lib("eunit/include/eunit.hrl").
get_returns_value_test() ->
Result = ar_timestamp:get(),
% get/0 should return whatever hb_client:arweave_timestamp() returns
?assertNotEqual(undefined, Result).
get_starts_server_test() ->
% Ensure clean state
case whereis(ar_timestamp) of
undefined -> ok;
OldPID ->
exit(OldPID, kill),
timer:sleep(10)
end,
?assertEqual(undefined, whereis(ar_timestamp)),
_Result = ar_timestamp:get(),
?assertNotEqual(undefined, whereis(ar_timestamp)).
get_cached_value_test() ->
Result1 = ar_timestamp:get(),
Result2 = ar_timestamp:get(),
Result3 = ar_timestamp:get(),
% Within cache window, should return same cached value
?assertEqual(Result1, Result2),
?assertEqual(Result2, Result3).
get_concurrent_test() ->
% Multiple processes getting timestamp concurrently
Self = self(),
NumProcs = 10,
lists:foreach(
fun(_) ->
spawn(fun() ->
Result = ar_timestamp:get(),
Self ! {timestamp, Result}
end)
end,
lists:seq(1, NumProcs)
),
Results = [receive {timestamp, R} -> R end || _ <- lists:seq(1, NumProcs)],
% All should get a result
?assertEqual(NumProcs, length(Results)),
% All should be the same (from cache)
[First | Rest] = Results,
lists:foreach(fun(R) -> ?assertEqual(First, R) end, Rest).Internal Functions
spawn_server/0
-spec spawn_server() -> PID
when PID :: pid().Description: Spawn new cache and refresher processes. Registers cache process as ar_timestamp.
- Spawn cache process with initial timestamp from
hb_client:arweave_timestamp() - Spawn refresher process that updates cache every 15 seconds
- Register cache process as
ar_timestamp - Return cache process PID
cache/1
-spec cache(Current) -> no_return()
when Current :: non_neg_integer().Description: Main cache loop. Handles {get, Pid} and {refresh, New} messages.
{get, Pid}→ Send{timestamp, Current}to Pid, continue with same timestamp{refresh, New}→ Update cached timestamp to New, continue with new value
Tail Recursive: Yes, infinite loop with tail recursion
refresher/1
-spec refresher(TSServer) -> no_return()
when TSServer :: pid().Description: Periodic refresh loop. Updates cache every 15 seconds.
Behavior:- Sleep for 15 seconds (
?TIMEOUT) - Fetch new timestamp from
hb_client:arweave_timestamp() - Send
{refresh, TS}to cache process - Repeat (tail recursion)
Tail Recursive: Yes, infinite loop with tail recursion
Configuration
Refresh Interval
-define(TIMEOUT, 1000 * 15). % 15 seconds = 15,000 milliseconds- Balances freshness vs network load
- Arweave block time is ~2 minutes
- 15 seconds provides reasonably fresh timestamps
- Reduces network calls by ~8x compared to per-request fetching
Common Patterns
%% Simple usage - get current timestamp
Timestamp = ar_timestamp:get().
%% Ensure server is running (usually automatic)
ar_timestamp:start(),
TS = ar_timestamp:get().
%% Use timestamp in transaction
TX = #tx{
timestamp = ar_timestamp:get(),
data = <<"Transaction data">>
}.
%% Multiple reads (efficient - uses cache)
TS1 = ar_timestamp:get(),
% ... do work ...
TS2 = ar_timestamp:get(),
% TS1 and TS2 will be the same within 15-second window
%% Time-based operations
Now = ar_timestamp:get(),
ExpiryTime = Now + 3600, % 1 hour from now
IsExpired = ar_timestamp:get() > ExpiryTime.
%% Startup in application
-spec init([]) -> {ok, State}.
init([]) ->
ar_timestamp:start(), % Start timestamp server
{ok, #state{}}.Timestamp Format
Unix Timestamp
- Format: Seconds since Unix epoch (1970-01-01 00:00:00 UTC)
- Type: Non-negative integer
- Example:
1699999999(2023-11-14)
Precision
- Granularity: 1 second
- Refresh: Every 15 seconds
- Staleness: Maximum 15 seconds old
Process Lifecycle
Startup Sequence
1. start() called
├─> Check if registered
│ ├─> Not registered → spawn_server()
│ └─> Registered
│ ├─> Process alive → return PID
│ └─> Process dead → spawn_server()
│
2. spawn_server()
├─> Spawn cache process
│ └─> Initial fetch: hb_client:arweave_timestamp()
├─> Spawn refresher process
│ └─> Loop: sleep 15s → fetch → send refresh
└─> Register cache as 'ar_timestamp'Message Flow
%% Client request
Client:
PID = start(),
PID ! {get, self()},
receive {timestamp, TS} -> TS end.
%% Cache response
Cache:
receive
{get, Pid} -> Pid ! {timestamp, Current}
end.
%% Refresher update
Refresher:
timer:sleep(15000),
TS = hb_client:arweave_timestamp(),
CachePID ! {refresh, TS}.Advantages
Performance
- Cached Reads: O(1) message passing, no network call
- Reduced Network Load: 15-second intervals vs per-request
- Low Latency: Memory access vs HTTP request
Reliability
- Automatic Recovery: Restarts if process dies
- Singleton Pattern: Single registered process
- No State Loss: Refresher continues independently
Simplicity
- No GenServer: Simple message-passing pattern
- Minimal Code: ~60 lines total
- Easy Testing: Direct process communication
Limitations
Staleness
- Maximum 15 seconds behind network time
- Not suitable for sub-second precision requirements
Single Point of Failure
- If refresher dies, timestamp becomes stale
- No supervision tree (relies on manual restart)
Network Dependency
- Requires
hb_client:arweave_timestamp()to work - Initial startup blocked on network call
- Refresh failures not handled (uses old timestamp)
Comparison with Alternatives
vs. Per-Request Fetching
% Direct (slow, accurate)
TS = hb_client:arweave_timestamp().
% Cached (fast, slightly stale)
TS = ar_timestamp:get().vs. System Time
% System time (instant, may be wrong)
TS = erlang:system_time(second).
% Arweave time (cached network time, authoritative)
TS = ar_timestamp:get().vs. GenServer
% GenServer (more features, more code)
{ok, TS} = gen_server:call(ar_timestamp, get_timestamp).
% Simple server (minimal, sufficient)
TS = ar_timestamp:get().Testing Strategies
Unit Tests
% Test server lifecycle
test_start_stop() ->
PID = ar_timestamp:start(),
?assert(is_process_alive(PID)).
% Test caching
test_cache_consistency() ->
TS1 = ar_timestamp:get(),
TS2 = ar_timestamp:get(),
?assertEqual(TS1, TS2). % Within 15s windowIntegration Tests
% Test refresh mechanism
test_refresh() ->
TS1 = ar_timestamp:get(),
timer:sleep(16000), % Wait for refresh
TS2 = ar_timestamp:get(),
?assert(TS2 >= TS1).
% Test concurrent access
test_concurrent() ->
Pids = [spawn(fun() -> ar_timestamp:get() end) || _ <- lists:seq(1, 100)],
timer:sleep(1000),
?assert(true). % Should not crashMocking
% Mock hb_client for deterministic testing
meck:new(hb_client, [passthrough]),
meck:expect(hb_client, arweave_timestamp, fun() -> 1234567890 end),
TS = ar_timestamp:get(),
?assertEqual(1234567890, TS),
meck:unload(hb_client).Error Handling
Current Behavior
- No explicit error handling: Assumes
hb_client:arweave_timestamp()succeeds - Process crashes: If initial fetch fails, cache process crashes
- Stale data: If refresh fails, continues serving old timestamp
Recommended Improvements
% Graceful degradation
cache(Current) ->
receive
{get, Pid} -> Pid ! {timestamp, Current};
{refresh, {ok, New}} -> cache(New);
{refresh, {error, _}} -> cache(Current) % Keep old on error
end.
% Retry logic
refresher(TSServer) ->
timer:sleep(?TIMEOUT),
case catch_arweave_timestamp() of
{ok, TS} -> TSServer ! {refresh, {ok, TS}};
{error, E} -> TSServer ! {refresh, {error, E}}
end,
refresher(TSServer).Performance Characteristics
Time Complexity
start/0: O(1) - process lookupget/0: O(1) - message passing- Cache hit: O(1) - memory access
- Refresh: O(1) - periodic, amortized
Space Complexity
- O(1) - Single integer cached
- Minimal process overhead (~2KB per process)
- Two processes total (cache + refresher)
Concurrency
- Thread-safe via message passing
- Multiple concurrent readers supported
- Single writer (refresher)
References
- Arweave Network Time - Block timestamp synchronization
- Erlang Processes - Lightweight process model
- Message Passing - Actor model communication
- hb_client - HyperBEAM client module
Notes
- Singleton: Only one server runs per Erlang VM (registered as
ar_timestamp) - Auto-Start:
get/0automatically starts server if needed - No Supervision: Process is not supervised; manual recovery only
- Staleness: Up to 15 seconds behind network time
- Blocking:
get/0blocks until response received - Concurrent Safe: Multiple processes can call
get/0safely - Refresh Interval: Fixed at 15 seconds (compile-time constant)
- Initial Fetch: First
start()call makes network request - No Timeout:
get/0has no timeout (could hang if server dead) - Event Logging: Includes
?eventmacros for debugging/monitoring