Skip to content

meta: pin a table's shard count once its shards are registered - #128

Open
bjmeetsfo wants to merge 1 commit into
mainfrom
oss/shard-count-growth-guard
Open

meta: pin a table's shard count once its shards are registered#128
bjmeetsfo wants to merge 1 commit into
mainfrom
oss/shard-count-growth-guard

Conversation

@bjmeetsfo

Copy link
Copy Markdown
Collaborator

Raising a table's shard count silently moves data out of reach

Bucket ranges are not stored. build_shards derives them from shard_count on every read:

let start_bucket = bucket_count * offset / table.shard_count;
let end_bucket = (bucket_count * (offset + 1) / table.shard_count).saturating_sub(1);

So raising shard_count renumbers the entire key space — and nothing rehashes. There is no split, repartition or redistribute anywhere in the tree; shard_id is just first_shard_id + offset.

The data for the buckets that moved is still sitting on the old shard. The routing table now sends those keys to a shard that has never seen them. The reads come back as misses, not errors.

Two tests print exactly what happens on a two-shard table grown to four:

shard 100 owned buckets 0..=536870911 and now owns Some((0, 268435455))
bucket 536870911 was rehomed to a shard that has never held it

Half of shard 100's key space, handed to a shard with none of the data.

partition_version and first_shard_id are already pinned after creation, and the guard sitting immediately above this one refuses to shrink. shard_count is the third knob that moves keys, and it was the one left open.

The fix

shard_count cannot change once any of the table's shards is registered. Registration is the metaserver's evidence that the table is holding data.

Growth before anything registers stays allowed — correcting the shard count of a table that is not yet holding anything is the legitimate use, and it is the one the existing suite already covered. Replica count and serving options are untouched: neither moves a key.

Please push back on this if online growth is meant to be supported

Two existing tests grew a table that already had a registered shard. Neither is about growth — one is about mutation-log recovery, the other about the mutation API replicating through raft — and both are adjusted here to keep testing what they test:

  • metaserver_mutation_log_recovers_routes_tables_and_state_changes — the shard is now registered after the table is sized. Where that call sits is incidental; the mutation count and the recovered state are identical either way. No assertion changed.
  • metaserver_raft_replicates_full_metadata_mutation_api — now updates the replica count instead, which demonstrates UpdateTable reaching every peer exactly as well. Two assertions changed with it.

I am flagging this rather than quietly absorbing it. If growing a live table is intended to be supported, then this is the wrong fix and what is actually needed is a redistribution step — the guard would then belong on "grow without redistributing" rather than on growth itself. What is not defensible is the current state, where the operation succeeds, reports success, and loses reachability without saying so.

Tests

5 new. Three fail on main:

test a_registered_shards_key_range_never_moves        ... FAILED
test a_key_does_not_change_shard_underneath_the_data  ... FAILED
test growing_a_table_that_already_owns_shards_is_refused ... FAILED

The other two are the edges: growth before registration is still allowed, and a registered table can still change its other options.

Verification:

  • cargo test -p temporalstore-rust --lib meta -- --test-threads=1257 passed, 0 failed.
  • cargo check -p temporalstore-rust --all-targets — clean.

Bucket ranges are not stored. build_shards derives them from shard_count on
every read, so raising it renumbers the entire key space - and nothing rehashes.
There is no split, repartition or redistribute anywhere in the tree, and a
shard id is just first_shard_id + offset.

The data for the buckets that moved is still sitting on the old shard, while the
routing table now sends those keys to a shard that has never seen them, so the
reads come back as misses rather than as errors. On a two-shard table grown to
four, the tests print it exactly: shard 100 owned buckets 0..=536870911 and now
owns 0..=268435455, and bucket 536870911 resolves to a shard with none of the
data.

partition_version and first_shard_id are already pinned after creation, and the
guard immediately above this one refuses to shrink. shard_count is the third
knob that moves keys, and the one left open.

It is now refused once any of the table's shards is registered, registration
being the evidence that the table is holding data. Growth before anything
registers stays allowed - correcting the shard count of a table that is not yet
holding anything is the legitimate use, and the one the existing suite covered.
Replica count and serving options are untouched; neither moves a key.

Two existing tests grew a table that already had a registered shard. Neither is
about growth - one is about mutation-log recovery, the other about the mutation
API replicating through raft - and both are adjusted to keep testing what they
test. The recovery one now registers its shard after the table is sized, which
is incidental to it and changes no assertion; the raft one updates the replica
count instead, which demonstrates UpdateTable reaching every peer just as well.

If growing a live table is meant to be supported, this is the wrong fix and what
is needed is a redistribution step. What is not defensible is the current state,
where the operation succeeds, reports success, and loses reachability silently.

Five tests. Three fail on main: a registered shard's key range moving, a key
changing shard underneath its data, and the growth not being refused.
@bjmeetsfo
bjmeetsfo requested a review from superhaiou as a code owner August 22, 2026 08:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants