\Drupal\strata\Storage ProviderStats

What a store cost over one window: requests, bytes, failures and latency.

Object storage is not billed by volume alone. Cloudflare R2 charges $0.015 per GB-month of storage, $4.50 per million class-A operations and $0.36 per million class-B operations; AWS S3 charges $0.023, $5.00 and $0.40 for the same three lines. A capture that writes a million small frames costs more in requests than in storage, and an accounting that only summed bytes would never show it.

Class A is the billed-write set: put, delete and list. Class B is the billed-read set: get and head. Every attempt is counted, failures included, because an endpoint that refused a request still handled it and still billed for it. ProviderStats::failures() is what separates the two.

Mutable, and scoped to one request or one flush; this is an accumulator, not a record. ProviderStats::merge() folds one window into another and ProviderStats::reset() starts a fresh one.

Summary

Methods
Properties
Constants
fromOperations
record
merge
reset
classA
classB
bytes
operations
failures
seconds
byOperation
secondsFor
slowestOf
latency
jsonSerialize
No public properties found
OPERATIONS
CLASS_A
CLASS_B
LATENCY_SAMPLES
No protected methods found
No protected properties found
No protected constants found
sample
countOf
assertOperation
counts
volumes
failed
samples
seconds
durations
peaks
No private constants found

Constant

OPERATIONS

OPERATIONS = ['put', 'delete', 'list', 'get', 'head']

Every operation that is counted, class A first.

CLASS_A

CLASS_A = ['put', 'delete', 'list']

The billed-write operations.

CLASS_B

CLASS_B = ['get', 'head']

The billed-read operations.

LATENCY_SAMPLES

LATENCY_SAMPLES = 1000

Most latency samples held per operation.

A flush of a large site issues far more requests than a percentile needs to describe, and one float per request would let the accumulator grow for the length of the run. The newest samples are kept and the oldest dropped, so the figures describe what the store is doing now rather than what it did during the first thousand calls.

Properties

$counts

$counts : array

Attempts per operation.

Type

array<string, int> —

$volumes

$volumes : array

Bytes moved per operation.

Type

array<string, int> —

$failed

$failed : array

Failed attempts per operation.

Type

array<string, int> —

$samples

$samples : array>

The newest LATENCY_SAMPLES durations per operation, in arrival order.

Type

array<string, array<int, float>> —

$seconds

$seconds : float

Seconds spent across every operation, untruncated.

Type

float —

$durations

$durations : array

Seconds spent per operation, untruncated.

Held separately from the latency samples because the samples are capped: a run that issues more than LATENCY_SAMPLES puts of its own would otherwise persist a total describing only the last thousand.

Type

array<string, float> —

$peaks

$peaks : array

The slowest single attempt per operation, untruncated.

Type

array<string, float> —

Methods

fromOperations()

fromOperations(array  $byOperation) : self

Rebuilds a window from figures already accumulated per operation.

The inverse of ProviderStats::byOperation(), for a caller reading a window back out of the stat table rather than watching it happen. Replaying ProviderStats::record() once per request would be correct and costs a call per request in the window, which for a month of traffic is millions of calls to reproduce three totals.

Latency samples are not reproduced, because the table does not hold them. ProviderStats::p95() on a rebuilt window therefore reports nothing, while the counts, the volumes and the failures are exact.

Parameters

array $byOperation

Operation keyed to its figures, as ProviderStatStore::byOperation() returns them.

Throws

\InvalidArgumentException

When an operation is not a billed verb.

Returns

self —

The window.

record()

record(string  $operation, int  $bytes, float  $seconds, bool  $failed = false) : void

Records one attempt against the store.

Parameters

string $operation

One of ProviderStats::OPERATIONS.

int $bytes

Bytes that crossed the wire. Zero for an operation that moves no body, such as a head or a delete.

float $seconds

Wall-clock time the attempt took.

bool $failed

TRUE when the attempt raised. It is still counted, because the request was still made.

Throws

\InvalidArgumentException

