mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-15 17:16:07 +00:00
fix(inbound): defer a local MTProto inbound edit's sidecar push until after commit
UpdateInbound applied a local MTProto inbound change by calling the runtime UpdateInbound (which stops/starts the mtg sidecar or talks to it) from inside runSerializedTx. That runs process and network I/O on the single traffic-writer goroutine while a DB transaction is open, so a slow sidecar stalls traffic accounting and every concurrent client mutation, and a later step failing the transaction leaves the sidecar ahead of the rolled-back row. Move the push into the post-commit hook, matching the xray branch. Adds a SetLocalRuntimeOverride test seam mirroring the existing node override so the deferral is regression tested.
This commit is contained in:
@@ -18,6 +18,7 @@ type Manager struct {
|
||||
mu sync.RWMutex
|
||||
remotes map[int]*Remote
|
||||
overrides map[int]Runtime // test-only: forces RuntimeFor to return a stub
|
||||
localOverride Runtime // test-only: forces RuntimeFor(nil) to return a stub
|
||||
egressResolver NodeEgressResolver
|
||||
}
|
||||
|
||||
@@ -44,6 +45,15 @@ func (m *Manager) SetRuntimeOverride(nodeID int, rt Runtime) {
|
||||
m.overrides[nodeID] = rt
|
||||
}
|
||||
|
||||
// SetLocalRuntimeOverride makes RuntimeFor(nil) return rt instead of the real
|
||||
// local runtime. Test seam for exercising the local dispatch path (MTProto
|
||||
// sidecar, local Xray) without a running child process; pass nil rt to clear.
|
||||
func (m *Manager) SetLocalRuntimeOverride(rt Runtime) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.localOverride = rt
|
||||
}
|
||||
|
||||
func (m *Manager) SetNodeEgressResolver(r NodeEgressResolver) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
@@ -61,6 +71,13 @@ func (m *Manager) NodeEgressProxyURL(nodeID int) string {
|
||||
|
||||
func (m *Manager) RuntimeFor(nodeID *int) (Runtime, error) {
|
||||
if nodeID == nil {
|
||||
m.mu.RLock()
|
||||
if m.localOverride != nil {
|
||||
rt := m.localOverride
|
||||
m.mu.RUnlock()
|
||||
return rt, nil
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
return m.local, nil
|
||||
}
|
||||
m.mu.RLock()
|
||||
|
||||
@@ -1303,13 +1303,16 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
pushable = false
|
||||
}
|
||||
}
|
||||
newProtocolIsMtproto := oldInbound.Protocol == model.MTProto
|
||||
if pushable {
|
||||
if err2 := rt.UpdateInbound(context.Background(), &oldSnapshot, payload); err2 == nil {
|
||||
logger.Debug("Updated inbound applied on", rt.Name(), ":", oldInbound.Tag)
|
||||
} else {
|
||||
logger.Debug("Unable to update inbound on", rt.Name(), ":", err2)
|
||||
if oldInbound.Protocol != model.MTProto {
|
||||
needRestart = true
|
||||
postCommitApply = func() {
|
||||
if err2 := rt.UpdateInbound(context.Background(), &oldSnapshot, payload); err2 == nil {
|
||||
logger.Debug("Updated inbound applied on", rt.Name(), ":", oldInbound.Tag)
|
||||
} else {
|
||||
logger.Debug("Unable to update inbound on", rt.Name(), ":", err2)
|
||||
if !newProtocolIsMtproto {
|
||||
needRestart = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
|
||||
)
|
||||
|
||||
// A local MTProto inbound edit must not push to the managed sidecar from inside
|
||||
// the serialized write transaction: that blocks the single traffic-writer
|
||||
// goroutine on process/network I/O, and a later step failing the transaction
|
||||
// would leave the sidecar ahead of the rolled-back database. The push belongs in
|
||||
// the post-commit hook, exactly as the xray branch already does it.
|
||||
func TestUpdateInboundLocalMtprotoDefersPushUntilCommit(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
|
||||
mgr := runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }})
|
||||
fake := &fakeNodeRuntime{}
|
||||
mgr.SetLocalRuntimeOverride(fake)
|
||||
runtime.SetManager(mgr)
|
||||
t.Cleanup(func() { runtime.SetManager(nil) })
|
||||
|
||||
seedInboundConflict(t, "mt-txfail", "", 46150, model.MTProto, "",
|
||||
`{"clients":[{"email":"mtx","secret":"`+mtprotoTestSecretA+`","enable":true}]}`)
|
||||
seeded := loadInboundByTag(t, "mt-txfail")
|
||||
seedClientTraffic(t, seeded.Id, "mtx", true)
|
||||
|
||||
db := database.GetDB()
|
||||
const cbName = "b1-05:fail-inbound-update"
|
||||
if err := db.Callback().Update().After("gorm:update").Register(cbName, func(tx *gorm.DB) {
|
||||
if tx.Statement != nil && tx.Statement.Table == "inbounds" {
|
||||
tx.AddError(errors.New("injected transaction failure"))
|
||||
}
|
||||
}); err != nil {
|
||||
t.Fatalf("register callback: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Callback().Update().Remove(cbName) })
|
||||
|
||||
update := *loadInboundByTag(t, "mt-txfail")
|
||||
update.Remark = "edited"
|
||||
if _, _, err := (&InboundService{}).UpdateInbound(&update); err == nil {
|
||||
t.Fatal("UpdateInbound: expected the injected transaction failure")
|
||||
}
|
||||
|
||||
if n := fake.updateInbound.Load(); n != 0 {
|
||||
t.Fatalf("the MTProto sidecar push ran %d time(s) inside the failed transaction; it must be deferred until the commit succeeds", n)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user