fix(tgbot): release the invite claim lock even when a claim panics

claimInvite took the package-level inviteClaimMu and released it
without defer, inside a handler whose panics the bot recovers. A panic
while resolving or binding a token left the lock held for good, and
every later /start claim parked on a worker slot until the bot answered
nobody.

The lock is now held in its own function with a deferred unlock.
This commit is contained in:
MHSanaei
2026-11-01 13:38:58 +01:00
parent d63583caaf
commit e02f51320a
2 changed files with 28 additions and 7 deletions
+13 -7
View File
@@ -71,13 +71,7 @@ func (t *Tgbot) claimInvite(chatId int64, fromID int64, payload string) inviteOu
return inviteInvalid
}
inviteClaimMu.Lock()
outcome, records := t.resolveInviteToken(token, fromID)
var bindErr error
if outcome == inviteBindable {
bindErr = t.bindRecordsToUser(records, fromID)
}
inviteClaimMu.Unlock()
outcome, records, bindErr := t.resolveAndBindInvite(token, fromID)
switch outcome {
case inviteAlreadyOwned:
@@ -97,6 +91,18 @@ func (t *Tgbot) claimInvite(chatId int64, fromID int64, payload string) inviteOu
return outcome
}
// resolveAndBindInvite holds inviteClaimMu with a deferred unlock: the handler recovers panics,
// and a lock a panic left held would park every later /start claim for good.
func (t *Tgbot) resolveAndBindInvite(token string, fromID int64) (inviteOutcome, []*model.ClientRecord, error) {
inviteClaimMu.Lock()
defer inviteClaimMu.Unlock()
outcome, records := t.resolveInviteToken(token, fromID)
if outcome != inviteBindable {
return outcome, records, nil
}
return outcome, records, t.bindRecordsToUser(records, fromID)
}
func recordEmails(records []*model.ClientRecord) string {
emails := make([]string, 0, len(records))
for _, record := range records {
@@ -1,6 +1,7 @@
package tgbot
import (
"encoding/base64"
"strings"
"sync"
"testing"
@@ -222,3 +223,17 @@ func TestTgUserMentionEscapesName(t *testing.T) {
t.Errorf("tgUserMention = %q, want %q", got, want)
}
}
// The bot's handlers recover panics, so a claim that panicked with inviteClaimMu held left
// it locked for good and every later /start claim parked on a worker slot.
func TestInviteClaimReleasesTheLockWhenItPanics(t *testing.T) {
func() {
defer func() { _ = recover() }()
// No database is open here, so resolving the token panics inside the lock.
(&Tgbot{}).claimInvite(1, 1, base64.RawURLEncoding.EncodeToString([]byte("some-sub-id")))
}()
if !inviteClaimMu.TryLock() {
t.Fatal("inviteClaimMu is still held after a claim panicked")
}
inviteClaimMu.Unlock()
}