Make the master's namespace durable and stop silent data loss - #1
Open
ChiragJS wants to merge 1 commit into
Open
Make the master's namespace durable and stop silent data loss#1ChiragJS wants to merge 1 commit into
ChiragJS wants to merge 1 commit into
Conversation
Live testing of the cluster surfaced four failures that all reported success while losing or corrupting data. Each is fixed here and pinned by a test that fails against the old behaviour. Master restart orphaned every file. Metadata lived only in Go maps, so a restart left chunks intact on disk but permanently unreachable, and the orphans were never reclaimed. The master now follows the GFS operation log design (HDFS edits + fsimage): the namespace is appended to a write-ahead log and fsync'd before the RPC is acknowledged, with checkpoints published by atomic rename to bound replay. Replica locations are deliberately not persisted -- chunkservers are the only authority on what they hold -- so they are relearned from registration and heartbeats, and orphan collection waits out a grace period first. Downloading a file twice into one directory appended to the previous result, doubling it. Downloads now stream into a temp file and rename into place, so a re-run replaces the old copy and an interrupted run leaves nothing behind. A download that could reach no replica returned nil and exited zero with no file written. Both the download and upload paths now propagate errors; the chunker distinguishes io.EOF from a real read failure rather than treating every error as end-of-file. Uploads wrote a single replica while the master recorded the full replication factor, leaving a window at RF=1 that the master could not see. The client now writes every allocated replica before reporting success. Also fixed, from the same review: - The master held its global mutex across the blocking stream.Send in Heartbeat, so one slow chunkserver could stall every RPC in the cluster. The response is now built under the lock and sent outside it, and chunkservers drop tasks when their queues are full instead of back-pressuring the stream. - Heartbeat reconciliation is rewritten to derive state from what a chunkserver reports rather than assuming dispatched tasks succeeded. Over-replication victims are chosen deterministically so concurrent heartbeats cannot each delete their copy. - Three error paths spun at 100% CPU by skipping their sleep; reconnects now use exponential backoff with jitter. - disk_usage measured a hardcoded /home instead of the storage directory, making placement decisions meaningless. - Chunk uploads landed directly on the final filename, so the directory scan could advertise a half-written chunk as a complete replica. They now commit via fsync + rename. - A failed dial was cached as a nil client in the uploader, which would panic on next use. - chunkId is attacker-controlled and was joined straight onto a path; traversal is now rejected. - Fatal startup errors logged and exited 0. - GracefulStop could never complete because heartbeat streams never end; shutdown now falls back to a forced stop. Adds the master flags the other binaries already had, a test suite covering all of the above (the repo had none), CI, and a Makefile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Live testing of the cluster turned up four failure modes that all reported success while losing or corrupting data. This fixes them, plus the higher-severity issues found alongside, and adds the test suite the repo was missing.
Every fix below is pinned by a test that I verified fails against the old behaviour.
The four confirmed data-loss bugs
file does not existforevergettwice appendedgetwith no reachable replica "succeeded"putwrote 1 replica, master recorded 2Metadata durability
The master kept its namespace only in Go maps. A restart left the data on disk but permanently unreachable, and the stranded chunks were never reclaimed — registration would resurrect them into
ms.Chunkswith an emptyFileName, after which nothing ever collected them.This now follows the GFS operation-log design (what HDFS calls
edits+fsimage), with metadata split into two classes:-metaNamespace mutations are
fsync'd before the RPC is acknowledged, so an acknowledged write survives a crash. Checkpoints are published by atomic rename and only bound replay time; the log stays the source of truth. A torn trailing record is discarded on recovery — it was neverfsync'd, so it was never acknowledged.Locations are intentionally not persisted: chunkservers are the only authority on what they actually hold, so storing locations would just yield a stale copy that disagrees with the disks. That is also why a restarted master waits out
-orphan-gracebefore deleting chunks it has no metadata for.The other three
O_APPENDwas applied to chunk 0 as well as 1..N. Downloads now stream into a temp file and rename into place, so a re-run replaces the previous copy and an interrupted run leaves nothing behind.continued past every failure and fell through toreturn nil. Both directions now propagate errors, and the chunker distinguishesio.EOFfrom a real read failure instead of treating every error as end-of-file (which silently truncated uploads).breaks on first success. Background replication did heal it, but left a window at RF=1 that the master could not see. The client now writes every allocated replica before reporting success.Also fixed
ms.muwas held across the blockingstream.SendinHeartbeat, while chunkservers blocked their stream reader when task queues filled (// its fine for now). The response is now built under the lock and sent outside it, and full queues drop tasks rather than back-pressure the stream — the master recomputes outstanding work each heartbeat, so a dropped task is retried, not lost.continued past the sleep; reconnects use exponential backoff with jitter.disk_usage()measured a hardcoded/home, not the storage directory — so co-located chunkservers reported identical figures and storage-aware placement degenerated to an arbitrary choice.fsync+ rename, with.partial-files ignored and cleaned up at startup.chunkIdis attacker-controlled and was joined straight onto a path. Now rejected.GracefulStopcould never complete, because heartbeat streams never end. Shutdown now falls back to a forced stop.NotFound,AlreadyExists,ResourceExhausted, …) instead of barefmt.Errorf, so clients can tell failures apart.GetFileInforeturns chunks in allocation order, removing the client's parse-IDs-and-sort step.-port,-meta,-replication-factor,-live-threshold,-orphan-grace) — it was the only binary with none.Tests
The repo had zero tests; that was the root cause of most of the above. Added ~40 across five suites — unit tests for the metastore, master reconciliation, chunker, and path validation, plus end-to-end tests over real gRPC covering all four data-loss bugs.
Clean under
-race.go test -shortskips anything that binds a port. Also adds CI (build, vet, gofmt,-race) and a Makefile.Not in this PR
Deliberately left for follow-ups, and now recorded under "Known Limitations" in the README: checksums (the protocol field exists but is always zero, so bitrot is undetectable — there's an existing
checksumbranch), client RPC deadlines, delete/list/overwrite, TLS/auth, and opaque chunk IDs.Reviewing
internal/master/metastore/and thereconcilerewrite ininternal/master/server.goare the parts worth the closest look — everything else is comparatively mechanical.🤖 Generated with Claude Code