\Drupal\strata\Budget BudgetGuard

Turns a window of store traffic into a monthly bill, and the bill into a rung.

A backup that quietly grows into a four-figure invoice is a worse outcome than one that told somebody at 80% and shortened its own retention at 100%. The guard reads what the store was actually asked to do over a window, extrapolates it to a month, and hands back an assessment the capture and compaction paths can act on.

Two ceilings, either of which may be left unset:

  • bytes per month, for an operator who thinks in volume or has a transfer allowance;
  • dollars per month, for one who thinks in invoices.

With both set, the rung is the worse of the two axes and BudgetAssessment::$reason names which one it came from. Zero on either axis means that axis does not constrain anything.

Prices default to Cloudflare R2: $0.015 per GB-month of storage, $4.50 per million class-A operations, $0.36 per million class-B operations, and no egress charge. AWS S3 charges $0.023, $5.00 and $0.40 for the same three lines and bills egress separately, so an S3 site sets its own prices through the constructor. R2's free allowance of 10 GB-months, 1 million class-A and 10 million class-B operations is recorded here as constants but is not subtracted from a projection: the allowance is per account, and Strata is rarely the only thing in the account.

Egress is not priced at all. R2 does not charge for it, and an S3 site's egress depends on where the reader is rather than on what Strata wrote.

A gigabyte is 2^30 bytes everywhere in this class.

Summary

Methods
Properties
Constants
__construct
assess
assessSince
No public properties found
SECONDS_PER_MONTH
BYTES_PER_GIGABYTE
R2_DOLLARS_PER_GIGABYTE_MONTH
R2_DOLLARS_PER_MILLION_CLASS_A
R2_DOLLARS_PER_MILLION_CLASS_B
R2_FREE_GIGABYTE_MONTHS
R2_FREE_CLASS_A
R2_FREE_CLASS_B
S3_DOLLARS_PER_GIGABYTE_MONTH
S3_DOLLARS_PER_MILLION_CLASS_A
S3_DOLLARS_PER_MILLION_CLASS_B
No protected methods found
No protected properties found
No protected constants found
dollarsFor
saturate
now
clock
bytesCeiling
dollarsCeiling
dollarsPerGigabyteMonth
dollarsPerMillionClassA
dollarsPerMillionClassB
No private constants found

Constant

SECONDS_PER_MONTH

SECONDS_PER_MONTH = 2630016

Seconds in the month every projection is made against: 30.44 days.

BYTES_PER_GIGABYTE

BYTES_PER_GIGABYTE = 1073741824

Bytes in the gigabyte storage is priced by.

R2_DOLLARS_PER_GIGABYTE_MONTH

R2_DOLLARS_PER_GIGABYTE_MONTH = 0.015

Cloudflare R2 storage, dollars per GB-month.

R2_DOLLARS_PER_MILLION_CLASS_A

R2_DOLLARS_PER_MILLION_CLASS_A = 4.5

Cloudflare R2 class-A operations, dollars per million.

R2_DOLLARS_PER_MILLION_CLASS_B

R2_DOLLARS_PER_MILLION_CLASS_B = 0.36

Cloudflare R2 class-B operations, dollars per million.

R2_FREE_GIGABYTE_MONTHS

R2_FREE_GIGABYTE_MONTHS = 10

GB-months of Cloudflare R2 storage included in the free allowance.

R2_FREE_CLASS_A

R2_FREE_CLASS_A = 1000000

Class-A operations included in the Cloudflare R2 free allowance, per month.

R2_FREE_CLASS_B

R2_FREE_CLASS_B = 10000000

Class-B operations included in the Cloudflare R2 free allowance, per month.

S3_DOLLARS_PER_GIGABYTE_MONTH

S3_DOLLARS_PER_GIGABYTE_MONTH = 0.023

AWS S3 standard storage, dollars per GB-month.

S3_DOLLARS_PER_MILLION_CLASS_A

S3_DOLLARS_PER_MILLION_CLASS_A = 5.0

AWS S3 PUT, COPY, POST and LIST requests, dollars per million.

S3_DOLLARS_PER_MILLION_CLASS_B

S3_DOLLARS_PER_MILLION_CLASS_B = 0.4

AWS S3 GET and SELECT requests, dollars per million.

Properties

$clock

$clock : \Closure

Returns the current unix timestamp.

Type

Closure

$bytesCeiling

$bytesCeiling : int

Type

int

$dollarsCeiling

$dollarsCeiling : float

