fix(whatsapp_native): fix goroutine and resource leak in Start/Stop lifecycle
- Move runCtx/runCancel creation before event handler registration and QR loop so Stop() can cancel at any point during startup - Replace blocking QR event loop in Start() with a background goroutine that selects on runCtx.Done(), preventing Start() from hanging indefinitely when waiting for QR scan - Track all background goroutines (QR handler, reconnect) with sync.WaitGroup; Stop() waits for them to finish before releasing client/container resources - Cancel runCtx on error paths in Start() to avoid leaked contexts Fixes resource leak introduced in #655.
This commit is contained in:
parent
cdbc9c4bd6
commit
c7d75a18f8
1 changed files with 46 additions and 13 deletions
|
|
@ -56,6 +56,7 @@ type WhatsAppNativeChannel struct {
|
||||||
runCancel context.CancelFunc
|
runCancel context.CancelFunc
|
||||||
reconnectMu sync.Mutex
|
reconnectMu sync.Mutex
|
||||||
reconnecting bool
|
reconnecting bool
|
||||||
|
wg sync.WaitGroup // tracks background goroutines (QR handler, reconnect)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWhatsAppNativeChannel creates a WhatsApp channel that uses whatsmeow for connection.
|
// NewWhatsAppNativeChannel creates a WhatsApp channel that uses whatsmeow for connection.
|
||||||
|
|
@ -112,6 +113,12 @@ func (c *WhatsAppNativeChannel) Start(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
client := whatsmeow.NewClient(deviceStore, waLogger)
|
client := whatsmeow.NewClient(deviceStore, waLogger)
|
||||||
|
|
||||||
|
// Create runCtx/runCancel BEFORE registering event handler and starting
|
||||||
|
// goroutines so that Stop() can cancel them at any time, including during
|
||||||
|
// the QR-login flow.
|
||||||
|
c.runCtx, c.runCancel = context.WithCancel(ctx)
|
||||||
|
|
||||||
client.AddEventHandler(c.eventHandler)
|
client.AddEventHandler(c.eventHandler)
|
||||||
|
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
|
|
@ -122,14 +129,29 @@ func (c *WhatsAppNativeChannel) Start(ctx context.Context) error {
|
||||||
if client.Store.ID == nil {
|
if client.Store.ID == nil {
|
||||||
qrChan, err := client.GetQRChannel(ctx)
|
qrChan, err := client.GetQRChannel(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
c.runCancel()
|
||||||
_ = container.Close()
|
_ = container.Close()
|
||||||
return fmt.Errorf("get QR channel: %w", err)
|
return fmt.Errorf("get QR channel: %w", err)
|
||||||
}
|
}
|
||||||
if err := client.Connect(); err != nil {
|
if err := client.Connect(); err != nil {
|
||||||
|
c.runCancel()
|
||||||
_ = container.Close()
|
_ = container.Close()
|
||||||
return fmt.Errorf("connect: %w", err)
|
return fmt.Errorf("connect: %w", err)
|
||||||
}
|
}
|
||||||
for evt := range qrChan {
|
// Handle QR events in a background goroutine so Start() returns
|
||||||
|
// promptly. The goroutine is tracked via c.wg and respects
|
||||||
|
// c.runCtx for cancellation.
|
||||||
|
c.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer c.wg.Done()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.runCtx.Done():
|
||||||
|
return
|
||||||
|
case evt, ok := <-qrChan:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
if evt.Event == "code" {
|
if evt.Event == "code" {
|
||||||
logger.InfoCF("whatsapp", "Scan this QR code with WhatsApp (Linked Devices):", nil)
|
logger.InfoCF("whatsapp", "Scan this QR code with WhatsApp (Linked Devices):", nil)
|
||||||
qrterminal.GenerateWithConfig(evt.Code, qrterminal.Config{
|
qrterminal.GenerateWithConfig(evt.Code, qrterminal.Config{
|
||||||
|
|
@ -141,14 +163,16 @@ func (c *WhatsAppNativeChannel) Start(ctx context.Context) error {
|
||||||
logger.InfoCF("whatsapp", "WhatsApp login event", map[string]any{"event": evt.Event})
|
logger.InfoCF("whatsapp", "WhatsApp login event", map[string]any{"event": evt.Event})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
} else {
|
} else {
|
||||||
if err := client.Connect(); err != nil {
|
if err := client.Connect(); err != nil {
|
||||||
|
c.runCancel()
|
||||||
_ = container.Close()
|
_ = container.Close()
|
||||||
return fmt.Errorf("connect: %w", err)
|
return fmt.Errorf("connect: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.runCtx, c.runCancel = context.WithCancel(ctx)
|
|
||||||
c.SetRunning(true)
|
c.SetRunning(true)
|
||||||
logger.InfoC("whatsapp", "WhatsApp native channel connected")
|
logger.InfoC("whatsapp", "WhatsApp native channel connected")
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -159,6 +183,11 @@ func (c *WhatsAppNativeChannel) Stop(ctx context.Context) error {
|
||||||
if c.runCancel != nil {
|
if c.runCancel != nil {
|
||||||
c.runCancel()
|
c.runCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for background goroutines (QR handler, reconnect) to finish so
|
||||||
|
// they don't reference the client/container after cleanup.
|
||||||
|
c.wg.Wait()
|
||||||
|
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
client := c.client
|
client := c.client
|
||||||
container := c.container
|
container := c.container
|
||||||
|
|
@ -189,7 +218,11 @@ func (c *WhatsAppNativeChannel) eventHandler(evt any) {
|
||||||
}
|
}
|
||||||
c.reconnecting = true
|
c.reconnecting = true
|
||||||
c.reconnectMu.Unlock()
|
c.reconnectMu.Unlock()
|
||||||
go c.reconnectWithBackoff()
|
c.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer c.wg.Done()
|
||||||
|
c.reconnectWithBackoff()
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue