Skip to content

fix(server): bound connection concurrency so a scan cannot exhaust the fd table - #3394

Merged
vpetersson merged 1 commit into
masterfrom
agent/anthias-claude/520fd277d119
Oct 19, 2026
Merged

vpetersson merged 1 commit into
masterfrom
agent/anthias-claude/520fd277d119

Conversation

@vpetersson-bot

Copy link
Copy Markdown
Contributor

Issues Fixed

Sentry ANTHIAS-62 (OSError: [Errno 24] Too many open files) and ANTHIAS-63 (OperationalError: unable to open database file).

Description

Nothing bounds how many connections a client may open against an Anthias box, and the stock container descriptor limit is 1024. A burst walks the table to that ceiling, and the failure is not graceful: SQLite can no longer open the database, every request 500s, and the 500 handler cannot open 500.html either.

The device in Sentry was hit by a Qualys scan. The ordering there settles the cause: the Errno 24 events land at 13:39:21, the 371 database is locked events only from 13:39:22. Descriptor exhaustion came first — it is not downstream of the lock failures fixed by #3383, which closed a leak that made this worse but did not bound the burst.

Two guards, because neither is sufficient alone:

  • Raise the soft descriptor limit to 8192. A process may raise its own soft limit up to the hard limit (the containers ship 1024/524288) without privilege, so this needs no ulimits: key — and therefore works identically under balena, whose supervisor renders its own compose file. Failure to raise is non-fatal; a headless device that will not boot is worse than one without the headroom.
  • Bound concurrency. The descriptor raise alone only moves the ceiling, because every accepted connection runs the full Django path including a database connect. --limit-concurrency has uvicorn answer 503 as soon as the in-flight count is reached, costing no database handle and no template render. Default 256 — about six times the ~39 sockets a device holds while idle with the viewer polling — overridable with ANTHIAS_LIMIT_CONCURRENCY.

Applied to the development invocation as well: a dev stack on a routable address is as reachable as a fleet device.

Testing

Measured on the testbed with a 1500-connection burst, against a Pi 5 and a Pi 2 (slowest board, worst case for recovery):

Pi 5 Pi 2
Unguarded peaked ~930 fds, served unable to open database file out of get_new_connection refused every connection past 1005, container died and was restarted by restart: always
Guarded 784 fds peak, cheap 503s, 0 descriptor/database errors 489 fds peak, process alive, 0 descriptor/database errors

Both boards stayed healthy afterwards. A 5000-connection burst against the guarded Pi 5 plateaued at 776 descriptors and still produced no errors.

Unit tests cover both guards, including that the development branch keeps its limit and that a refused descriptor raise cannot abort startup. Verified by mutation — removing either guard fails the suite.

Checklist

  • I have performed a self-review of my own code.
  • New and existing unit tests pass locally and on CI with my changes.
  • I have done an end-to-end test for Raspberry Pi devices.
  • I have tested my changes for x86 devices.
  • I added a documentation for the changes I have made (when necessary).

Note: one pre-existing failure, tests/test_viewer.py::TestWaitForWaylandSocket::test_probe_reports_not_ready_for_a_non_socket_path, fails under pytest -n auto on master as well as here, and passes serially. Unrelated to this change and left alone.

🤖 Generated with Claude Code

…e fd table

A burst of concurrent connections walked the server container's file
descriptor table to its stock 1024 ceiling. Past that, SQLite could not
open the database, every request 500d, and the 500 handler could not
open 500.html either.

- raise the soft descriptor limit to 8192, which a process may do for
  itself up to the hard limit, so no compose or balena change is needed
- pass --limit-concurrency (default 256, ANTHIAS_LIMIT_CONCURRENCY
  overrides) to both the development and production invocations, so a
  burst is shed with a cheap 503 instead of a database connect per
  socket

Measured on the testbed: unguarded, a Pi 2 refused every connection
past ~1005 and the container died and was restarted by the policy; a
Pi 5 peaked at ~930 descriptors and served the ANTHIAS-63 signature,
"unable to open database file" out of get_new_connection. Guarded, the
same 1500-connection burst left both boards alive with no descriptor
or database errors, peaking at 489 and 784 descriptors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
@vpetersson-bot
vpetersson-bot requested a review from a team as a code owner October 17, 2026 08:28
Copilot AI lite review requested due to automatic review settings October 17, 2026 08:28
@vpetersson-bot vpetersson-bot self-assigned this Oct 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

@vpetersson-bot
vpetersson-bot force-pushed the agent/anthias-claude/520fd277d119 branch from c0bf241 to 37d0009 Compare October 19, 2026 08:54
@sonarqubecloud

Copy link
Copy Markdown

@vpetersson
vpetersson merged commit 739f15f into master Oct 19, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants