Skip to content

fix(cnstore): require instance-bound lock drain before CN exit - #624

Open
VioletQwQ-0 wants to merge 16 commits into
matrixorigin:mainfrom
VioletQwQ-0:codex/cn-safe-drain-operator-20260923
Open

VioletQwQ-0 wants to merge 16 commits into
matrixorigin:mainfrom
VioletQwQ-0:codex/cn-safe-drain-operator-20260923

Conversation

@VioletQwQ-0

@VioletQwQ-0 VioletQwQ-0 commented Sep 23, 2026 •

Copy link
Copy Markdown

What type of PR is this?

  • API-change
  • BUG
  • Improvement
  • Documentation
  • Feature
  • Test and CI
  • Code Refactoring

Which issue(s) this PR fixes:

Related: #623 (no automatic closure). Companion MatrixOne PR: matrixorigin/matrixone#29291, merged at 5e965dece9ea15fa5d73c9e9615c59fbde651aaa.

What this PR does / why we need it:

A CN with no client sessions may still host locks needed by another CN. A UUID/boolean restart check cannot establish that its response belongs to the current process. This PR queries the running CN for its full lock-service ID, uses BeginDrain/QueryDrain with instance, attempt and allocator epoch, and binds completion to Pod UID, CN UUID, main-container ID/start time, lifecycle and upgrade revision.

The durable Prepared → Requesting → Requested → CompletionAuthorized attempt is validated using fresh API reads and optimistic concurrency. Direct DELETE carries UID/resourceVersion preconditions; delete failures and controller restarts preserve authorization only for the same instance. Upgrade recovery requires the expected revision, new main container and fresh health observation before business admission resumes. Missing identity, unsupported protocol, cancellation and instance changes retain protection. There is no legacy UUID boolean fallback or automatic force-delete.

The drain deadline does not stop observing the same attempt. Identity discovery uses the MO IdentityOnly response, not lock enumeration. Allocator epoch loss remains fail-closed: establish a current compatible instance and a fresh valid attempt through the supported recovery path; do not clear annotations, force-delete or re-admit a drained process to bypass the guard.

Current dependency and setup update (3d8a27f8fdae971fa66c8c8d6a8186b186635c80):

  • Directly requires upstream github.com/matrixorigin/matrixone v0.7.1-0.20261004020908-5e965dece9ea. The personal-fork replacement has been removed. This is the immutable merged companion commit, not moving main. Native libraries are rebuilt from that exact module. MORPC drain methods require v102; upstream v101 remains the JSON capability.
  • Previous e2e run stopped after the Kruise hook pull with Docker unauthorized, before lifecycle assertions. Its logs did not identify the failing command. Independent anonymous manifest reads of the configured Quay MinIO tag fail locally and on mo-55.
  • kind now builds the same RELEASE.2023-11-01T01-57-10Z MinIO source at official commit 55e713db0a367f6cccee00af49f00c269e6ca619, instead of requiring that inaccessible registry image or silently upgrading MinIO. The build includes the upstream license. Only the kind fixture uses the local image; standalone examples remain unchanged.
  • Image preparation explicitly logs pull/export/load boundaries. Platform-scoped archive export remains in place. Errors are propagated without retry or success masking. Shell regression covers MinIO build failure, image export/load failure, and installation of the real multi-document fixture including its Service and Secret.

Validation at the current candidate:

  • PASS: exact-module native build; nonempty drain/upgrade test selection; all ./pkg/... tests; full cnstore/cnclaim ordinary and CGO_ENABLED=1 -race -count=1 tests; mocli/querycli compilation.
  • PASS: real envtest TestDrainRealAPIConcurrency and TestUpgradeRecoveryRealAPIConflicts in ordinary and race runs, not skipped (Kubernetes 1.24.1 API server).
  • PASS: separate API-module ordinary/race tests with GOWORK=off; relevant vet; manager build and --help; shell regression, syntax and ShellCheck; tidy/diff checks; full GOWORK=off make verify, including generated/chart and clean-tree checks.
  • Local toolchain: Go 1.26.4, macOS arm64. Exact-module cgo/libmo.a SHA-256: d60c29ef4ca45d850f5786cca55e090f85650150e0395b2b97fa6025eac7e695.
  • Raw commands and output are retained under evidence/operator-upstream-20261008.vRrnpW/ in the task workspace. A real MinIO image build on mo-55 was stopped after roughly eight minutes of stalled public base-image downloads, before MinIO compilation (BLOCKED_ENVIRONMENT, not Docker/kind PASS). Only the task-owned build process was stopped; shared containers and the daemon were not changed. Fresh-head checks/e2e must complete before claiming CI PASS.
  • BVT: N/A for this incremental dependency/setup delta. Shell regression exercises the actual helper and fixture; controller/envtest regression covers API concurrency. Static SQL cannot validate Kubernetes eviction timing.

