Skip to content

feat: add "reset wal" to empty an instance's WAL - #535

Open
tinswzy wants to merge 4 commits into
milvus-io:mainfrom
tinswzy:feat/clear-wal
Open

feat: add "reset wal" to empty an instance's WAL#535
tinswzy wants to merge 4 commits into
milvus-io:mainfrom
tinswzy:feat/clear-wal

Conversation

@tinswzy

@tinswzy tinswzy commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Closes #534

Adds reset wal, which deletes an instance's WAL data and metadata so the
instance can be restarted onto an empty WAL. It is the missing first half of the
cold-switch flow: reset checkpoint rewrites the positions, but until now nothing
could establish the precondition both it and cold restore assume — that the WAL is
actually empty.

Only woodpecker is supported. The other WALs are external services with their own
tooling, and the command rejects them by name rather than pretending to handle them.

Usage

reset wal --minio-address <host> --minio-bucket <bucket> --minio-root-path <milvus's minio.rootPath> --storage-type service

Dry-run by default; --run=true executes. Run it before reset checkpoint.

Milvus must be stopped, but the WAL service, etcd and object storage must stay
up.
The log-store nodes are what delete their own local data — with woodpecker
scaled to zero the client cannot resolve any node and the delete fails closed.

What it does

  1. GetAllLogs → prints the logs it is about to remove
  2. DeleteAllLogsSync → per log: fence the nodes, delete objects by prefix, delete
    metadata last
  3. ClearMetaExceptLogIdGen → clears the instance's woodpecker metadata

Why the log id counter is preserved

logId appears in the data paths of both storage tiers, and residue cannot be
ruled out in a distributed system — a node that is permanently down, a segment
whose metadata was already truncated so its node is not in the delete fan-out at
all, a node decommissioned between write and delete. "All historical files are
gone" is not a guarantee this system can offer, so correctness rests on an
invariant instead: logId never goes backwards. Wiping the counter would restart
at logId=1 and let a new log write into a previous one's directory.

The /wp suffix, and why a wrong root is dangerous

Object storage roots at {minio.rootPath}/wp — the suffix Milvus appends
unconditionally in pkg/streaming/walimpls/impls/wp/builder.go. The command
derives it rather than asking for the final path, because the obvious thing to
paste into --minio-root-path is the rootPath from milvus.yaml, and that value
is wrong by exactly this suffix.

A wrong root does not fail loudly. The node's local reclaim is an os.RemoveAll on
a path that simply does not exist, and the object prefix lists empty, so every
tier reports success while all of the data survives
. This was not hypothetical —
the first E2E run hit it, reported "deleted 16 log(s)", and left all 16 data.log
files untouched on every node.

Two things guard it now. The dry-run prints the resolved prefix
(milvus-bucket/file/wp/{logId}/) so it can be confirmed by eye, and the run
reports what each tier actually removed:

deleted 1 log(s): 11 object(s), 3 of 3 node fence(s) had local data

When a clear deletes metadata but finds no data anywhere, it says so:

deleted 17 log(s): 0 object(s), 0 of 51 node fence(s) had local data

WARNING: no data found on any tier. If this WAL was not already empty,
         check --minio-bucket and --minio-root-path: milvus-bucket/WRONGPATH/wp

That is a warning rather than a failure on purpose: an already-empty WAL looks
identical, and failing would break the idempotent re-run this command depends on.

Verification

go test ./states/etcd/reset/... covers WAL-type rejection, that metadata roots at
the instance rather than the meta path, storage-type handling, and the /wp
derivation.

End to end on minikube — milvus v3.0.0, chart 5.0.25, three-node woodpecker
service deployment, etcd and minio in-cluster.

Reaching all three tiers took some setup worth recording. Segments only reach
object storage after the client auditor compacts them, and milvus v3.0.0 embeds
woodpecker client v0.1.33, which calls the legacy CompactSegment RPC that current
servers reject (legacy CompactSegment RPC is disabled; upgrade the client to use CompactSegmentWithExpected, segmentsCompacted=0). With a stock milvus image,
object storage stays empty and that tier is never exercised at all — the first
round of this E2E passed without ever deleting a single object. Data was
subsequently written through woodpecker's own client to produce genuine
footer.blk / m_0.blk objects in the real layout.

With all three tiers populated:

step result
baseline 119 etcd keys, 46 objects, 68 data.log per node × 3, logidgen=49
stop milvus (woodpecker up)
reset wal --run=true 17 logs deleted; etcd → 4 keys; objects → 0; local → 16 per node; logidgen still 49
re-run deleted 0 log(s), counter unchanged — idempotent
reset checkpoint --target-wal woodpecker 5 segment keys, 1 channel-cp, 16 consume-checkpoints, querycoord target cache dropped
restart milvus 10/10 pods ready, 0 restarts
read 52000 rows intact, sampled ids byte-identical, ANN search works
write +500 rows on the empty WAL, reads back 52500
counter logidgen 49 → 65, never returned to 1

