Преглед изворни кода

fix(node): don't stamp InboundsAdoptedAt when the sync adopted nothing (#6284)

* fix(node): don't stamp InboundsAdoptedAt when the sync adopted nothing

Onboarding a node in selected mode with an empty tag list empties the
traffic snapshot via FilterNodeSnapshot before the merge sees it, so
the first clean sync adopts nothing — yet syncOne stamped
InboundsAdoptedAt regardless. The flag is documented as the first
clean sync that imported the node's pre-existing inbounds; stamping it
in this state arms the reconcile sweep (gated on the flag since
200ea091, the fix for #5898) to delete the node's pre-existing
inbounds on their next real sync: registering first and choosing tags
afterwards destroyed the node's inbounds.

Gate the stamp on the sync actually being able to adopt: in selected
mode, at least one selected tag or adopted alias must exist for the
snapshot filter to keep anything.

Fixes #6283

* restore atomicBool tests; trim comment to repo 2-line cap

The new test file unintentionally replaced the existing
node_traffic_sync_job_test.go, dropping its four atomicBool tests;
restore them and keep only an additive diff. Trim the syncCanAdopt
doc comment to the repository's 2-line comment cap.

* trim syncCanAdopt comment to the 2-line cap
yzxcj797 пре 9 часа
родитељ
комит
a255ab7c65

+ 11 - 1
internal/web/job/node_traffic_sync_job.go

@@ -390,6 +390,7 @@ func (j *NodeTrafficSyncJob) syncOne(mgr *runtime.Manager, n *model.Node, doIpSy
 		return nil
 	}
 	snap.ManagedAliases = rt.AdoptedInboundAliases()
+	syncCanAdopt := syncCanAdoptInbounds(n, snap.ManagedAliases)
 	service.FilterNodeSnapshot(n, snap)
 	_, _, dirty, _, _ := j.nodeService.NodeSyncState(n.Id)
 	if !dirty {
@@ -414,7 +415,7 @@ func (j *NodeTrafficSyncJob) syncOne(mgr *runtime.Manager, n *model.Node, doIpSy
 	if changed {
 		j.structural.set()
 	}
-	if !dirty && n.InboundsAdoptedAt == 0 {
+	if !dirty && n.InboundsAdoptedAt == 0 && syncCanAdopt {
 		if markErr := j.nodeService.MarkNodeInboundsAdopted(n.Id); markErr != nil {
 			logger.Warningf("node traffic sync: mark inbounds adopted for %s failed: %v", n.Name, markErr)
 		}
@@ -475,3 +476,12 @@ func (j *NodeTrafficSyncJob) syncOne(mgr *runtime.Manager, n *model.Node, doIpSy
 	}
 	return active
 }
+
+// Whether this sync can perform the "first clean adoption" that
+// InboundsAdoptedAt records (#6283).
+func syncCanAdoptInbounds(n *model.Node, adoptedAliases []string) bool {
+	if n == nil || n.InboundSyncMode != "selected" {
+		return true
+	}
+	return len(n.InboundTags) > 0 || len(adoptedAliases) > 0
+}

+ 43 - 0
internal/web/job/node_traffic_sync_job_test.go

@@ -3,6 +3,8 @@ package job
 import (
 	"sync"
 	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 )
 
 func TestAtomicBool_DefaultIsFalse(t *testing.T) {
@@ -67,3 +69,44 @@ func TestAtomicBool_ConcurrentSettersExactlyOneTakeWins(t *testing.T) {
 		t.Fatalf("expected exactly one reader to observe true, got %d", trueCount)
 	}
 }
+
+// Regression (#6283): a node onboarded in selected mode with an empty tag
+// list empties its snapshot via FilterNodeSnapshot before the merge sees it,
+// so that sync adopts nothing and must not stamp InboundsAdoptedAt.
+func TestSyncCanAdoptInbounds(t *testing.T) {
+	cases := []struct {
+		name     string
+		node     *model.Node
+		aliases  []string
+		expected bool
+	}{
+		{"all mode always adopts", &model.Node{InboundSyncMode: "all"}, nil, true},
+		{
+			"selected with tags adopts",
+			&model.Node{InboundSyncMode: "selected", InboundTags: []string{"in-443-tcp"}},
+			nil,
+			true,
+		},
+		{
+			"selected empty with adopted alias adopts",
+			&model.Node{InboundSyncMode: "selected"},
+			[]string{"in-443-tcp"},
+			true,
+		},
+		{
+			// The reported bug: registering in selected mode and choosing
+			// tags afterwards stamped adoption while adopting nothing.
+			"selected empty with no aliases adopts nothing",
+			&model.Node{InboundSyncMode: "selected"},
+			nil,
+			false,
+		},
+	}
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			if got := syncCanAdoptInbounds(c.node, c.aliases); got != c.expected {
+				t.Fatalf("syncCanAdoptInbounds(%+v, %v) = %v, want %v", c.node, c.aliases, got, c.expected)
+			}
+		})
+	}
+}