\Drupal\strata\Tier TieredProvider

One store spread across several buckets.

Implements the same provider contract as a single bucket, so nothing above it changes: the object store, the commit log, the segment writer and the ref store all compose the keys they always composed. What changes is only which endpoint a key is served from, and a key's address is not touched by that - a tier is where an object lives, never part of its name, so the same frame in two tiers is one object rather than a copy under a second name.

A write goes to the nearest tier and nowhere else. Far tiers are written to once per object, by TierMigrator, on a schedule. That is the whole cost argument: a far tier's class-A count is the number of objects that have ever aged into it, not the number of times the site was written to.

A read tries what the placement index knows, then everything else nearest-first. A known object costs one request. An unknown one costs up to one request per tier and then records what it found, so it is unknown only once - which is also why a dropped placement table costs requests rather than history.

Nothing is ever reconstructed. An object no tier holds raises, naming every tier that was asked. A head or an exists answers absent only after every tier said absent. The store never reports clean about a question it could not put.

A delete refuses when a tier holding the key cannot be reached. Deleting from the reachable tiers and forgetting the placement would leave an object in a bucket with no row naming it, which is indistinguishable from a stale index and is exactly the state a prune receipt exists to avoid.

Summary

Methods
Properties
Constants
__construct
tiers
placement
tier
tierStatus
tierNames
listIn
putIn
getFrom
deleteFrom
id
label
capabilities
isReachable
unreachableReason
get
stream
head
exists
put
delete
list
No public properties found
CURSOR_SEPARATOR
No protected methods found
No protected properties found
No protected constants found
readOrder
deleteTargets
rangedGet
reachableTier
learn
nowhere
parseCursor
composeCursor
resolver
built
broken
policy
placement
No private constants found

Constant

CURSOR_SEPARATOR

CURSOR_SEPARATOR = "\x1f"

Separator between the tier index and that tier's own cursor in a composite listing cursor.

A unit separator, because an endpoint's continuation token is opaque and may contain anything a base64 alphabet does, including every printable character a more obvious delimiter would use.

Properties

$resolver

$resolver : \Closure

Builds the provider for one tier.

Type

Closure

$built

$built : array

Providers already built, keyed by tier index.

Built on first use, the same way StorageProviderManager defers: a far tier that is misconfigured must report itself unreachable rather than stopping a flush that never touches it.

Type

array<int, StorageProviderInterface> —

$broken

$broken : array

Why a tier could not be built, keyed by tier index.

Type

array<int, string> —

$policy

$policy : \Drupal\strata\Tier\TierPolicy

Type

TierPolicy

Methods

__construct()

__construct(\Drupal\strata\Tier\TierPolicy  $policy, callable  $resolver, \Drupal\strata\Tier\PlacementIndexInterface  $placement) : mixed

Constructs a router.

Parameters

\Drupal\strata\Tier\TierPolicy $policy

Decides where a write goes, which order a read tries, and what a promotion targets.

callable $resolver

Builds the provider for one tier. Injected rather than resolved here so the engine keeps being the only place that reads settings, and so a test can hand over local directories.

\Drupal\strata\Tier\PlacementIndexInterface $placement

Remembers which tier served which key.

Returns

mixed —

tiers()

tiers() : \Drupal\strata\Tier\TierMap

The ladder this router applies.

Returns

\Drupal\strata\Tier\TierMap —

The ladder.

placement()

placement() : \Drupal\strata\Tier\PlacementIndexInterface

The placement index this router reads and writes.

Returns

\Drupal\strata\Tier\PlacementIndexInterface —

The index.

tier()

tier(int  $index) : \Drupal\strata\Storage\StorageProviderInterface

The provider for one tier.

Parameters

int $index

Tier index.

Throws

\InvalidArgumentException

When the index is not on the ladder.

\RuntimeException

When the tier cannot be built. The reason is remembered, so a second attempt reports the same thing without paying to find out again.

Returns

\Drupal\strata\Storage\StorageProviderInterface —

The provider.

tierStatus()

tierStatus() : array<int,string|null>

Why each tier cannot be reached, if it cannot.

Returns

array

Tier index keyed to the reason it is unreachable, or NULL when it is reachable.

tierNames()

tierNames() : array<int,string>

Tier names keyed by index.

Returns

array

Names, so a finding can say "the year bucket" rather than "tier 3".

listIn()

listIn(int  $index, string  $prefix = '', string|null  $cursor = null, int  $limit = 1000) : \Drupal\strata\Storage\ObjectPage