The 16 data.log files left behind on each node are orphans from the first,
misdirected run: their metadata was already deleted, so nothing can enumerate them.
They are the concrete cost of the failure mode described above.

A wrong-bucket run was also exercised: object cleanup failed and the log's metadata
was deliberately kept for retry (keeping metadata for retry) rather than
deleted, so the log stays enumerable instead of becoming an orphan.

Dependency note

go.mod currently points at a woodpecker pseudo-version carrying
zilliztech/woodpecker#281, which adds the synchronous delete, ClearMeta, and the
per-tier counts this command reports. This is temporary, to make the change
reviewable and verifiable as a whole.
Once #281 merges I will repoint this at a
released woodpecker version; please do not merge before then.

🤖 Generated with Claude Code

@sre-ci-robot
sre-ci-robot requested a review from congqixia August 31, 2026 05:08
@sre-ci-robot

Copy link
Copy Markdown
Collaborator

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: tinswzy
To complete the pull request process, please assign congqixia after the PR has been reviewed.
You can assign the PR to them by writing /assign @congqixia in a comment when ready.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@mergify

mergify Bot commented Aug 31, 2026

Copy link
Copy Markdown

@tinswzy Thanks for your contribution. Please submit with DCO, see the contributing guide https://github.com/milvus-io/milvus/blob/master/CONTRIBUTING.md#developer-certificate-of-origin-dco.

@mergify mergify Bot added the needs-dco label Aug 31, 2026
"Switch a stopped instance onto a different WAL" and "restore an instance from a
cold backup" both assume the WAL is already empty, and nothing in the operator's
toolbox could establish that. reset wal fills the gap for woodpecker: it deletes
every log's data across both storage tiers, then clears the instance metadata.

The log id counter is deliberately preserved. logId appears in the data paths of
both tiers, and residue cannot be ruled out in a distributed system, so a wiped
counter would let a new log write into a previous one's directory. Monotonic
logId is the invariant that makes reuse safe; ClearMetaExceptLogIdGen keeps it.

Milvus must be stopped, but the WAL service, etcd and object storage must stay
up: the log-store nodes are what delete their own local data.

Object storage roots at "{minio.rootPath}/wp", the suffix Milvus appends
unconditionally when it builds woodpecker's config. The command derives it rather
than asking for the final path, because the value an operator would paste from
milvus.yaml is wrong by exactly that suffix — and a wrong root fails silently: the
node's local reclaim is an os.RemoveAll on a path that does not exist and the
object prefix lists empty, so every tier reports success while all of the data
survives.

Verified end to end on minikube against milvus v3.0.0 with a three-node woodpecker
service deployment: 1500 rows seeded and flushed, milvus stopped, reset wal then
reset checkpoint, milvus restarted. Node-local data.log files for all 16 logs were
removed on all three nodes, the log id counter held at 32 across the clear, no pod
restarted, existing rows read back intact and new writes landed on the empty WAL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: tinswzy <zhenyuan.wei@zilliz.com>
@mergify

mergify Bot commented Aug 31, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

tinswzy and others added 3 commits August 31, 2026 16:06
A clear that removes nothing looked exactly like a clear that had nothing to
remove: the node's reclaim is an os.RemoveAll on a path that may not exist and the
object prefix simply lists empty, so a wrong --minio-bucket or --minio-root-path
produced a confident "deleted N log(s)" while every byte survived.

woodpecker now returns per-tier counts, so the command prints what it actually did
and calls out the case where it deleted metadata but found no data anywhere. That
is a warning rather than a failure: an already-empty WAL looks identical, and
refusing would break the idempotent re-run the command depends on.

Also corrects the dry-run hint, which still told the operator to stop the WAL
service. The service must stay up — its nodes are what delete their own local data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: tinswzy <zhenyuan.wei@zilliz.com>
Picks up the verbatim object prefix, the deleting gate surviving a synchronous
delete, the retryable cleanup-storage initializer, the sync_applied handshake, and
the parked-log object sweep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: tinswzy <zhenyuan.wei@zilliz.com>
A wrong --meta-prefix was the one argument that produced a confident success with
nothing done. Every enumeration is scoped to the prefix, so a wrong one listed no
logs, deleted nothing, re-seeded the instance-level keys somewhere harmless, and
printed "wal is empty. Next: reset checkpoint". An operator following that
instruction rewrites every position to earliest against a WAL that still holds all
of its data — the exact corruption this command exists to prevent. Verified on a
live deployment: with --meta-prefix WRONGPREFIX the run reported success while all
52 metadata keys and 92 objects survived untouched.

The command now probes the version key before doing anything. ClearMeta re-seeds
that key, so a re-run of a completed clear still passes and the idempotency
contract holds; an instance that never started woodpecker has no key either, and
refusing there is right too, since there is no WAL to clear.

Metadata at the legacy top-level "woodpecker" prefix — which woodpecker itself
falls back to — is reported as such rather than as missing, because the fix differs:
point --meta-prefix at it instead of concluding there is no WAL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: tinswzy <zhenyuan.wei@zilliz.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add reset wal to clear a stopped instance's WAL data and metadata

2 participants