Special notes for your reviewer:

QA required: yes. MO merge is complete, but merge and package tests do not establish rollout, eviction acceptance or QA. Deploy compatible CN/TN binaries before this Operator; old combinations without instance-level proof must block normal eviction.

Baseline CI e2e still runs MO 1.2.3. It is environment/controller coverage, not acceptance of this new drain protocol. Candidate-binary Kubernetes/Kruise controlled eviction, mixed-version binaries, unit-agent/cloud node release, shared-dev deployment and QA remain separate NOT_RUN gates. A live Pod object alone does not prove the protected main container remains running. Direct external DELETE/Eviction is not converted into safe drain by OnDeleted.

Earlier fixture/proof regressions are retained: valid Requested/Requesting records and unchanged-instance positive controls precede identity mutation; mismatched BeginDrain identity cannot persist Requested or authorize later QueryDrain. Previous package, process-level two-CN and pinned-fork results remain historical evidence, not exact-current integration results.

Additional documentation (e.g. design docs, usage docs, etc.):

Protocol: matrixorigin/matrixone#29291. Historical failure and cross-layer boundary: #623. Failed FULLTEXT2 runs and their databases are retained; none is resumed or combined with these results.

@VioletQwQ-0
VioletQwQ-0 force-pushed the codex/cn-safe-drain-operator-20260923 branch from 7afa077 to 962ddb4 Compare September 23, 2026 11:42
@VioletQwQ-0
VioletQwQ-0 marked this pull request as ready for review September 24, 2026 06:22
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@XuPeng-SH XuPeng-SH left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design and recovery review at head 02fbe0e38702bf8af304ec4ad721571cc73eb0b6.

[P1] A drain that exceeds StoreDrainTimeout cannot complete automatically, even after its dependencies disappear. OnPreparingStop reloads the persisted start timestamp and returns at controller.go:632-635 before handleConnectionDraining or handleLockMigration. For an attempt still in Requested when a remote transaction passes the deadline, every subsequent reconcile repeats that return. The transaction can later commit and QueryDrain can become safe, but the controller never asks again; the timeout test even asserts that this path makes no lock RPC. Retaining deletion protection is correct. Keep observing completion after the deadline while alerting on the overdue attempt, or define an explicit recoverable transition. Add a deterministic test: held remote transaction -> deadline -> commit -> same Pod/attempt eventually authorizes completion without unsafe deletion.

[P2] GetLockServiceIdentity uses GetLockInfo solely to read the exact service ID (pkg/querycli/query.go:82-95). The CN handler always enumerates all local locks and copies their keys, holders, and waiters (matrixone/pkg/cnservice/server_query.go:390-425); IterLocks holds each local lock-table read lock while invoking that callback (matrixone/pkg/lockservice/service_observability.go:193-224). Each drain identity probe therefore costs work proportional to current locks and can contend with lock mutations. Please add a lightweight identity-only response path, preserving existing GetLockInfo behavior, and validate that it avoids lock enumeration.

The instance/attempt/allocator proof and persisted phase are sound safety boundaries. The recovery and deployment contract still needs to be explicit: a Requested attempt keeps its old allocator epoch after allocator replacement, so repeated QueryDrain cannot complete; the new Operator also requires a LockServiceID unavailable from old CN binaries. Document either a safe re-handshake or an operational recovery procedure, plus a mixed-version rollout order that does not rely on the legacy UUID boolean check. The current PR evidence does not cover real Operator/Kruise eviction or that version matrix. These are not reasons to relax the fail-closed deletion rule.

@VioletQwQ-0

Copy link
Copy Markdown
Author