Type

float

$dollarsPerGigabyteMonth

$dollarsPerGigabyteMonth : float

Type

float

$dollarsPerMillionClassA

$dollarsPerMillionClassA : float

Type

float

$dollarsPerMillionClassB

$dollarsPerMillionClassB : float

Type

float

Methods

__construct()

__construct(int  $bytesCeiling = 0, float  $dollarsCeiling = 0.0, float  $dollarsPerGigabyteMonth = \self::R2_DOLLARS_PER_GIGABYTE_MONTH, float  $dollarsPerMillionClassA = \self::R2_DOLLARS_PER_MILLION_CLASS_A, float  $dollarsPerMillionClassB = \self::R2_DOLLARS_PER_MILLION_CLASS_B, callable|null  $clock = null) : mixed

Constructs the guard.

Parameters

int $bytesCeiling

Bytes per month the store may be sent, or 0 for no ceiling on that axis.

float $dollarsCeiling

Dollars per month the store may cost, or 0.0 for no ceiling on that axis.

float $dollarsPerGigabyteMonth

Storage price. Defaults to R2's.

float $dollarsPerMillionClassA

Price per million billed writes: put, delete and list. Defaults to R2's.

float $dollarsPerMillionClassB

Price per million billed reads: get and head. Defaults to R2's.

callable|null $clock

Returns a unix timestamp as an int. NULL uses time(). Read only by BudgetGuard::assessSince().

Throws

\InvalidArgumentException

When a ceiling or a price is negative. A negative ceiling would put every reading above it at once, and a negative price would let traffic buy budget back.

Returns

mixed —

assess()

assess(int  $storedBytes, \Drupal\strata\Storage\ProviderStats  $stats, int  $secondsElapsed) : \Drupal\strata\Budget\BudgetAssessment

Reads a window of traffic and returns the rung it lands on.

The window is extrapolated to a 30.44-day month. Storage is priced on what the store holds now, held for the whole month; requests are priced on the observed rate extrapolated over it. Growth is not compounded into the storage line, so the dollars figure is what the current shape of the site costs to keep rather than a forecast of where it is heading.

Parameters

int $storedBytes

What the store holds right now.

\Drupal\strata\Storage\ProviderStats $stats

Traffic observed over the window.

int $secondsElapsed

How long the window was. Zero or less means no rate can be read from it, and the reading comes back on the normal rung with nothing projected rather than dividing by zero.

Throws

\InvalidArgumentException

When $storedBytes is negative.

Returns

\Drupal\strata\Budget\BudgetAssessment —

The reading.

assessSince()

assessSince(int  $windowStartedAt, int  $storedBytes, \Drupal\strata\Storage\ProviderStats  $stats) : \Drupal\strata\Budget\BudgetAssessment

Reads a window that started at a given time and runs to now.

The default way to call the guard from a cron run or a status page, which knows when the counter was last reset but not how long ago that was. BudgetGuard::assess() takes the duration directly for a caller that already has it.

Parameters

int $windowStartedAt

Unix timestamp the accumulator was last reset at.

int $storedBytes

What the store holds right now.

\Drupal\strata\Storage\ProviderStats $stats

Traffic observed since $windowStartedAt.

Throws

\InvalidArgumentException

When $storedBytes is negative.

\UnexpectedValueException

When the injected clock does not return an int.

Returns

\Drupal\strata\Budget\BudgetAssessment —

The reading.

dollarsFor()

dollarsFor(int  $storedBytes, \Drupal\strata\Storage\ProviderStats  $stats, float  $months) : float

What a month of this traffic costs.

Parameters

int $storedBytes

What the store holds right now.

\Drupal\strata\Storage\ProviderStats $stats

Traffic observed over the window.

float $months

How many months the window extrapolates to.

Returns

float —

Dollars, storage and requests together.

saturate()

saturate(float  $projected) : int

Narrows a projected byte count to an int without wrapping.

A short window multiplies the observed volume by a large factor, and a projection past PHP_INT_MAX cast straight to int produces a number with no relationship to the input. Pinning it at the maximum keeps the reading above every ceiling, which is where a projection that large belongs.

Parameters

float $projected

The projected byte count.

Returns

int —

The count, at most PHP_INT_MAX.

now()

now() : int

Reads the injected clock.

Throws

\UnexpectedValueException

When the injected clock returns anything but an int. Caught here rather than left to the subtraction, where a string clock would make every window look empty.

Returns

int —

A unix timestamp.