One page of one tier's listing, without merging the others in.

What TierMigrator and TierPlacementRebuilder work from: both are asking about one bucket, and the merged listing would hide which bucket an object came from, which is the answer they need.

Parameters

int $index

Tier index.

string $prefix

Key prefix.

string|null $cursor

That tier's own continuation token.

int $limit

Most objects to return.

Throws

\InvalidArgumentException

When the index is not on the ladder.

\RuntimeException

When the tier cannot be built or the listing fails.

Returns

\Drupal\strata\Storage\ObjectPage —

The page, with that tier's own cursor.

putIn()

putIn(int  $index, string  $key, string  $body) : \Drupal\strata\Storage\PutResult

Writes one object into a named tier.

Only TierMigrator uses this. A write that arrives through the provider contract goes where the policy says, which is always the nearest tier; this is the one path that puts bytes into a far one, and it records the placement so the object is findable in one request afterwards.

Parameters

int $index

Tier index to write to.

string $key

Object key.

string $body

The bytes.

Throws

\InvalidArgumentException

When the index is not on the ladder.

\RuntimeException

When the tier cannot be built or the write fails.

Returns

\Drupal\strata\Storage\PutResult —

What was written.

getFrom()

getFrom(int  $index, string  $key) : string

Reads one object out of a named tier, without falling back to another.

A verify or a migration asking "is it in THIS bucket" must not be answered by another bucket, because the answer it wants is precisely which one has it.

Parameters

int $index

Tier index.

string $key

Object key.

Throws

\InvalidArgumentException

When the index is not on the ladder.

\RuntimeException

When the tier cannot be built, or the object is not in it.

Returns

string —

The bytes.

deleteFrom()

deleteFrom(int  $index, string  $key) : bool

Removes one object from a named tier.

Parameters

int $index

Tier index.

string $key

Object key.

Throws

\InvalidArgumentException

When the index is not on the ladder.

\RuntimeException

When the tier cannot be built or is unreachable, or the delete fails.

Returns

bool —

TRUE when the endpoint accepted the delete.

id()

id() : string

The plugin id this provider registers under.

Returns

string —

A short lowercase token such as "s3", "r2", "sftp" or "local".

label()

label() : string

Human-readable name for the settings form.

Returns

string —

The label.

capabilities()

capabilities() : \Drupal\strata\Storage\Capabilities

What this endpoint can do.

The nearest tier's capabilities, with one exception. Every write goes to the nearest tier, so what a write may ask for is what that tier can do; and a range read is honoured here whatever the serving tier can do, by fetching the object whole and slicing it, so the range flag is reported as available rather than intersected down to the weakest tier.

Returns

\Drupal\strata\Storage\Capabilities —

The capability set.

isReachable()

isReachable() : bool

Whether the endpoint is reachable and the credentials work.

Reachable when the nearest tier is, because that is where every write lands. A far tier that is down does not stop a flush, and reporting the whole store unreachable for it would take a working site offline over a bucket nothing is currently writing to. TieredProvider::tierStatus() is what a verify pass reads to find out about the rest.

Returns

bool —

TRUE when a request would succeed right now.

unreachableReason()

unreachableReason() : string|null

Why the endpoint is unreachable.

Returns

string|null —

A short human-readable reason, or NULL when it is reachable.

get()

get(string  $key, ?\Drupal\strata\Storage\ByteRange  $range = null) : string

Reads an object.

Parameters

string $key

Object key relative to the store root.

?\Drupal\strata\Storage\ByteRange $range

A byte range, or NULL for the whole object.

Returns

string —

The bytes.

stream()

stream(string  $key) : resource

Opens an object as a stream.

Parameters

string $key

Object key relative to the store root.

Returns

resource —

An open readable stream the caller must close.

head()

head(string  $key) : \Drupal\strata\Storage\ObjectMeta|null

Metadata for one object without reading it.

Parameters

string $key

Object key relative to the store root.

Returns

\Drupal\strata\Storage\ObjectMeta|null —

The metadata, or NULL when the object is absent. Absence is not an error, because every caller of this method is asking precisely in order to find out.

exists()

exists(string  $key) : bool

Whether an object exists.

Parameters

string $key

Object key relative to the store root.

Returns

bool —

TRUE when it is present.

put()

put(string  $key, mixed  $body, array  $options = []) : \Drupal\strata\Storage\PutResult

Writes an object.

Parameters

string $key

Object key relative to the store root.

mixed $body

The bytes, or an open readable stream. A stream is read once and not rewound, so a caller that needs it again must seek itself.