@XuPeng-SH CR update at 5d33fe3: StoreDrainTimeout now keeps deletion protection while continuing to observe the same drain attempt; a multi-round Observe regression covers a held remote transaction that commits after the deadline. GetLockServiceIdentity requests the new MO IdentityOnly response so CN identity lookup does not enumerate locks. The PR body now states fail-closed allocator-epoch recovery and MO-first mixed-version rollout; no UUID fallback or automatic force-delete was added. The dependency is pinned to companion MO 06bcdb8b2742. Local controller compilation is blocked by missing matching native headers; CI is running. Please review the code and the explicit remaining integration gates.

@VioletQwQ-0

Copy link
Copy Markdown
Author

Follow-up at 96aa8c6: the overdue-drain controller regression now accounts for the diagnostic RemainTxnCount call on the unsafe QueryDrain round, then verifies the same attempt reaches completion with a second QueryDrain after the held transaction releases. This changes only the test expectation; deletion protection and request order remain asserted. GOWORK=off cnstore package tests and the focused CGO_ENABLED=1 race test pass locally. New CI is running; the prior e2e job stopped while loading the OpenKruise hook image (unauthorized), before lifecycle tests. XuPeng-SH: the timeout and identity-only changes from the previous update remain ready for re-review; controlled cloud eviction and mixed-version rollout remain separate gates.

@VioletQwQ-0

Copy link
Copy Markdown
Author

Exact head 058e81a: checks is PASS after the test expectation and go.sum tidy fixes. e2e remains BLOCKED_ENVIRONMENT: three consecutive runs (jobs 108772913054, 108814042049, 108816252542) stop at kind load docker-image for openkruise/kruise-helm-hook:v0.1.0 with Docker unauthorized, after the host pull succeeds and before any Operator lifecycle assertions. Please have the CI/image-store owner inspect this kind/Docker load path; I have not weakened the test or claimed e2e passed. The requested code re-review can proceed independently, but merge/deployment validation still needs a real e2e run.

XuPeng-SH added a commit to matrixorigin/matrixone that referenced this pull request Oct 4, 2026
## What type of PR is this?

- [x] API-change
- [x] BUG
- [ ] Improvement
- [x] Documentation
- [ ] Feature
- [x] Test and CI
- [ ] Code Refactoring

## Which issue(s) this PR fixes:

