fix(webhook): avoid synchronous SQLite writes in async event route #95

Open
opened 2026-09-25 09:59:12 +00:00 by gabogg · 1 comment
Owner

Problem

PR #94 review found that the async /api/event/webhook route calls DoorStateManager.handle_webhook_event, which performs synchronous SQLite writes (including door transition persistence). Those writes block the event loop during webhook bursts. This predates #94; its new transition write increases the work done on that path.

Approach

Move the webhook persistence and broadcast path to async repository methods, or isolate all synchronous work in a worker thread and schedule the WebSocket emission back on the main loop. Preserve the existing event ordering, one-broadcast-per-persisted-transition rule, and error handling.

Verification

Add an ASGI webhook integration test with real SQLite and a concurrent event-loop task to prove webhook persistence does not block the loop. Keep external HikCentral calls stubbed.

Found in PR #94 review.

## Problem PR #94 review found that the async `/api/event/webhook` route calls `DoorStateManager.handle_webhook_event`, which performs synchronous SQLite writes (including door transition persistence). Those writes block the event loop during webhook bursts. This predates #94; its new transition write increases the work done on that path. ## Approach Move the webhook persistence and broadcast path to async repository methods, or isolate all synchronous work in a worker thread and schedule the WebSocket emission back on the main loop. Preserve the existing event ordering, one-broadcast-per-persisted-transition rule, and error handling. ## Verification Add an ASGI webhook integration test with real SQLite and a concurrent event-loop task to prove webhook persistence does not block the loop. Keep external HikCentral calls stubbed. Found in PR #94 review.
Author
Owner

PR #112 second-pass review follow-ups (minor; keep this issue open after merge):

  • Threaded overview can race async polling. app/controllers/webhook_controller.py:36 now builds the overview with asyncio.to_thread. get_door_overview() iterates self.doors.items() while sync_doors_async() can insert a newly discovered door into that dict. A deterministic reproduction raises RuntimeError: dictionary changed size during iteration; the webhook then returns {"code":"1"} and omits its door_event overview, although its transition has already been persisted and broadcast. Snapshot the door cache or serialize overview construction with cache writes, and add a concurrent polling/webhook test.
  • Immediate cardholder lookup is deferred. The threaded overview calls _cardholder_name(), whose person_directory.request() requires a running event loop and silently returns False in the worker thread. A card-only webhook therefore does not start lookup during its immediate overview. The periodic monitor can start it later. Schedule the lookup on the event loop and cover the ASGI webhook route with a card-only event.

The main #95 event-loop responsiveness and cancellation/broadcast requirements are covered by PR #112.

PR #112 second-pass review follow-ups (minor; keep this issue open after merge): - **Threaded overview can race async polling.** `app/controllers/webhook_controller.py:36` now builds the overview with `asyncio.to_thread`. `get_door_overview()` iterates `self.doors.items()` while `sync_doors_async()` can insert a newly discovered door into that dict. A deterministic reproduction raises `RuntimeError: dictionary changed size during iteration`; the webhook then returns `{"code":"1"}` and omits its `door_event` overview, although its transition has already been persisted and broadcast. Snapshot the door cache or serialize overview construction with cache writes, and add a concurrent polling/webhook test. - **Immediate cardholder lookup is deferred.** The threaded overview calls `_cardholder_name()`, whose `person_directory.request()` requires a running event loop and silently returns `False` in the worker thread. A card-only webhook therefore does not start lookup during its immediate overview. The periodic monitor can start it later. Schedule the lookup on the event loop and cover the ASGI webhook route with a card-only event. The main #95 event-loop responsiveness and cancellation/broadcast requirements are covered by PR #112.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
gabogg/hikcentral#95
No description provided.