When $operation is not a billed verb, or $bytes or $seconds is negative. Each of those is a caller bug that would otherwise show up as a budget reading nobody can explain.

Returns

void —

merge()

merge(\Drupal\strata\Storage\ProviderStats  $other) : void

Folds another window into this one.

Latency samples are appended, so the other window's observations count as the newer ones and the cap is applied to the result.

Parameters

\Drupal\strata\Storage\ProviderStats $other

The window to fold in. It is left unchanged.

Returns

void —

reset()

reset() : void

Forgets everything and starts a new window.

Returns

void —

classA()

classA() : int

Billed writes: put, delete and list.

Returns

int —

How many class-A requests were made.

classB()

classB() : int

Billed reads: get and head.

Returns

int —

How many class-B requests were made.

bytes()

bytes() : int

Bytes that crossed the wire.

This is transfer volume, not the size of the store. What a bucket holds is a level the store reports; this is a rate the engine produced.

Returns

int —

Total bytes over every operation.

operations()

operations() : int

Requests made, of either class.

Returns

int —

Total attempts, failures included.

failures()

failures() : int

Requests that raised.

Returns

int —

How many attempts failed.

seconds()

seconds() : float

Wall-clock time spent waiting on the store.

Counted over every attempt, so it is not affected by the latency sample cap.

Returns

float —

Seconds.

byOperation()

byOperation() : array<string,array{count: int, bytes: int, failures: int}>

Per-operation attempts, bytes and failures.

Operations that were never attempted are left out, so a table rendered from this shows the verbs the run actually used. Order follows ProviderStats::OPERATIONS.

Returns

array

Keyed by operation.

secondsFor()

secondsFor(string  $operation) : float

Wall-clock time spent on one operation.

Counted over every attempt, so it is the figure to persist. ProviderStats::latency() reports the retained sample instead and can describe less than the whole run.

Parameters

string $operation

One of ProviderStats::OPERATIONS.

Throws

\InvalidArgumentException

When $operation is not a billed verb.

Returns

float —

Seconds, or 0.0 when the operation was never attempted.

slowestOf()

slowestOf(string  $operation) : float

The slowest single attempt at one operation.

Taken over every attempt rather than the retained sample, so a spike that happened early in a long run is still reported.

Parameters

string $operation

One of ProviderStats::OPERATIONS.

Throws

\InvalidArgumentException

When $operation is not a billed verb.

Returns

float —

Seconds, or 0.0 when the operation was never attempted.

latency()

latency(string  $operation) : array{count: int, total: float, min: float, max: float, mean: float, p95: float}

Latency figures for one operation.

Every figure describes the retained sample, which is the newest LATENCY_SAMPLES durations for that operation. Below the cap that is every attempt; above it, count and total describe the retained window rather than the whole run, and ProviderStats::byOperation() and ProviderStats::seconds() carry the untruncated figures.

The percentile is nearest-rank: the smallest sample at or above the 95th position of the sorted window, with no interpolation between neighbours.

Parameters

string $operation

One of ProviderStats::OPERATIONS.

Throws

\InvalidArgumentException

When $operation is not a billed verb.

Returns

array{count: int, total: float, min: float, max: float, mean: float, p95: float} —

All zeroes when the operation has not been attempted.

jsonSerialize()

jsonSerialize() : array<string,mixed>

{@inheritdoc}

Returns

array

The window as a plain array for a report table or a JSON response.

sample()

sample(string  $operation, float  $seconds) : void

Adds one duration to an operation's window, dropping the oldest once it is full.

Parameters

string $operation

The operation the duration belongs to.

float $seconds

The duration.

Returns

void —

countOf()

countOf(list  $operations) : int

Sums the attempt counts of a set of operations.

Parameters

list $operations

The operations to add up.

Returns

int —

Their total attempt count.

assertOperation()

assertOperation(string  $operation) : void

Guards an operation argument.

Parameters

string $operation

The candidate verb.

Throws

\InvalidArgumentException

When it is not one of ProviderStats::OPERATIONS.

Returns

void —