Kernel-side work for #29477; keep the issue open for Operator/cloud
acceptance.
Related: matrixorigin/matrixone-operator#623. Companion:
[matrixone-operator#624](matrixorigin/matrixone-operator#624).

## What this PR does / why we need it:

A CN with no client sessions can still serve locks held by transactions
on another CN. An allocator lookup miss, a UUID matching a different
incarnation, or a stale restart response must not authorize eviction.
BeginDrain/QueryDrain bind completion to the exact lock-service
incarnation, drain attempt, and allocator identity/epoch. Missing or
mismatched proof fails closed; completion requires the CN's existing
local drain checks and terminal allocator observation.

The protocol preserves existing admission and cleanup ownership. Invalid
allocator bindings are rejected before publication, allocation waiters
are released on failure, and retries cannot reopen drain admission.
After allocator state loss, a pending non-admitting binding requires a
matching-epoch heartbeat; an already-draining CN can recover without
returning to Enable. Terminal heartbeats containing active transactions
remain rejected. Retired proofs are bounded and retries are idempotent.
Transient timeout validation failures no longer leave an idle draining
CN stuck behind negative retirement: BeginDrain reuses the pending
non-admitting handshake, clears only obsolete retirement metadata, and
preserves inactive-service and commit fences. Positive retirement proofs
remain idempotent. IdentityOnly fetches the exact identity without
enumerating locks; existing requests retain their lock-list behavior.

### Rebase and deployment contract

Rebased onto main `f1137a6cac72852be2ab2e5078b684b98f23d96c`. The nine
ordinary commits were replayed separately; merge-only recovery
documentation and protocol guards were preserved explicitly. Drain RPC
methods remain 23/24 and now require MORPC **v105**, preserving main's
v102 vector-cache control, v103 distributed IVF PRE, and v104 typed JSON
behavior. Generated bindings preserve upstream query cache fields,
LockOptions.KeepRows, and the removed Result trace fields' reserved
numbers 6–9. Generation uses the repository's lock gogofast/query
gogofaster modes and is reproducible byte for byte.

Acceptance is a **coordinated downtime upgrade**: stop CN/TN services,
deploy compatible binaries, restart them, then enable the matching
Operator client. Mixed-version and rolling upgrades are outside this
release's acceptance scope. Version rejection and fail-closed proof
handling still apply. This PR does not update the companion Operator's
dependency pin; integration must use the rebased kernel revision.

### Prior rebase validation at `3856a3dae1`

- Fresh native `make cgo` build passed.
- Full normal tests passed for lockservice (90.934s), cnservice
(9.776s), lockop (5.334s), pb/query, and defines, with `-short -tags
matrixone_test -count=1 -vet=all`.
- pb/lock has no package tests; it compiled successfully and its schema
behavior passed consumer tests and the external wire QA.
- Focused real RPC/instance-bound proof, allocator-loss recovery, stale
admission, and remote-holder tests passed; capability tests reject
versions 99–104 and accept v105.
- Six-package go vet and repository-config golangci-lint, `make
err-check`, and `git diff --check` passed.
- External wire QA passed: existing vector-cache request fields coexist
with IdentityOnly; old trace tags decode safely; reserved fields and
KeepRows survive generation.

- Full lockservice race tests passed (97.786s). Both changed capability
tests also passed 100 repetitions each under race.
- Separately configured **gpt-6.1-sol / xhigh** design and final overall
review passed. Final reviewed revision:
`3856a3dae1eaf7ff4d644d48d7ed5cdfc632b3d2`; no unresolved material
blocker. Review reconciled complete ownership/state/wire/test closures
and independently checked original-history and upstream preservation.

### Negative-retirement recovery at `b559459d73`

The original allocator implementation fails the new real two-CN
negative-retirement regression; allocator-loss is the passing control.
The correction changes only BeginDrain recovery and adds no normal
lock-path work, worker, or retry loop. Existing recovery tests now cover
both failure modes, including held remote locks, stale heartbeats,
non-admission, old-generation cleanup, retained named/persistent commit
fences, and terminal positive-proof retries. A duplicate phase test and
ordinary SQL atomicity cases that never invoked drain were removed;
actual drain coverage remains in real RPC/two-CN tests.

- Fresh native build and full lockservice normal tests passed (89.977s);
final strengthened recovery matrix passed (0.275s).
- Full lockservice race passed (90.581s). Recovery handshake and real
remote-holder recovery each passed 100 exact race repetitions (7.823s /
22.807s).
- Final go vet, repository-config golangci-lint v2.6.2, make err-check,
and diff checks passed.
- Separately configured gpt-6.1-sol / xhigh design and final overall
review passed at `b559459d734c0a0e2c38fb2ed5b7dbc2ae399014`; no
unresolved material blocker.

### CI upgrade-test fixture correction at `713f4bd3de`

The previous CI run passed SCA, shared build, and multi-CN BVT, but
failed `TestV408LoginRejectsAccountDroppedAfterAuthentication`: DROP
ACCOUNT legitimately closed the borrowed authenticated session, and the
test called MaybeUpgradeTenant after its owner had destroyed it. The
test and frontend code were unchanged from the frozen PR base. This
correction retains real AuthenticateUser, captures authenticated scalar
values before DROP, and checks compensation twice through its stable
real CN owner, CheckTenantUpgrade. Existing wrapper UT covers version
gating/error propagation. Both ErrNotFound results, no successful-cache
publication, and independent CN SQL liveness remain required.
Query-registry removal before tenant mutation remains; account routine
cleanup is unchanged. No production nil guard, retry, sleep, hook, or
normal-query overhead is added.

- The three related V408 integration tests passed normally (10.622s);
the previously failing account-drop test passed under race (22.308s).
- Incremental upgrade-package vet/lint (0 issues), make err-check, and
diff checks passed.
- Separately configured gpt-6.1-sol / xhigh design and final overall
review passed at `713f4bd3de8f58eb24cbd6172a2b28b37906ba9c`. Drain
implementation and its prior acceptance evidence remain unchanged.
- Initial local startup attempts encountered an overlong Unix socket
path; only the external test wrapper TMPDIR was shortened before the
successful source-bound runs. No production change was made for that
environment error.

The current evidence is local Linux kernel validation. Live SQL
distributed cases, real process-restart deployment, Operator/Kruise
eviction, and cloud control-plane acceptance were not rerun for this
rebase. They remain separate integration gates; earlier experiments on
previous heads are not presented as current-head proof. GitHub CI will
run on the pushed revision and is not claimed as passed.

---------

Co-authored-by: XuPeng-SH <xupeng3112@163.com>

This branch has not been deployed

No deployments
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