Shared\Counter

OxPHP\Shared\Counter est un entier signé atomique de 64 bits à l'échelle du processus, spécialisé dans l'accumulation : compter des événements, additionner des deltas, tenir des totaux sur fenêtre glissante. Chaque opération est sans verrou ; deux workers qui incrémentent simultanément ne perdent jamais un tick.

Pour un état atomique arbitraire qui doit synchroniser d'autres zones mémoire (machines à états, estampilles de version, seqlocks, masques de bitflags), utilisez plutôt Shared\Atomic.

Vue d'ensemble

  • int64 atomique. Plage −9_223_372_036_854_775_808 … 9_223_372_036_854_775_807. Le dépassement boucle.
  • Sans verrou. add se compile en un unique fetch_add.
  • Toujours Relaxed. Les opérations sont atomiques (aucun tick perdu, aucune lecture déchirée) mais n'établissent aucun happens-before avec les autres zones mémoire. Un Counter est une statistique, pas un point de synchronisation — si vous avez besoin d'ordonnancement, utilisez Shared\Atomic.
  • Partageable. Les instances peuvent être stockées dans Shared\Map / Shared\Channel et transmises aux Fibers via des captures use.

Référence de l'API

php
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; }
Méthode Retourne Cas d'usage
get valeur courante Lecture sans mutation.
set valeur précédente Échange atomique ; set(0) est la lecture-et-remise-à-zéro de fin de fenêtre.
add nouvelle valeur add() incrémente de 1, add(-1) décrémente, tout autre delta sinon.
compareAndSet bool Compteurs bornés / saturants (plafond, plancher) via une boucle CAS.
id id de registre Journalisation, traçage, corrélation /__ox_shared/entry?id=….

Exemples

Compteur de requêtes par worker

php
<?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"; });

Bascule par fenêtre

php
<?php $hits = new OxPHP\Shared\Counter(); // Every N minutes in your cron/worker loop: $prev = $hits->set(0); // atomically reads and zeroes logWindowMetric($prev);

Compteur borné (boucle CAS)

php
<?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));

Accumulation groupée

php
<?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));

Sémantique et pièges

set() retourne la valeur précédente, puis stocke — atomiquement. set(0) est le motif capture-et-remise-à-zéro (LongAdder::sumThenReset) ; set($n) amorce un nouveau point de départ.

Ordonnancement Relaxed

Chaque opération est atomique, mais un Counter ne publie pas d'autres zones mémoire. Si un lecteur doit observer des données qu'un écrivain a écrites avant d'incrémenter l'entier, cela relève de la synchronisation — utilisez Shared\Atomic avec Ordering::Release/Acquire.

compareAndSet est Relaxed/Relaxed et ne prend aucun argument d'ordonnancement. Il est correct pour des décisions prises sur la valeur propre du compteur (plafond, plancher, réservation par valeur). Un CAS qui publie un autre état relève de Shared\Atomic.

Le dépassement boucle

Additionner au-delà de INT_MAX boucle vers INT_MIN. Pour des compteurs qui tournent pendant des mois à plusieurs milliers par seconde, gardez la valeur dans la plage des dizaines de milliers de milliards ou remettez-la à zéro périodiquement.

Pas de valeurs fractionnaires. Vous comptez des octets pour des moyennes à précision flottante ? Suivez le numérateur (Counter) et le dénominateur (Counter) séparément et divisez au moment de la lecture.

Exceptions

Exception Levée par
StaleHandleException Toute méthode sur un handle dont l'entrée de registre a été évincée.
UninitializedException id() sur un wrapper qui n'a pas terminé son __construct.

Les Counters ne lèvent jamais d'exception en cas de dépassement ou de valeurs extrêmes — ils bouclent.

Observabilité

Voir Observabilité de l'état partagé pour la visite complète. Références rapides :

  • GET /__ox_shared/entry?id=N expose { value, type: "Counter" }.
  • La jauge Prometheus oxphp_shared_counter_value{counter_id="…"} suit la valeur courante.
  • Les compteurs à l'échelle du registre (oxphp_shared_operations_total, oxphp_shared_objects_total) couvrent Counter via le label type="Counter".

Quand ne pas l'utiliser

  • Flottants ou décimaux. Utilisez une paire de Counters (numérateur / dénominateur) ou un Shared\Mutex<array{total_cents: int, count: int}>.
  • Événements non numériques nécessitant un contexte riche. Si vous avez besoin de {count, last_actor, last_reason} couplé à une seule clé, tournez-vous vers Shared\Map ou Shared\Mutex.
  • Totaux inter-hôtes. Un Counter est strictement in-process. Pour une agrégation multi-hôtes, utilisez un pipeline de métriques (Prometheus + rate(), ou un INCR Redis centralisé).
  • Durabilité. L'état d'un Counter s'évapore à l'arrêt du serveur. Persistez des snapshots ailleurs si le total doit survivre aux redémarrages.

Voir aussi

  • État partagé — vue d'ensemble et motifs de migration.
  • Shared\Atomic — int64 atomique générique avec CAS, swap et contrôle complet de l'ordonnancement mémoire.
  • Shared\Map — quand les compteurs sont indexés par clé (Map<string, Counter>).
  • Shared\Flag — quand la valeur est simplement activée/désactivée.
  • Shared\Mutex — quand un compteur doit être mis à jour en lockstep avec d'autres champs.