\Drupal\strata\Compaction Recompressor

Rewrites stored objects at a denser compression setting.

The flush path compresses at zstd level 1 because it runs inside a web request: 4.28x at 422.9 MB/s, against level 19's 4.87x at 4.5 MB/s. Compaction is not in a request and can spend the CPU, and with a trained dictionary level 19 reaches 5.86x. On a store where tree manifests are 72% of the bytes, that is the largest single reduction available after the base interval itself.

A frame's content address does not change, because the address is the digest of the DECODED bytes. That is what makes this safe to do to live data: every tree, segment and delta parent that named the frame still names it, and nothing has to be rewritten to follow it. Only the object the frame lives in changes, and the index row that says where.

Packs are rewritten whole, never frame by frame. Densifying one 16 KiB frame out of a 1 MiB pack into its own object would trade a compression gain for one Class A operation per frame, which is the cost Packer exists to avoid. One pack in, one pack out, one GET and one PUT.

Every frame in a pack is carried forward, including ones nothing references any more. Compaction changes how bytes are stored and never which bytes exist; removing content is a prune, and a prune produces a receipt.

Summary

Methods
Properties
Constants
__construct
recompressPack
recompressFrame
recompressPacks
No public properties found
MIN_GAIN
No protected methods found
No protected properties found
No protected constants found
decoded
encode
relocatePacked
writer
dictionaryBytes
dictionaryFor
provider
index
store
codecs
cipher
packer
level
dictionary
dictionaryId
No private constants found

Constant

MIN_GAIN

MIN_GAIN = 0.05

How much smaller a rewrite must be to be worth the requests it costs.

An object that shrinks by less than this is left alone. The saving has to cover a GET, a PUT and the index writes.

Properties

$store

$store : \Drupal\strata\Cas\ObjectStore

Type

ObjectStore

$codecs

$codecs : \Drupal\strata\Codec\CodecRegistry

Type

CodecRegistry

$cipher

$cipher : \Drupal\strata\Crypto\CipherInterface

Type

CipherInterface

$packer

$packer : \Drupal\strata\Cas\Packer

Type

Packer

$level

$level : int

Type

int

$dictionary

$dictionary : ?string

Type

string|null

$dictionaryId

$dictionaryId : ?string

Type

string|null

Methods

__construct()

__construct(\Drupal\strata\Storage\StorageProviderInterface  $provider, \Drupal\strata\Cas\FrameIndexInterface  $index, \Drupal\strata\Cas\ObjectStore  $store, \Drupal\strata\Codec\CodecRegistry  $codecs, \Drupal\strata\Crypto\CipherInterface  $cipher, \Drupal\strata\Cas\Packer  $packer = new \Drupal\strata\Cas\Packer(), int  $level = 19, string|null  $dictionary = null, string|null  $dictionaryId = null) : mixed

Constructs a recompressor.

Parameters

\Drupal\strata\Storage\StorageProviderInterface $provider

Where objects are read from and written to.

\Drupal\strata\Cas\FrameIndexInterface $index

Repointed at each frame's new location and size.

\Drupal\strata\Cas\ObjectStore $store

Reads each frame back through whatever encoded it.

\Drupal\strata\Codec\CodecRegistry $codecs

Supplies the dense writer, which may be one that shells out.

\Drupal\strata\Crypto\CipherInterface $cipher

Re-seals each rewritten frame.

\Drupal\strata\Cas\Packer $packer

Rebuilds the pack, so the rewritten object carries a directory like any other.

int $level

Compression level to rewrite at.

string|null $dictionary

Dictionary bytes to compress against, or NULL.

string|null $dictionaryId

Identifier recorded in each rewritten frame, required whenever a dictionary is given.

Returns

mixed —

recompressPack()

recompressPack(string  $key, bool  $force = false) : array{frames: int, before: int, after: int, saved: int, rewritten: bool}

Rewrites one pack at the dense setting.

Parameters

string $key

The pack's object key.

bool $force

TRUE to write the new object however little it saves. A key rotation re-seals bytes without shrinking them, and refusing on the gain would leave it unable to finish.

Throws

\RuntimeException

When the pack cannot be read or a frame inside it does not decode. Either is a corruption symptom and neither is safe to skip silently while rewriting the rest.

Returns

array{frames: int, before: int, after: int, saved: int, rewritten: bool} —

How many frames were carried forward, the object's size before and after, how many bytes that saved, and whether the new object was actually written.

recompressFrame()

recompressFrame(string  $hash, bool  $force = false) : int

Rewrites one standalone frame at the dense setting.

Only frames at or above the pack target are stored this way, so this path is rare and stays standalone rather than being folded into a pack.

Parameters

string $hash

Frame content address.

bool $force

TRUE to write the new object however little it saves, for the same reason Recompressor::recompressPack() takes the flag.

Throws

\RuntimeException

When the frame is not indexed, is packed, or does not decode.

Returns

int —

Bytes saved, or zero when the frame was left as it was. Zero is also a re-seal that saved nothing, so it is not a statement that nothing was written when $force is TRUE.

recompressPacks()

recompressPacks(list  $keys, int  $budget = 0) : array{packs: int, frames: int, before: int, after: int, skipped: int, problems: list<string>}

Rewrites a run of packs, stopping at a byte budget.

Compaction runs inside a cron window, so the caller sets how much work one pass may do rather than discovering it afterwards. A pack that fails is reported and skipped, because one corrupt object must not stop the pass from densifying the rest.

Parameters

list $keys

Pack object keys.

int $budget

Most stored bytes to read in this pass; zero for no limit.

Returns

array{packs: int, frames: int, before: int, after: int, skipped: int, problems: list} —

How many packs were rewritten, how many frames that moved, what those packs occupied before and occupy now, how many packs were already dense enough, and what could not be read.

decoded()

decoded(string  $hash) : string

Reads a frame back and checks it against its address.

Parameters

string $hash

Frame content address.

Throws

\RuntimeException

When the frame does not read back, or reads back as different content. Re-encoding content that does not match its address would file the wrong bytes under a name something else depends on.

Returns

string —

The decoded frame.

encode()

encode(string  $plain, string  $hash, string|null  $parent = null) : string

Compresses and seals a frame at the dense setting.

Parameters

string $plain

The decoded frame.

string $hash

Its content address, used as the associated data exactly as the flush path does.

string|null $parent

The frame this one is a delta of, or NULL when it stands alone. A delta has to be re-encoded against the same parent it was coded against: the index goes on calling it a delta, so a frame recompressed against the realm dictionary instead would be decoded with the parent's bytes as its dictionary and come back as noise.

Throws

\RuntimeException

When the codec or the cipher fails, or the parent does not read back.

Returns

string —

The encoded frame.

relocatePacked()

relocatePacked(string  $pack, array  $entry) : void

Points the index at a frame's new home inside a rewritten pack.

Parameters

string $pack

The new pack's id.

array $entry

The directory entry describing where the frame now sits.

Returns

void —

writer()

writer() : \Drupal\strata\Codec\CompressionCodecInterface

The codec that writes the dense form.

The bulk writer rather than the per-frame one, because a codec whose per-call overhead rules it out of the request path is exactly what compaction wants.

Returns

\Drupal\strata\Codec\CompressionCodecInterface —

The codec.

dictionaryBytes()

dictionaryBytes() : string|null

The dictionary bytes to compress against, if the codec can take one.

Returns

string|null —

The bytes, or NULL.

dictionaryFor()

dictionaryFor() : string|null

The dictionary id to record, if a dictionary is actually being used.

Returns

string|null —

The id, or NULL.