fix(hwid): serialize the device-limit write with its trim (#6591)

setClientLimitHwidByEmail wrote clients.limit_hwid and then trimmed client_hwids as two independent statements. A traffic-cycle Save that read the record before the limit changed could write the stale value back after it, and a failed trim committed the new limit anyway.

Both halves now run inside runSerializedTx, the transaction the traffic writer already owns. setClientLimitHwidByEmailTx and clearClientHwidsBySubIDTx refuse a handle that is not that transaction (errClientHwidWriteNotSerialized) instead of falling back to the shared handle. Client delete moves onto the same writer, and BulkCreate withdraws a re-created client's tombstone before applying its optional HWID limit.

TestSetClientLimitHwidIsSerializedWithSyncInbound holds a stale traffic-cycle Save open across the limit change and fails without the serialization (limit_hwid = 5, want 1).
This commit is contained in:
n0ctal
2026-09-26 23:30:22 +05:00
committed by GitHub
parent d86a3def85
commit c0c0136037
6 changed files with 255 additions and 16 deletions
+14 -5
View File
@@ -48,6 +48,8 @@ const (
hwidFingerprintLength = 12
)
var errClientHwidWriteNotSerialized = errors.New("client HWID write requires the serialized transaction")
type ClientHwidInfo struct {
Id int `json:"id"`
FirstSeen int64 `json:"firstSeen"`
@@ -300,9 +302,16 @@ func (s *ClientService) DeleteClientHwid(email string, id int) error {
return nil
}
func (s *ClientService) setClientLimitHwidByEmail(tx *gorm.DB, email string, limit int) error {
if tx == nil {
tx = database.GetDB()
// Serialize the limit write and trim with SyncInbound and client deletion.
func (s *ClientService) setClientLimitHwidByEmail(email string, limit int) error {
return runSerializedTx(func(tx *gorm.DB) error {
return s.setClientLimitHwidByEmailTx(tx, email, limit)
})
}
func (s *ClientService) setClientLimitHwidByEmailTx(tx *gorm.DB, email string, limit int) error {
if !isSerializedTx(tx) {
return errClientHwidWriteNotSerialized
}
if limit < 0 {
limit = 0
@@ -346,8 +355,8 @@ func trimClientHwidsForSubID(tx *gorm.DB, subID string, limit int) error {
}
func clearClientHwidsBySubIDTx(tx *gorm.DB, subIDs ...string) error {
if tx == nil {
tx = database.GetDB()
if !isSerializedTx(tx) {
return errClientHwidWriteNotSerialized
}
clean := make([]string, 0, len(subIDs))
seen := map[string]struct{}{}