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.
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.
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.
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.
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".
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.
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.
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.
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.
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.