fix(clients): stop client ops from reverting a renewal committed mid-op

Invariant: an operation on an inbound's clients writes back only what it
changed, onto the settings as committed when it writes. Every client op
(add, edit, delete, bulk adjust/detach/delete/set-enable) read the inbound
outside the serial traffic writer and then tx.Save'd the whole row inside
it. A traffic tick that committed in between - auto-renew re-enabling a
client, the delayed-start conversion, a node adoption - was overwritten
with the stale copy. A renewed neighbour ended up enable=false with its old
expiry in settings while client_traffics said enabled, so the next runtime
rebuild dropped a healthy client nobody had touched. The bulk ops then ran
a full SyncInbound from those stale settings, copying enable=false into the
client record too.

Each site now commits through commitInboundClientSettings: inside the
serialized tx it three-way merges the op's edit (read -> output, per client
and per field) onto the committed settings and updates only the settings
column, which also stops the stale up/down/enable inbound columns being
written back. advancePushedInbound now records what the per-client push
delivered rather than the merged settings, so the node's reconcile-skip
fingerprint cannot claim a renewal it never received.
This commit is contained in:
MHSanaei
2026-09-28 01:59:33 +02:00
parent 15d82a5e47
commit 63ffc083e4
4 changed files with 497 additions and 16 deletions
+6 -4
View File
@@ -795,6 +795,7 @@ func (s *ClientService) bulkAdjustInboundClients(
}
return res
}
prevSettings := oldInbound.Settings
oldInbound.Settings = string(newSettings)
// A flow change rewrites the user's xray config, which the lightweight
@@ -807,7 +808,7 @@ func (s *ClientService) bulkAdjustInboundClients(
// Serialize against the traffic poll to avoid the cross-transaction
// lock-order deadlock on inbounds/client_records (runSerializedTx).
txErr := runSerializedTx(func(tx *gorm.DB) error {
if err := tx.Save(oldInbound).Error; err != nil {
if err := commitInboundClientSettings(tx, oldInbound, prevSettings); err != nil {
return err
}
finalClients, gcErr := inboundSvc.GetClients(oldInbound)
@@ -1112,6 +1113,7 @@ func (s *ClientService) bulkDelInboundClients(
}
return res
}
prevSettings := oldInbound.Settings
oldInbound.Settings = string(newSettings)
foundList := make([]string, 0, len(foundEmails))
@@ -1181,7 +1183,7 @@ func (s *ClientService) bulkDelInboundClients(
// Serialize against the traffic poll to avoid the cross-transaction
// lock-order deadlock on inbounds/client_records (runSerializedTx).
txErr := runSerializedTx(func(tx *gorm.DB) error {
if err := tx.Save(oldInbound).Error; err != nil {
if err := commitInboundClientSettings(tx, oldInbound, prevSettings); err != nil {
return err
}
finalClients, err := inboundSvc.GetClients(oldInbound)
@@ -1780,7 +1782,7 @@ func (s *ClientService) bulkSetEnableInboundClients(inboundSvc *InboundService,
}
txErr := runSerializedTx(func(tx *gorm.DB) error {
if e := tx.Save(oldInbound).Error; e != nil {
if e := commitInboundClientSettings(tx, oldInbound, prevSettings); e != nil {
return e
}
finalClients, gcErr := inboundSvc.GetClients(oldInbound)
@@ -1850,7 +1852,7 @@ func (s *ClientService) bulkSetEnableInboundClients(inboundSvc *InboundService,
}
}
if !pushFailed {
advancePushedInbound(rt, prevSettings, oldInbound)
advancePushedInbound(rt, prevSettings, string(newSettings), oldInbound)
}
}