array $options

Provider options. ifNoneMatch (write only when the key is absent) is honoured wherever Capabilities::$conditionalWrite is true. metadata, contentType and storageClass are honoured only by providers whose endpoint carries them; a provider that cannot store an option refuses the write rather than dropping it, because an object written without the metadata its reader expects is indistinguishable from a corrupt one. Nothing in the engine depends on user metadata: a stored object carries whatever a reader needs in its own bytes.

Returns

\Drupal\strata\Storage\PutResult —

What was written.

delete()

delete(array  $keys) : int

Deletes objects.

Grouped by the tiers that hold each key, so a key in one bucket costs one request there rather than one in every bucket. A key nothing is recorded for is deleted from every tier, which is safe because deleting an absent key is not an error, and is what keeps a prune correct while the placement table is still cold.

Parameters

array $keys

Object keys relative to the store root.

Returns

int —

How many keys the endpoint accepted. A store that can tell an absent key from a removed one counts only what it removed; S3 reports success for an absent key and cannot distinguish, so it counts everything it was given. Do not read this as "how many existed" - a prune receipt counts from the frame index, which knows.

list()

list(string  $prefix = '', ?string  $cursor = null, int  $limit = 1000, ?string  $delimiter = null) : \Drupal\strata\Storage\ObjectPage

Lists one page of objects under a prefix.

Tiers are drained in order and a key is emitted by the nearest tier that holds it, so a replicated object is named once rather than once per copy. The cursor carries the tier being drained and that tier's own token, because an endpoint's continuation token is opaque and cannot be synthesised from a key.

A page can come back empty with a cursor still set, when the tier being drained had nothing left under the prefix and a further tier has not been reached yet. Every caller in the engine pages until the cursor is NULL, which is the contract, so that costs a request rather than a missed object.

Parameters

string $prefix

Key prefix relative to the store root; an empty string lists everything.

?string $cursor

Continuation token from a previous page, or NULL to start.

int $limit

Most objects to return in this page.

?string $delimiter

Grouping delimiter, or NULL for a flat listing.

Returns

\Drupal\strata\Storage\ObjectPage —

The page.

readOrder()

readOrder(string  $key) : list<int>

The order tiers are tried in for one key.

Parameters

string $key

Object key.

Returns

list

Tier indexes.

deleteTargets()

deleteTargets(string  $key) : list<int>

The tiers a delete has to reach for one key.

Parameters

string $key

Object key.

Returns

list

Tier indexes.

rangedGet()

rangedGet(int  $index, string  $key, \Drupal\strata\Storage\ByteRange|null  $range) : string

A ranged read, satisfied here when the serving tier cannot do one itself.

Parameters

int $index

Tier index.

string $key

Object key.

\Drupal\strata\Storage\ByteRange|null $range

The range, or NULL for the whole object.

Throws

\RuntimeException

When the tier cannot be built, the object is absent, or the object is shorter than the range asks for. A short read raises rather than returning what there was, because a truncated frame cannot be told apart from a real one downstream.

Returns

string —

The bytes.

reachableTier()

reachableTier(int  $index) : \Drupal\strata\Storage\StorageProviderInterface

The provider for a tier that can be reached, or a refusal.

Parameters

int $index

Tier index.

Throws

\RuntimeException

When the tier cannot be built or is unreachable.

Returns

\Drupal\strata\Storage\StorageProviderInterface —

The provider.

learn()

learn(string  $key, int  $index, int  $bytes = 0) : void

Records that a tier served a key.

Parameters

string $key

Object key.

int $index

Tier index that answered.

int $bytes

The object's size, or zero when the call did not report one.

Returns

void —

nowhere()

nowhere(string  $key, array  $tried) : string

The message for an object no tier has.

Names every tier that was asked and what each said, because the useful question after this is which bucket was supposed to hold it.

Parameters

string $key

Object key.

array $tried

Tier index keyed to the reason it did not answer.

Returns

string —

The message.

parseCursor()

parseCursor(string|null  $cursor) : array{0: int, 1: string|null}

Splits a composite cursor.

Parameters

string|null $cursor

The cursor, or NULL to start at the nearest tier.

Returns

array{0: int, 1: string|null} —

The tier index and that tier's own token.

composeCursor()

composeCursor(int  $index, string|null  $inner) : string

Joins a tier index and its token into one cursor.

Parameters

int $index

Tier index.

string|null $inner

That tier's token, or NULL to start it from the beginning.

Returns

string —

The composite cursor.