Skip to content

fix(zero): size-aware predicate move timeout and rebalancer backoff - #9792

Merged
matthewmcneely merged 6 commits into
mainfrom
matthewmcneely/predicate-move-timeout
Jul 29, 2026
Merged

fix(zero): size-aware predicate move timeout and rebalancer backoff#9792
matthewmcneely merged 6 commits into
mainfrom
matthewmcneely/predicate-move-timeout

Conversation

@matthewmcneely

@matthewmcneely matthewmcneely commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Zero cancels every predicate move at a hardcoded 120 minutes (predicateMoveTimeout in dgraph/cmd/zero/tablet.go), so moves of large tablets can never complete. The report in #9784 is a 20GB predicate that needed about 16 hours. This constant has been raised once before for the same reason, from 20 minutes to 2 hours in #6388, so any fixed wall-clock value is eventually wrong for someone's data size.

The failure mode is worse than a cancelled move. While a move runs, Zero aborts every commit on the predicate (blockTablet, enforced in oracle.go). On timeout, all streamed work is discarded, and the auto-rebalancer deterministically re-picks the same largest tablet every --rebalance_interval (default 8m). A cluster with one oversized predicate ends up in a permanent loop: two hours of write aborts and streaming, cancel, discard, repeat.

Changes

  • Scale the move timeout with tablet size. moveTimeout() grants at least size / 256 KiB-per-second, using the larger of on-disk and uncompressed bytes, with the previous 2h constant as the floor. Tablets under ~1.8GiB behave exactly as before; the 20GB tablet from Unblock large (>15GB) predicate moves by removing hardcoded 2-hour timeout #9784 gets ~23h.
  • Back off after a failed move. A failure that ran at least a minute puts the tablet on a cooldown that doubles per consecutive failure (1h up to 24h) and is never shorter than the failed attempt itself. chooseTablet skips tablets on cooldown, a successful move clears the state, and manual moves via /moveTablet bypass the backoff.
  • Log the computed timeout in the move announcement line so operators can see the budget each move was given.

A configurable flag was considered and dropped. A floor-style flag reads ambiguously against a "timeout" name, and a cap-style flag recreates the original bug; the size-derived value needs no configuration.

Why 256 KiB/s

The receive path applies the whole stream through serial 32MB Raft proposals (batchAndProposeKeyValues in worker/predicate_move.go): each chunk is replicated to quorum, written to the Raft WAL, and fully applied before the next is proposed. Sustained rates in the field are therefore low; the #9784 report works out to ~0.35 MB/s on-disk. The floor rate is deliberately conservative. The throughput problem itself is out of scope here and tracked in #9790 (pipeline the receive path) and #9791 (two-phase move to shrink the commit-abort window).

