mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-11 15:16:07 +00:00
fix(sync): mark node dirty inside the mutation transaction (atomic ConfigDirty) (#5611)
* fix(sync): mark node dirty inside the mutation transaction ConfigDirty is currently set by MarkNodeDirty AFTER the mutation, on a separate DB handle outside the mutation's transaction. A crash or error between the committed change and the mark leaves a committed config change that never reconciles to the node (silent drift). Add MarkNodeDirtyTx(tx, id) and call it inside each mutation's transaction so the dirty mark commits atomically with the change. * fix(test): initialize DB in TestResolveInboundAddress and group gorm import Two CI failures on this branch: - race (-shuffle=on): TestResolveInboundAddress reaches resolveInboundAddress -> configuredPublicHost -> GetSubDomain, which reads the global DB. The test never initialized one, relying on another sub-package test to do so first; under shuffle it ran first and nil-dereferenced gorm. Call initSubDB(t) so it is self-sufficient (empty DB yields an empty subDomain, so the subscriber-host fallback still holds). - golangci goimports: gorm.io/gorm was grouped with the github.com/mhsanaei/3x-ui local imports in node_dirty_test.go. Move it into the third-party group.
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
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"
|
||||
@@ -145,6 +148,43 @@ func TestNodeDirty_ClearIsCASOnDirtyAt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkNodeDirtyTxRollsBackWithTransaction(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
db := database.GetDB()
|
||||
|
||||
node := &model.Node{Name: "n3", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true, Status: "online"}
|
||||
if err := db.Create(node).Error; err != nil {
|
||||
t.Fatalf("create node: %v", err)
|
||||
}
|
||||
|
||||
nodeSvc := NodeService{}
|
||||
rollbackErr := errors.New("force rollback")
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := nodeSvc.MarkNodeDirtyTx(tx, node.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
return rollbackErr
|
||||
}); !errors.Is(err, rollbackErr) {
|
||||
t.Fatalf("rollback tx: got %v want %v", err, rollbackErr)
|
||||
}
|
||||
if _, _, dirty, _, err := nodeSvc.NodeSyncState(node.Id); err != nil {
|
||||
t.Fatalf("NodeSyncState after rollback: %v", err)
|
||||
} else if dirty {
|
||||
t.Fatal("dirty flag escaped a rolled-back transaction")
|
||||
}
|
||||
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
return nodeSvc.MarkNodeDirtyTx(tx, node.Id)
|
||||
}); err != nil {
|
||||
t.Fatalf("commit tx: %v", err)
|
||||
}
|
||||
if _, _, dirty, _, err := nodeSvc.NodeSyncState(node.Id); err != nil {
|
||||
t.Fatalf("NodeSyncState after commit: %v", err)
|
||||
} else if !dirty {
|
||||
t.Fatal("dirty flag should commit with its transaction")
|
||||
}
|
||||
}
|
||||
|
||||
// Editing a node must mark it config-dirty so the next traffic-sync tick
|
||||
// reconciles (pushes the panel's inbounds to the remote) before pulling a
|
||||
// snapshot. Without the dirty flag, re-pointing a node to a fresh server
|
||||
|
||||
Reference in New Issue
Block a user