partition
blobmap.partition ¶
The decisions: arrays in, Manifest out.
Pure functions with no I/O. Everything that actually matters lives here, and
it is testable with hand-written Array objects in
milliseconds.
The cut proceeds top down:
- Coordinates and arrays under
t_hot_bytesare pinned hot. - If the whole scope fits in
t_max_bytes, it becomes one blob. This is the common case, and the best outcome for tape: small stores are read whole, so one mount beats one per variable. - Arrays over
t_max_bytesare bucketed, which is the only way to cut inside an array. - What is left is coalesced with its neighbours until each group clears
t_min_bytes, so a wide tree of small variables does not cost a tape mount per gigabyte.
Example
from blobmap.model import Array, GiB, Policy arrays = [Array("time", (400,), (400,), 8, is_coordinate=True), ... Array("tas", (400, 4, 4), (10, 4, 4), 4, ... stored_bytes=400 * GiB)] manifest = partition("cordex/a.zarr", arrays) sorted(b.id for b in manifest.blobs) ['b_tas'] "time/**" in manifest.hot_always True
Diff
dataclass
¶
Diff(
added: tuple[Blob, ...] = (),
removed: tuple[Blob, ...] = (),
modified: tuple[tuple[Blob, Blob], ...] = (),
)
What a repartition changed.
Attributes:
| Name | Type | Description |
|---|---|---|
added |
tuple[Blob, ...]
|
Blobs that did not exist before. Always safe. |
removed |
tuple[Blob, ...]
|
Blobs that disappeared. Any tape copy held against these ids is now orphaned. |
modified |
tuple[tuple[Blob, Blob], ...]
|
Pairs of (old, new) for blobs whose definition changed. The id still resolves but now covers different objects, so the tape copy no longer matches. |
Example
from blobmap.model import Blob, Manifest old = Manifest("s", (Blob("a", ("a",)),), ()) new = Manifest("s", (Blob("a", ("a",)), Blob("b", ("b",))), ()) diff(old, new).is_additive True
is_additive
property
¶
is_additive: bool
Whether no existing blob definition changed or disappeared.
Anything else invalidates ids that blobtier holds tape addresses for.
Returns:
| Type | Description |
|---|---|
bool
|
True when only additions were made. |
is_empty
property
¶
is_empty: bool
Whether nothing changed at all.
Returns:
| Type | Description |
|---|---|
bool
|
True when there is nothing to write. |
describe ¶
describe() -> str
Render for a log line or --dry-run output.
Returns:
| Type | Description |
|---|---|
str
|
One line per change, prefixed |
Source code in src/blobmap/partition.py
428 429 430 431 432 433 434 435 436 437 438 | |
bucket_width ¶
bucket_width(a: Array, policy: Policy) -> int
Choose how many objects along dimension 0 go into one blob.
Two bounds apply:
- ideal hits
t_max_bytesat the compression observed today - hard keeps the blob under
width_clamp * t_max_byteseven if compression degrades to nothing, computed from uncompressed object size, which cannot change without rewriting the array
The result is rounded down to a power of two so a ratio drifting from 2.0
to 2.3 does not move the width and renumber every blob. Rounding is
skipped below pow2_floor, where it would throw away too much of the
target. That is the common case for sharded arrays, which have far fewer
and larger objects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
Array
|
The array to size. Its
|
required |
policy
|
Policy
|
Thresholds to apply. |
required |
Returns:
| Type | Description |
|---|---|
int
|
Objects per blob, at least 1. |
Note
Logs a warning when a single object already exceeds t_max_bytes,
since there is then nothing left to cut.
Example
from blobmap.model import Array, GiB, Policy plain = Array("tas", (1_314_000, 412, 424), (128, 412, 424), 4, ... stored_bytes=459 * GiB) bucket_width(plain, Policy()) 2048
The same bytes in a 10x sharded array is a tenth of the objects, so
the width falls with it. Reading chunks instead of shards here
would give both arrays the same width, and blobs 10x the target:
sharded = Array("tas", (1_314_000, 412, 424), (128, 412, 424), 4, ... shards=(1280, 412, 424), stored_bytes=459 * GiB) bucket_width(sharded, Policy()) 128
Source code in src/blobmap/partition.py
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | |
partition ¶
partition(
scope: str,
arrays: list[Array],
*,
policy: Policy | None = None,
previous: Manifest | None = None
) -> Manifest
Compute the blob definitions for one scope.
When previous is given its blobs are pinned: carried over byte
identical, with new cuts only in regions no existing blob claims. That is
what keeps blob ids, and the tape addresses blobtier holds against them,
valid across a repartition. Growth along a bucketed dimension needs no
repartition at all, since the id is arithmetic.
Because pinning is unconditional, a policy change alone has no effect on
an existing scope. Cuts are frozen once made. Pass previous=None to
recompute from scratch, which is the only way to move a blob and the only
way to orphan a tape copy.
hot_always is always recomputed, since it follows from the store's
structure: hot data is never archived, so there is no state to invalidate,
and an array that grew past t_hot_bytes should stop being held back.
pinned is carried over untouched, because it follows from intent rather
than structure and can only be changed deliberately.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
str
|
Prefix these definitions apply to, relative to the data store. |
required |
arrays
|
list[Array]
|
Every array under the scope, from
|
required |
policy
|
Policy | None
|
Thresholds. Defaults to |
None
|
previous
|
Manifest | None
|
The manifest currently in effect, whose blobs are pinned. |
None
|
Returns:
| Type | Description |
|---|---|
Manifest
|
A validated |
Manifest
|
that intend to write should use |
Manifest
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the result fails validation, which would indicate a bug in the cut rather than bad input. |
Example
from blobmap.model import Array, GiB first = partition("s", [Array("tas", (400, 4, 4), (10, 4, 4), 4, ... stored_bytes=400 * GiB)]) grown = partition("s", [Array("tas", (800, 4, 4), (10, 4, 4), 4, ... stored_bytes=800 * GiB)], ... previous=first) diff(first, grown).is_empty # an append changes nothing True
Source code in src/blobmap/partition.py
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | |
diff ¶
diff(old: Manifest | None, new: Manifest) -> Diff
Compare two manifests by blob id.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
old
|
Manifest | None
|
The manifest previously in effect, or |
required |
new
|
Manifest
|
The freshly computed manifest. |
required |
Returns:
| Type | Description |
|---|---|
Diff
|
A |
Source code in src/blobmap/partition.py
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 | |