Skip to content

StopContinuousRecognitionAsync goroutine leak due to unbuffered channel + math.MaxUint32 wait timeout #173

@ZenoRewn

Description

@ZenoRewn

Description

StopContinuousRecognitionAsync() (and similarly StartContinuousRecognitionAsync()) use an unbuffered channel internally, while the C-layer wait uses math.MaxUint32 timeout (~49 days).

In real-world usage, this combination can easily leak goroutines.

Root Cause

Looking at speech_recognizer.go:

func (recognizer SpeechRecognizer) StopContinuousRecognitionAsync() chan error {
    outcome := make(chan error) // unbuffered

    go func() {
        // ... C calls ...
        ret = uintptr(C.recognizer_stop_continuous_recognition_async_wait_for(
            recognizer.handleAsyncStopContinuous, math.MaxUint32)) // ~49 days timeout
        // ...
        outcome <- nil // blocks forever if no reader
    }()

    return outcome
}

The internal goroutine blocks on outcome <- nil (or outcome <- err) indefinitely if the caller does not read from the returned channel.

Because the C-layer wait uses math.MaxUint32, caller-side timeout patterns (which are reasonable and common) often abandon the channel, making the goroutine leak permanent.

How to Reproduce

// Common pattern: caller adds timeout to avoid waiting forever
stopChan := recognizer.StopContinuousRecognitionAsync()

select {
case err := <-stopChan:
    // OK path: goroutine exits
    _ = err
case <-time.After(5 * time.Second):
    // Timeout path: goroutine leaked
    // SDK goroutine is stuck in C.recognizer_stop_continuous_recognition_async_wait_for
    // and when it eventually returns, it may block forever on outcome <- nil
}

This issue is also noted in #129.

Suggested Fix

Use a buffered result channel with size 1 so internal goroutines never block on send:

outcome := make(chan error, 1)

This is the standard Go async-result pattern and should be backward-compatible.

Affected methods likely include:

  • StartContinuousRecognitionAsync
  • StopContinuousRecognitionAsync
  • StartKeywordRecognitionAsync
  • StopKeywordRecognitionAsync
  • RecognizeOnceAsync

Environment

  • Go SDK version: latest (master)
  • OS: Linux
  • Go version: 1.22+

Additional Context

When using PushAudioInputStream with continuous recognition, if audioConfig.Close() is called (for example via defer) before StopContinuousRecognitionAsync() completes, the C-layer stop operation may hang indefinitely, making leaks even more likely.

It would help to clarify cleanup order in docs, for example:

  1. Stop recognition
  2. Close recognizer
  3. Close stream
  4. Close audioConfig
  5. Close speechConfig

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions