Shared\Counter
OxPHP\Shared\Counter is a process-wide atomic 64-bit signed integer specialised for accumulation: counting events, summing deltas, rolling window totals. Every operation is lock-free; two workers adding concurrently never lose a tick.
For arbitrary atomic state that must synchronise other memory (state machines, version stamps, seqlocks, bitflag masks), use Shared\Atomic instead.
Overview
- The value is an atomic int64 with the range
−9_223_372_036_854_775_808 … 9_223_372_036_854_775_807, and overflow wraps. addis lock-free and compiles to a singlefetch_add.- Ordering is always Relaxed. Operations are atomic (no lost ticks, no torn reads) but establish no happens-before with other memory. A Counter is statistics, not a synchronisation point; if you need ordering, use
Shared\Atomic. - Instances are shareable: they can be stored inside
Shared\Map/Shared\Channeland handed to fibers viausecaptures.
API reference
namespace OxPHP\Shared;
final class Counter implements Shareable
{
public function __construct(int $initial = 0);
public function get(): int; // current
public function set(int $value): int; // returns previous; set(0) = window reset
public function add(int $delta = 1): int; // returns new; add()=+1, add(-1)=decrement
public function compareAndSet(int $expect, int $new): bool;
public function id(): int;
}| Method | Returns | Use case |
|---|---|---|
get |
current | Read without mutation. |
set |
previous | Atomic exchange; set(0) is the end-of-window read-and-zero. |
add |
new | add() increments by 1, add(-1) decrements, any delta otherwise. |
compareAndSet |
bool | Bounded / saturating counters (cap, floor) via a CAS loop. |
id |
registry id | Logging, tracing, /__ox_shared/entry?id=… correlation. |
Examples
Per-worker request counter
<?php
$requests = new OxPHP\Shared\Counter();
oxphp_worker(function () use ($requests) {
$count = $requests->add(); // +1, returns the new total
header("X-Request-Count: {$count}");
echo "ok";
});Windowed rollover
<?php
$hits = new OxPHP\Shared\Counter();
// Every N minutes in your cron/worker loop:
$prev = $hits->set(0); // atomically reads and zeroes
logWindowMetric($prev);Bounded counter (CAS loop)
<?php
$slots = new OxPHP\Shared\Counter();
$cap = 100;
// Claim a slot only while under the cap.
do {
$cur = $slots->get();
if ($cur >= $cap) {
// full — reject
break;
}
} while (!$slots->compareAndSet($cur, $cur + 1));Bulk accumulation
<?php
$bytes = new OxPHP\Shared\Counter();
// Sum a batch in PHP, then one atomic add (one FFI call).
$deltas = array_map(fn ($req) => strlen($req['body']), $batch);
$newTotal = $bytes->add(array_sum($deltas));Semantics & gotchas
set() returns the previous value, then stores, as one atomic step. set(0) is the snapshot-and-zero (LongAdder::sumThenReset) pattern; set($n) seeds any new starting point.
Each operation is atomic, but a Counter does not publish other memory. If a reader must observe data a writer wrote before bumping the integer, that is synchronisation, so use Shared\Atomic with Ordering::Release/Acquire.
compareAndSet is Relaxed/Relaxed and takes no ordering arguments. It is correct for decisions made on the counter's own value (cap, floor, claim-by-value). CAS that publishes other state belongs to Shared\Atomic.
Adding past INT_MAX loops to INT_MIN. For counters running for months at thousands-per-second, keep the value in tens-of-trillions range or reset periodically.
A Counter holds no fractional values. If you count bytes for float-precision averages, track numerator (Counter) and denominator (Counter) separately and divide at read time.
Exceptions
| Exception | Raised by |
|---|---|
StaleHandleException |
Any method on a handle whose registry entry was evicted. |
UninitializedException |
id() on a wrapper that has not finished __construct. |
Counters never throw on overflow or extreme values. They wrap.
Observability
See Shared Observability for the full tour. Quick references:
GET /__ox_shared/entry?id=Nexposes{ value, type: "Counter" }.- Prometheus
oxphp_shared_counter_value{counter_id="…"}gauge tracks the current value. - Registry-wide counters (
oxphp_shared_operations_total,oxphp_shared_objects_total) cover Counter via thetype="Counter"label.
When not to use
- For floats or decimals, use a pair of Counters (numerator / denominator) or a
Shared\Mutex<array{total_cents: int, count: int}>. - Non-numeric events that need rich context belong elsewhere. If you need
{count, last_actor, last_reason}coupled to one key, reach forShared\MaporShared\Mutex. - A Counter is in-process only, so it cannot hold cross-host totals. For multi-host aggregation use a metric pipeline (Prometheus +
rate(), or a central RedisINCR). - Counter state evaporates at server stop, so it offers no durability. Persist snapshots elsewhere if the total must survive restarts.
Prompts for your agent
These prompts assume an agent with the OxPHP documentation MCP server connected. The agent reads the pages it needs itself.
Use an OxPHP Shared Counter in my code instead of the APCu increment or static property I use today to tally requests, hits, or per-tenant usage. Read the `shared-state/shared-counter` page through the OxPHP documentation MCP server.
Find where this repository counts that way and show me the Counter replacement, including the `Registry::counter` binding that keeps it one total across workers.Related
- Shared State: overview and migration patterns.
- Shared\Atomic: generic atomic int64 with CAS, swap, and full memory-ordering control.
- Shared\Map: when counts are keyed (
Map<string, Counter>). - Shared\Flag: when the value is just on/off.
- Shared\Mutex: when a counter must update in lockstep with other fields.