mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-08 21:34:33 +00:00
fix(postgres): make node traffic sync robust after public API inbound updates
The background NodeTrafficSyncJob (every 5s) started failing after a
successful POST /panel/api/inbounds/update/{id} (including flows that
inject streamSettings.externalProxy) with:
node traffic sync: merge for <node> failed:
ERROR: CASE types boolean and integer cannot be matched (SQLSTATE 42804)
Root cause:
- The merge lives in setRemoteTrafficLocked (called from SetRemoteTraffic).
- The client_traffics delta path used a dialect-sensitive expression:
enable = enable AND ?
last_online = GREATEST(last_online, ?)
- On PostgreSQL, GREATEST / AND / COALESCE are implemented with internal
CASE expressions. When "enable" columns (client_traffics, inbounds, ...)
were INTEGER (common after SQLite → PG data migrations, older
AutoMigrate, or mixed write paths) and the right-hand side was a
boolean parameter (from snapshot ClientStats or form-bound API payload),
PG rejected the expression at plan time.
- The public API update path (unlike the internal remote wire path)
always runs updateClientTraffics + UpdateClientStat + SyncInbound.
This touches client_traffics.enable rows for any inbound that has
clients.
- SQLite tolerated 0/1 numeric bools; PG is strict.
Fix:
- Use an explicit CASE with ::boolean casts in the critical enable
expression so the result type is always boolean.
- Make GreatestExpr emit safe casts on Postgres.
- Add a one-time normalization step in MigrationRequirements (runs on
startup + xray restarts) that forces the relevant enable/enabled
columns to boolean on Postgres using an idempotent DO block + USING
cast. This cleans up pre-existing skew without a full re-migration.
This branch is based on upstream/main (original mhsanaei/3x-ui main).
The node traffic sync now survives arbitrary public-API inbound
updates on PostgreSQL.
This commit is contained in:
@@ -22,7 +22,7 @@ func JSONFieldText(expr, key string) string {
|
||||
|
||||
func GreatestExpr(a, b string) string {
|
||||
if IsPostgres() {
|
||||
return fmt.Sprintf("GREATEST(%s, %s)", a, b)
|
||||
return fmt.Sprintf("GREATEST(%s::bigint, %s::bigint)", a, b)
|
||||
}
|
||||
return fmt.Sprintf("MAX(%s, %s)", a, b)
|
||||
}
|
||||
|
||||
@@ -1809,7 +1809,11 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
||||
// from the node arriving after a central disable would otherwise
|
||||
// overwrite enable=false back to true, letting the client accumulate
|
||||
// far more traffic than their limit before being disabled again.
|
||||
enableExpr := "enable AND ?"
|
||||
// Use explicit CASE + boolean cast so the expression type is consistent
|
||||
// on PostgreSQL (GREATEST/AND/CASE can surface "types boolean and integer"
|
||||
// mismatches) even if the row was last touched via the public API update
|
||||
// path or externalProxy-bearing inbounds.
|
||||
enableExpr := "CASE WHEN ?::boolean THEN enable::boolean ELSE false END"
|
||||
if err := tx.Exec(
|
||||
fmt.Sprintf(
|
||||
`UPDATE client_traffics
|
||||
@@ -3524,6 +3528,39 @@ func (s *InboundService) MigrationRequirements() {
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize "enable" columns to boolean on Postgres. Legacy SQLite data
|
||||
// (0/1 integers), partial migrations, or mixed write paths (public API
|
||||
// inbound updates that flow through UpdateClientStat + client syncs, plus
|
||||
// node traffic merge deltas using enable AND/CASE) can leave the column as
|
||||
// integer or with mixed interpretation. This prevents "CASE types boolean
|
||||
// and integer cannot be matched" (internal for GREATEST/COALESCE/AND) in
|
||||
// the node traffic sync merge (SetRemoteTraffic) and makes the sync robust
|
||||
// even when inbounds are updated via /panel/api/inbounds/update (incl. ones
|
||||
// carrying externalProxy in streamSettings).
|
||||
if database.IsPostgres() {
|
||||
// Use DO block so it is idempotent and doesn't fail if already boolean.
|
||||
normalizeBool := func(table, col string) {
|
||||
_, _ = tx.Exec(fmt.Sprintf(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = '%s' AND column_name = '%s'
|
||||
AND data_type <> 'boolean'
|
||||
) THEN
|
||||
ALTER TABLE %s ALTER COLUMN %s
|
||||
TYPE boolean USING (CASE WHEN %s::text IN ('1','true','t','yes') THEN true ELSE false END);
|
||||
END IF;
|
||||
END $$;`, table, col, table, col, col))
|
||||
}
|
||||
normalizeBool("inbounds", "enable")
|
||||
normalizeBool("client_traffics", "enable")
|
||||
normalizeBool("nodes", "enable")
|
||||
normalizeBool("clients", "enable")
|
||||
normalizeBool("api_tokens", "enabled")
|
||||
normalizeBool("outbound_subscriptions", "enabled")
|
||||
}
|
||||
|
||||
// Fix inbounds based problems
|
||||
var inbounds []*model.Inbound
|
||||
err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan", "shadowsocks", "hysteria"}).Find(&inbounds).Error
|
||||
|
||||
Reference in New Issue
Block a user