mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-01 07:57:14 +00:00
fix(node): stop Postgres deadlocks and deleted-client resurrection in node sync
Two defects in the node traffic sync, both hit hard on busy
master+multi-node Postgres deployments:
Client-IP merges deadlocked. Each node syncs on its own goroutine and
shared clients appear in several nodes' reports, but MergeInboundClientIps
and upsertNodeClientIps locked rows in whatever order each node's report
arrived. Two concurrent merges taking the same rows in opposite order is
exactly what Postgres aborts with SQLSTATE 40P01 ("merge client ips from
<node> failed: deadlock detected"). Both merges now process emails in
sorted order so every transaction acquires row locks in one global order.
Deleted clients resurrected with zeroed traffic. A snapshot fetched just
before a deletion still names the deleted email; applying it after the
delete committed re-added the client. The delete tombstone existed for
precisely this race but only zeroed the seed counters: the sync still
recreated the client_traffics row, and worse, adopted the node's stale
settings JSON wholesale, putting the client back in the central inbound
as if it were brand new with 0 traffic. Snapshot application now skips
row creation for tombstoned emails on known inbounds and strips
tombstoned clients from adopted settings; fresh node-adoption semantics
(rows seeded at zero) are unchanged.
The mass-disconnect part of the report is the forced node restart on
auto-disable, removed separately in 4d6f2ddd.
Closes #5739
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -139,3 +140,39 @@ func isClientEmailTombstoned(email string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// stripTombstonedClients drops just-deleted client entries from a node
|
||||
// snapshot's settings JSON so adopting a stale snapshot can't re-add them to
|
||||
// the central inbound while the delete tombstone is live. Returns the filtered
|
||||
// JSON and whether anything was removed.
|
||||
func stripTombstonedClients(settings string) (string, bool) {
|
||||
if settings == "" {
|
||||
return settings, false
|
||||
}
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
|
||||
return settings, false
|
||||
}
|
||||
clients, _ := parsed["clients"].([]any)
|
||||
if len(clients) == 0 {
|
||||
return settings, false
|
||||
}
|
||||
kept := make([]any, 0, len(clients))
|
||||
for _, c := range clients {
|
||||
if cm, ok := c.(map[string]any); ok {
|
||||
if email, _ := cm["email"].(string); email != "" && isClientEmailTombstoned(email) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
kept = append(kept, c)
|
||||
}
|
||||
if len(kept) == len(clients) {
|
||||
return settings, false
|
||||
}
|
||||
parsed["clients"] = kept
|
||||
b, err := json.MarshalIndent(parsed, "", " ")
|
||||
if err != nil {
|
||||
return settings, false
|
||||
}
|
||||
return string(b), true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user