Skip to content

MQTTManager: wait for the web interface's database lock instead of failing - #399

Merged
tpanajott merged 1 commit into
NSPManager:develfrom
jamesmulcahy:fix/mqttmanager-db-busy-timeout
Oct 16, 2026
Merged

tpanajott merged 1 commit into
NSPManager:develfrom
jamesmulcahy:fix/mqttmanager-db-busy-timeout

Conversation

@jamesmulcahy

Copy link
Copy Markdown
Contributor

Problem

The web interface tells MQTTManager to reload on every save, so a reload's reads often run while the write that triggered it is still committing. MQTTManager opens SQLite without a busy timeout, so those reads fail at once:

[error] [mqtt_manager_config.hpp:270] Caught exception while trying to access database to retrieve setting screen_dim_level. Exception: ... database is locked: database is locked
terminate called after throwing an instance of 'std::invalid_argument'
  what():  stoi
MQTTManager binary has exited unexpectedly. Killed by signal: 6

The settings getters log the error and return "". The typed getters turn that into 0, so a reload that loses the race quietly sends panels config built from zeros (button timings and so on). NSPanel's config builder calls std::stoi on screen_dim_level and similar settings directly, and on "" that aborts the process. Deleting a light in the web interface and adding another straight afterwards was enough to trigger it.

Django already waits for locks: settings.py sets "timeout": 20. MQTTManager was the side that didn't wait.

Fix

Every connection MQTTManager opens now gets a 20 s busy timeout (sqlite3_busy_timeout), matching Django's.

database_manager::database is a static inline variable in a header, so each translation unit that includes database_manager.hpp has its own storage and its own connections. A timeout set in init() would only reach main.cpp's copy. So the timeout is set through on_open in a make_database() helper that builds each copy. That is also why database.open_forever() in init() only ever applies to main.cpp's copy, and every other translation unit opens a connection per query. This PR leaves that as it is, but it may not be what was intended.

Testing

A new test, tests/database_test.cpp, has a second connection take the write lock (BEGIN EXCLUSIVE), as the web interface does during a save, and release it after 500 ms. It then checks that a read through database_manager::database waits for the lock rather than throwing.

  • Without the fix: the read throws database is locked.
  • With the fix: the read waits about 530 ms and succeeds.

I ran this test on its own, built against the dev Conan cache. I have not run the full run_tests.sh suite on this branch.

The crash was found by running the panel firmware in QEMU against the dev stack. The firmware's integration tests add and remove a light, and delete a panel, in quick succession.

Not in this PR

  • A fallback for the bare std::stoi calls in nspanel.cpp, so a bad or missing setting can't abort the process.
  • WAL journal mode. It would let reads proceed during a write, but the startup backup in run_uwsgi.sh copies only the main database file, so it would need changing too.

🤖 Generated with Claude Code

…iling

The web interface triggers a reload on every save, so a reload's reads often
run while that write is still committing. MQTTManager opened SQLite without a
busy timeout, so those reads failed at once with "database is locked", the
settings getters returned empty strings, and NSPanel's config builder aborted
the process on std::stoi(""). Deleting a light and then adding another in quick
succession was enough to trigger it.

Give every connection a 20 s busy timeout, matching the web interface's.
`database` is a static in a header, so each translation unit has its own
storage; the timeout is set where each copy is built rather than in init(),
which only ever configured main.cpp's copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tpanajott

Copy link
Copy Markdown
Collaborator

Nice one! I've been chasing these issues for a while but never been able to reliably reproduce them when trying to debug them. Thanks!

Also, having only main open_forever is not the intended functionality. I need to do some tests where that is not the case.

@tpanajott
tpanajott merged commit fe3fb7b into NSPManager:devel Oct 16, 2026
1 check 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.

2 participants