From a9a40a3046ece8532d046a3191f088a9a99dc4ae Mon Sep 17 00:00:00 2001 From: viettranx Date: Mon, 23 Mar 2026 08:16:09 +0700 Subject: [PATCH] fix(zalo): allow QR session restart with race-safe cancel (#346) Cancel previous QR session when user retries instead of blocking with "QR session already active". Uses sync.Map.Swap for atomic replacement and CompareAndDelete so old goroutines don't clobber new sessions. --- .../channels/zalo/personal/zalomethods/qr.go | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/internal/channels/zalo/personal/zalomethods/qr.go b/internal/channels/zalo/personal/zalomethods/qr.go index 9fe5266c..df0048c8 100644 --- a/internal/channels/zalo/personal/zalomethods/qr.go +++ b/internal/channels/zalo/personal/zalomethods/qr.go @@ -22,7 +22,7 @@ import ( type QRMethods struct { instanceStore store.ChannelInstanceStore msgBus *bus.MessageBus - activeSessions sync.Map // instanceID (string) -> struct{} + activeSessions sync.Map // instanceID (string) -> context.CancelFunc } func NewQRMethods(s store.ChannelInstanceStore, msgBus *bus.MessageBus) *QRMethods { @@ -53,27 +53,28 @@ func (m *QRMethods) handleQRStart(ctx context.Context, client *gateway.Client, r return } - if _, loaded := m.activeSessions.LoadOrStore(params.InstanceID, struct{}{}); loaded { - client.SendResponse(goclawprotocol.NewErrorResponse(req.ID, goclawprotocol.ErrInvalidRequest, "QR session already active for this instance")) - return + qrCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) + + // Atomically swap cancel func; cancel any previous QR session so the user can retry. + if prev, loaded := m.activeSessions.Swap(params.InstanceID, cancel); loaded { + if cancelFn, ok := prev.(context.CancelFunc); ok { + cancelFn() + } } // ACK immediately — QR arrives via event. client.SendResponse(goclawprotocol.NewOKResponse(req.ID, map[string]any{"status": "started"})) - go m.runQRFlow(ctx, client, params.InstanceID, instID) + go m.runQRFlow(qrCtx, cancel, client, params.InstanceID, instID) } -func (m *QRMethods) runQRFlow(ctx context.Context, client *gateway.Client, instanceIDStr string, instanceID uuid.UUID) { - defer m.activeSessions.Delete(instanceIDStr) +func (m *QRMethods) runQRFlow(ctx context.Context, cancel context.CancelFunc, client *gateway.Client, instanceIDStr string, instanceID uuid.UUID) { + defer cancel() + defer m.activeSessions.CompareAndDelete(instanceIDStr, cancel) sess := protocol.NewSession() - // LoginQR has internal 100s timeout per QR code. Use 2m as outer bound. - // Derived from parent ctx so QR flow cancels when the WS client disconnects. - qrCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) - defer cancel() - cred, err := protocol.LoginQR(qrCtx, sess, func(qrPNG []byte) { + cred, err := protocol.LoginQR(ctx, sess, func(qrPNG []byte) { client.SendEvent(goclawprotocol.EventFrame{ Type: goclawprotocol.FrameTypeEvent, Event: goclawprotocol.EventZaloPersonalQRCode,