ソースを参照

fix(job): gate ip-limit scan on clients.limit_ip instead of parsing all settings

hasLimitIp ran settings LIKE '%limitIp%' and JSON-parsed every matching
inbound's settings blob — and since clients marshal limitIp without
omitempty, every inbound matched, so each 10s scan loaded and parsed
every settings blob in the database (~75MB of JSON at 500k clients) just
to decide whether any limit exists.

It now probes the normalized clients table (limit_ip > 0, Limit(1) count
like depletedCond does), which SyncInbound and the legacy seeder keep in
sync with the settings JSON. Semantics note: a limitIp that exists only
in settings JSON with no clients row no longer enables enforcement — the
enforcement path itself already resolves clients through the same
normalized tables.
MHSanaei 21 時間 前
コミット
c3cc8b4374

+ 7 - 25
internal/web/job/check_client_ip_job.go

@@ -113,33 +113,15 @@ func (j *CheckClientIpJob) collectFromOnlineAPI() (map[string]map[string]int64,
 	return observed, true
 }
 
+// hasLimitIp reports whether any client carries an IP limit. It probes the
+// normalized clients table (limit_ip is synced there by SyncInbound and the
+// legacy seeder), replacing the old `settings LIKE '%limitIp%'` scan that
+// loaded and JSON-parsed every inbound's settings blob on each 10s run.
 func (j *CheckClientIpJob) hasLimitIp() bool {
 	db := database.GetDB()
-	var inbounds []*model.Inbound
-
-	err := db.Model(model.Inbound{}).Where("settings LIKE ?", "%limitIp%").Find(&inbounds).Error
-	if err != nil {
-		return false
-	}
-
-	for _, inbound := range inbounds {
-		if inbound.Settings == "" {
-			continue
-		}
-
-		settings := map[string][]model.Client{}
-		_ = json.Unmarshal([]byte(inbound.Settings), &settings)
-		clients := settings["clients"]
-
-		for _, client := range clients {
-			limitIp := client.LimitIP
-			if limitIp > 0 {
-				return true
-			}
-		}
-	}
-
-	return false
+	var probe int64
+	err := db.Model(&model.ClientRecord{}).Where("limit_ip > 0").Limit(1).Count(&probe).Error
+	return err == nil && probe > 0
 }
 
 // processObserved runs collection + enforcement for one scan's observations

+ 25 - 0
internal/web/job/check_client_ip_job_integration_test.go

@@ -388,3 +388,28 @@ func TestGetInboundByEmailRejectsSubstringFallbackMatch(t *testing.T) {
 		t.Fatalf("substring email matched inbound %d; want no exact match", got.Id)
 	}
 }
+
+// hasLimitIp gates every 10s scan on the normalized clients table: a bare
+// "limitIp":0 in settings JSON (which the old LIKE scan matched and parsed)
+// must not enable enforcement, while a single clients.limit_ip > 0 row must.
+func TestHasLimitIp_ProbesClientRecords(t *testing.T) {
+	setupIntegrationDB(t)
+	j := &CheckClientIpJob{}
+
+	if j.hasLimitIp() {
+		t.Fatal("hasLimitIp = true on an empty database")
+	}
+
+	seedLinkedInboundWithClient(t, "no-limit", "[email protected]", 0)
+	if j.hasLimitIp() {
+		t.Fatal("hasLimitIp = true with only limit_ip=0 clients")
+	}
+
+	limited := &model.ClientRecord{Email: "[email protected]", LimitIP: 2}
+	if err := database.GetDB().Create(limited).Error; err != nil {
+		t.Fatalf("seed limited client: %v", err)
+	}
+	if !j.hasLimitIp() {
+		t.Fatal("hasLimitIp = false with a limit_ip=2 client present")
+	}
+}