Repository navigation
fix(xray): let a restart abort user syncs stuck on an unresponsive core - #133
Open
Free-Guy-IR wants to merge 4 commits into
Open
Free-Guy-IR wants to merge 4 commits into
Free-Guy-IR wants to merge 4 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
A sync queued on syncMu could take the lock after a restart aborted the running one and wedge the restart again. While a restart waits for syncMu, new syncs now fail fast. The health loop no longer skips its restart when it had to abort a sync, so the core is rebuilt from the current config.
A restart that aborts a running SyncUser or UpdateUsers no longer drops the account from the in-memory config, so the core it rebuilds from that config carries the update. The abort flag is consumed by every restart, and an aborted WireGuard remove-first path reports the abort.
An aborted SyncUser or UpdateUsers now returns a backend.RetryLaterError, so the node answers it as gRPC Unavailable / HTTP 503 instead of Internal / 500, and a client retries the batch instead of splitting it to find refused users. errors.Is(err, errSyncAborted) still holds.
Free-Guy-IR
force-pushed
the
upn/xray-restart-races-w
branch
from
November 7, 2026 19:31
862bb2d to
f9a2f8e
Compare
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.
Fixes #128
Found by code review and reproduced in a lab; never seen in production. Rebased onto dev 689f266;
git merge-treeonto the current dev 9ccd263 (#124) is clean, and the tests below pass on that merge result too.Keeps the health-check restart's
syncMuserialization added in #113.SyncUserandUpdateUsersrun their core API calls under a cancellable context registered on the backend (beginSync,backend/xray/xray.go:40at f9a2f8e).Restart()(backend/xray/xray.go:201) and the health-check restart (backend/xray/jobs.go:100) go throughlockSyncForRestart(backend/xray/xray.go:58): it marks a restart as pending, cancels the running sync, then takessyncMu. While a restart is pending, a sync that reachesbeginSyncis cancelled at once, so a second sync queued onsyncMucannot take the lock after the abort and block the restart again.An aborted sync never reports success. It returns
backend.RetryLater(... user sync aborted by a core restart)(backend/xray/user.go:298,:426), which the node answers as gRPCUnavailableor REST 503. Before this, the same abort was answeredInternal/ 500, and node_bridge_py treatsInternalwith the prefixfailed to update usersas a rejection by the backend and splits the batch (grpclib.py:345-348,controller.py:740at c5632b65). The aborted sync keeps the accounts it was applying in the in-memory config, so the rebuilt core has them.If the health-check restart had to abort a sync, it no longer skips the restart when the core answers the re-probe (
backend/xray/jobs.go:105), so the core and the in-memory config cannot be left disagreeing about the aborted users.Tests
All local runs: macOS arm64, go1.27.3
-race, real Xray 26.3.27 throughXRAY_EXECUTABLE_PATH/XRAY_ASSETS_PATHfor the controller tests. Thebackend/xraytests use a fake core (a gRPC server whoseAlterInboundblocks until its context ends). Base = dev 9ccd263 with the new test files copied in; the two xray tests that nameerrSyncAbortedneed a one-line test-only declaration of it to compile on the base.TestRestartIsNotBlockedByAUserSyncOnAnUnresponsiveCoreTestHealthLoopRestartsAnUnresponsiveCoreWhileAUserSyncIsStuckOnItTestHealthLoopRestartsAnUnresponsiveCoreWithNoUserSyncInFlight(control)TestRestartIsNotBlockedByAUserSyncQueuedBehindAStuckOnesyncMu)TestHealthLoopReplacesTheCoreAfterAbortingAUserSyncEvenWhenTheCoreAnswersAgainTestRestartDuringAUserSyncKeepsTheUserForTheRebuiltCoreTestRestartDuringAUserUpdateAbortsItAsRetryLaterTestGRPC_SyncUsersChunkedAbortedByACoreRestartIsUnavailable(real Xray, SIGSTOP)InternalTestREST_SyncUserAbortedByACoreRestartIsServiceUnavailable(real Xray, SIGSTOP)The two controller tests also fail with
user.gofrom d814391, the commit before the last one (abort withoutRetryLater): gRPC answeredInternal (... user sync aborted by a core restart), REST answered 500. Two tests also fail when only their own mechanism is removed: the queued-sync test with 0dfc854'sxray.go,user.goandjobs.go(abort, but no pending-restart guard), and the recovered-core test with only the!abortedcheck removed fromjobs.go:105.Linux CI on this head: the Test Modules run 42406639575 (job
test; it tests the merge refpull/133/merge72274b47013c, i.e. f9a2f8e merged withdev; linux/amd64, go1.27.3, stepTEST_INTEGRATION=true go test ./... -v -p 1without-race, Xray 26.10.31) passes all nine:With the real bridge
Real node, real Xray 26.3.27 frozen with SIGSTOP, node_bridge_py c5632b65 calling
_sync_isolating_rejectionswith 8 users and its defaultinternal_timeoutof 15 s; the sync starts 40 s after the freeze, so the health loop reaches its 10th failed probe during the sync:Unavailable ... user sync aborted by a core restartafter 9.9 s, in the same second asrestarting Xray core...; a new xray process was running 2 s laterRetryLater)Internal ... user sync aborted by a core restartafter 10.0 scontext canceledper user, and the health loop replaced the core 3 s later, after its re-probeThe rows for this PR and for dev were each run twice with the same result.
Known limits
ApplyConfigLive(backend/xray/live.go:51) still holdssyncMuwith the caller's context and does not register withbeginSync(read from the code).SyncUserstream, the abort ends the stream, so users sent later on the same stream are not applied; the caller has to resend them (read from the code).