Testing

  • New unit tests for the timeout computation and backoff bookkeeping in dgraph/cmd/zero/tablet_test.go (TestMoveTimeout, TestMoveCooldown, TestMoveBackoff).
  • New end-to-end test: TestLargePredicateMove in systest/predicate-move runs the full scenario against a real two-group cluster. It loads several GiB of incompressible data (self-calibrating the size to the host's ingest rate so the move reliably outlasts the kill point), kills the destination Alpha mid-stream, asserts the failed move triggers the rebalancer backoff and leaves the source group intact, then restarts the Alpha, retries, and asserts the move completes with both announcements carrying the size-scaled timeout. It sits behind the largemove build tag, which no CI workflow compiles; run it manually per the package README (a run loads GiBs of data and takes 15+ minutes).
  • Verified on GCE (c3-standard-8, Ubuntu 24.04, pd-ssd): PASS in 16m at the default MOVE_TEST_GB=8. Zero announced timeout: 9h11m59s for the 8.68GB tablet on both attempts, the mid-stream kill produced the expected failure plus the "Skipping automatic rebalancing" backoff line, and the retry completed with all 131,296 triples intact on the destination. Observed move throughput was ~19 MiB/s against 42-68 MiB/s raw ingest even with single-replica groups, which is the receive-path bottleneck tracked in Predicate moves: pipeline destination Raft proposals and batch KV writes #9790.
  • go build, gofmt, and trunk pass.
  • Existing move coverage (systest/group-delete TestNodes, systest/integration2 TestUniqueMultipleGroups) moves small tablets, so the 2h floor applies and behavior there is unchanged.

Fixes #9784

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@matthewmcneely
matthewmcneely requested a review from a team as a code owner July 27, 2026 17:22
matthewmcneely and others added 2 commits July 28, 2026 18:44
Zero cancelled every predicate move at a hardcoded 120 minutes, so moves
of large tablets (>15GB) could never complete (#9784). The constant has
been bumped once before for the same reason (#6388); a fixed wall-clock
value is always wrong for someone.

- Scale the move timeout with tablet size: a move now gets at least
  size / 256KiB-per-second, using the larger of on-disk and uncompressed
  bytes, with the previous 2h constant as the floor. Small tablets
  behave exactly as before; a 20GB tablet gets ~23h instead of 2h.
- Back off in the auto-rebalancer after a failed move: the cooldown
  doubles per consecutive failure (1h up to 24h) and is never shorter
  than the failed attempt itself, so Zero stops re-picking the same
  tablet every rebalance tick, re-streaming it for hours, and aborting
  commits on the predicate the whole time. Manual moves via /moveTablet
  bypass the backoff, and a successful move clears it.
- Log the computed timeout in the move announcement so operators can
  see the budget a move was given.

Fixes #9784

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a long-running integration test for the size-aware move timeout
and rebalancer backoff, excluded from CI via the largemove build tag
(no workflow compiles it). Against a real two-group cluster it:

- loads MOVE_TEST_GB (default 8) GiB of incompressible values into one
  predicate and waits for Zero to report the tablet size,
- kills the destination Alpha mid-move and asserts the move fails and
  Zero records the rebalancer backoff,
- restarts the destination, retries, and asserts the move completes,
  the data survives intact, and every move announcement carried a
  size-scaled timeout above the 2h floor.

Run with:
  go test -v -timeout=8h --tags=largemove ./systest/predicate-move/

See systest/predicate-move/README.md for knobs and requirements.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@shiva-istari
shiva-istari force-pushed the matthewmcneely/predicate-move-timeout branch from 8b9f01b to ca799f7 Compare July 28, 2026 13:14
Comment thread dgraph/cmd/zero/tablet.go
The induced-failure leg killed the destination Alpha at a fixed 75s,
which assumed the move would still be streaming. On fast hosts it is
not: an M4 laptop moves a 3.7GiB tablet in ~64s, so the test failed
with 'move finished before the kill point'.

Measure the host's ingest rate during the initial load and top up the
data until the estimated move duration (bytes / ingest rate) is at
least twice the kill point. A move cannot outrun ingest on the same
host since it re-reads, rolls up, streams, and re-applies every byte
through the destination's Raft, so the kill now reliably lands
mid-stream. MOVE_TEST_GB becomes a floor rather than the final size.

Verified on the same host: the size-scaled timeout announcement
(timeout: 4h13m0s for a 3.7GiB tablet) and the full move path worked;
only the kill timing needed calibration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@blacksmith-sh

This comment has been minimized.

matthewmcneely and others added 3 commits July 28, 2026 17:05
An Alpha killed mid-move replays several GiB of badger/WAL state on
restart and needs time to re-establish cluster connections; asserting
HealthCheck once immediately after StartAlpha fails with 'No connection
exists'. Poll with a 10m deadline instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…urns

The cluster HTTP client binds to one alpha, and the group->alpha mapping
is not deterministic, so the killed destination can be the client's
alpha; querying /state during the outage then fails with connection
refused. Assert the tablet stayed on the source group after health
returns instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@matthewmcneely
matthewmcneely merged commit 41f2cd6 into main Jul 29, 2026
19 checks passed
@matthewmcneely
matthewmcneely deleted the matthewmcneely/predicate-move-timeout branch July 29, 2026 00:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Unblock large (>15GB) predicate moves by removing hardcoded 2-hour timeout

3 participants