fix: attach ring buffers in the parent for fork producers (candidate cause of the fork-lane hangs) #210
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| # Only run the (expensive) test matrix when files that can affect the tests or | |
| # package change. Docs, license, images and other non-code changes are skipped. | |
| # This is an allowlist: anything test-relevant added later (e.g. a new data dir | |
| # or a root conftest.py) must be added here or CI will silently skip it. | |
| push: | |
| branches: ["main"] | |
| paths: | |
| - "src/**" # birdnet, birdnet_tests (incl. TEST_FILES), birdnet_benchmark | |
| - "example/**" # test data: test_issue29/55 load example/soundscape.wav | |
| - "pyproject.toml" # deps + tox + pytest/ruff/mypy config | |
| - "MANIFEST.in" # packaging -> build/twine check step | |
| - ".github/workflows/ci.yml" # the workflow itself | |
| pull_request: | |
| branches: ["main"] | |
| paths: | |
| - "src/**" | |
| - "example/**" | |
| - "pyproject.toml" | |
| - "MANIFEST.in" | |
| - ".github/workflows/ci.yml" | |
| # Allows you to run this workflow manually from the Actions tab | |
| workflow_dispatch: | |
| # Cancel a running PR check when new commits are pushed, so a wedged run does not | |
| # keep burning runner hours after it has been superseded. Pushes to main still run | |
| # to completion. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| run-tests: | |
| runs-on: ${{ matrix.os }} | |
| continue-on-error: false | |
| # Backstop for a wedged job. The library defaults to the "spawn" start method | |
| # (see birdnet.core.start_method), which removes the fork-after-TensorFlow | |
| # deadlock from the general suite; the isolated fork lanes can still burn up to | |
| # the 600 s per-test timeout each when that inherent deadlock fires. Budget is | |
| # set by the fattest cell, ubuntu 3.12, which runs three tox envs (py312 + | |
| # py312-repro + py312-coverage, ~45 min healthy): 40 was proven too tight when | |
| # a repro fork-lane deadlock pushed it over. | |
| timeout-minutes: 50 | |
| env: | |
| # Fixed, cacheable location for birdnet's model/label downloads so the ~11 min | |
| # "load_model" download step is skipped on a cache hit. Placed under the | |
| # workspace because the `runner` context is not available in job-level env; | |
| # this is safe since checkout runs once, before the cache is restored, and is | |
| # never re-run. tox forwards this var to the pytest subprocess via `passenv`. | |
| BIRDNET_APP_DATA: ${{ github.workspace }}/.birdnet-app-data | |
| # Enables the GIL-immune hang watchdog (see birdnet_tests/conftest.py): a | |
| # wedged test that survives pytest-timeout gets its thread stacks dumped | |
| # here and the process hard-killed shortly after its timeout, instead of | |
| # silently burning the job budget. Uploaded as an artifact on failure. | |
| BIRDNET_TEST_WATCHDOG_DIR: ${{ github.workspace }}/.watchdog-dumps | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: | |
| [ | |
| ubuntu-24.04, | |
| ubuntu-24.04-arm, | |
| macos-15, | |
| macos-15-intel, | |
| windows-2025, | |
| ] | |
| python-version: ["3.11", "3.12", "3.13", "3.14"] | |
| exclude: | |
| - os: macos-15-intel | |
| python-version: "3.13" # no TF support | |
| - os: macos-15-intel | |
| python-version: "3.14" # no TF / torch x86 macOS wheels | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 2 | |
| - name: Install audio system libs (Linux) | |
| if: startsWith(matrix.os, 'ubuntu') | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libsndfile1 | |
| - name: Install audio system libs (macOS) | |
| if: startsWith(matrix.os, 'macos') | |
| env: | |
| # Keep `brew install` from running its own implicit update, which would | |
| # reintroduce the tap fetches removed below. | |
| HOMEBREW_NO_AUTO_UPDATE: "1" | |
| run: | | |
| # No `brew update` here on purpose. It git-fetches every tap preinstalled | |
| # on the runner image -- hashicorp/tap, aws/tap, azure/bicep, | |
| # homebrew/core, homebrew/cask -- and exits non-zero if any single fetch | |
| # fails, so one transient TLS fault takes the whole job down before | |
| # libsndfile is even requested. `brew install` resolves formulae through | |
| # the JSON API and pulls bottles from ghcr.io, so the tap checkouts are | |
| # not needed to get libsndfile. | |
| brew install libsndfile | |
| - name: Install audio system libs (Windows) | |
| if: startsWith(matrix.os, 'windows') | |
| run: | | |
| choco install libsndfile | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| # Provides the `uv` binary and persists uv's wheel cache across runs, so the | |
| # large TensorFlow/torch/cuda downloads are not repeated every run. | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v6 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: pyproject.toml | |
| # Cache the downloaded models/labels. The files are identical across OS and | |
| # Python, so we key only on the download *profile* (full vs the TF-free py3.14 | |
| # surface) plus the model definitions (their URLs/sizes). This keeps the cache | |
| # to ~2 entries (~3.6 GB) instead of one ~3 GB copy per (os, python), which | |
| # overran the 10 GB repo cache limit and thrashed. A changed definition busts | |
| # the key; restore-keys then falls back to the last cache of the same profile | |
| # and only the stale models are re-downloaded. | |
| - name: Cache downloaded models | |
| uses: actions/cache@v4 | |
| with: | |
| path: ${{ env.BIRDNET_APP_DATA }} | |
| key: birdnet-models-${{ matrix.python-version == '3.14' && 'notf' || 'full' }}-${{ hashFiles('src/birdnet/**/models/**/*.py') }} | |
| restore-keys: | | |
| birdnet-models-${{ matrix.python-version == '3.14' && 'notf' || 'full' }}- | |
| - name: Install dependencies | |
| run: uv pip install --system '.[tests]' | |
| - name: Run tests py311 | |
| if: matrix.python-version == '3.11' | |
| run: | | |
| python -m tox -e py311 | |
| - name: Run tests py312 | |
| if: matrix.python-version == '3.12' | |
| run: | | |
| python -m tox -e py312 | |
| - name: Run tests py313 | |
| if: matrix.python-version == '3.13' | |
| run: | | |
| python -m tox -e py313 | |
| # TensorFlow has no Python 3.14 wheels yet (issue #55), so 3.14 installs | |
| # without it and only the TensorFlow-free surface (acoustic 3.0 via onnx) | |
| # is exercised here. | |
| - name: Run tests py314 | |
| if: matrix.python-version == '3.14' | |
| run: | | |
| python -m tox -e py314 | |
| - name: Run repro tests | |
| if: matrix.python-version == '3.12' && matrix.os != 'macos-15-intel' | |
| run: | | |
| python -m tox -e py312-repro | |
| - name: Run coverage tests | |
| if: matrix.os == 'ubuntu-24.04' && matrix.python-version == '3.12' | |
| run: | | |
| python -m tox -e py312-coverage | |
| - name: Upload results to Codecov | |
| if: matrix.os == 'ubuntu-24.04' && matrix.python-version == '3.12' | |
| uses: codecov/codecov-action@v6 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| slug: birdnet-team/birdnet | |
| - name: Extract log path | |
| id: extract-log-path | |
| shell: pwsh | |
| run: | | |
| $logPaths = @() | |
| if (Test-Path output.log) { | |
| $output = Get-Content output.log -Raw | |
| $pattern = '(?<path>(?:[A-Za-z]:\\|/)[^\r\n]*?\.log)' | |
| $logPaths = [regex]::Matches($output, $pattern) | | |
| ForEach-Object { $_.Groups['path'].Value } | | |
| Sort-Object -Unique | | |
| Where-Object { Test-Path $_ -PathType Leaf } | |
| } | |
| if ($logPaths.Count -gt 0) { | |
| "log_paths<<EOF" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| $logPaths | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| "EOF" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| } | |
| else { | |
| "log_paths=" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 | |
| } | |
| - name: Upload log file | |
| if: failure() && steps.extract-log-path.outputs.log_paths != '' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: birdnet-log | |
| path: ${{ steps.extract-log-path.outputs.log_paths }} | |
| # Diagnostic for silent test-process deaths: rules the kernel OOM killer | |
| # in or out directly in the job log. (Checked for the fork-lane wedges: | |
| # no OOM kills logged — those are the inherent fork-after-TF child | |
| # deadlock, ended by pytest-timeout at the per-test timeout.) | |
| - name: Check for OOM kills (Linux) | |
| if: failure() && startsWith(matrix.os, 'ubuntu') | |
| run: sudo dmesg | grep -iE "out of memory|oom-kill|killed process" || echo "no OOM kills logged" | |
| # Stack dumps written by the hang watchdog (see BIRDNET_TEST_WATCHDOG_DIR | |
| # above); on a wedge they show every thread's stack at kill time. | |
| # include-hidden-files is required: upload-artifact skips dotfiles by | |
| # default, and the dump dir is hidden (.watchdog-dumps). | |
| - name: Upload hang watchdog dumps | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: watchdog-dumps-${{ matrix.os }}-py${{ matrix.python-version }} | |
| path: .watchdog-dumps/ | |
| if-no-files-found: ignore | |
| include-hidden-files: true |