Fix LogConfig ID lifecycle - #608
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes the host-side LogConfig ID lifecycle so log block IDs can wrap and be reused safely without stale Log.log_blocks entries capturing replies/log data for reused IDs. It introduces an acknowledgement-owned “lease” model for log block IDs, aligning host and firmware state across delete/reset/disconnect and concurrent lifecycle commands.
Changes:
- Replace modulo ID counter behavior with a thread-safe free-ID pool covering the full
0..255range, releasing IDs only after delete acknowledgements. - Serialize log lifecycle commands (create/start/stop/delete/reset/disconnect) and validate registrations after packet sends to prevent stale follow-up actions.
- Add comprehensive unit tests for ID reuse, delete/reset/disconnect behavior, retries, and concurrency.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| cflib/crazyflie/log.py | Implements ID leasing, deferred release on delete ack, reset/disconnect draining, and command serialization with registration validation. |
| test/crazyflie/test_log.py | Adds tests covering ID wrap/reuse, idempotent delete, reset/disconnect semantics, retry behavior, and concurrent registrations. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
LogConfig._create() iterates over log_blocks without holding the registration lock, which can race with concurrent registration/detach and raise runtime errors or compute inconsistent limits.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
There’s a confirmed TOC access race in add_config() and several new concurrency tests can hang the suite if an early assertion fails before unblocking worker threads.
Review details
Suppressed comments (4)
Previously missed (3) — in code that hasn't changed since the last review.
cflib/crazyflie/log.py:533
add_config()readsself.tocoutside of any lock and multiple times. Sincerefresh_toc()setsself.toc = Nonebefore acquiring any locks, a concurrent refresh can make this code raise an unexpectedAttributeError(instead of a controlledLogConfigError/KeyError) or validate against two different TOC objects. Consider snapshottingtoc = self.toconce (and rejectingNone) and using that consistently for lookups during validation.
test/crazyflie/test_log.py:280- If
create_send_started/delete_send_startedis set slightly after the 1s timeout, theassertTrue(...wait...)will fail before thefinallyunblocks the worker thread, potentially leaving a non-daemon thread stuck inallow_*_send.wait()and hanging the test run. Consider registering cleanup (or using daemon threads) before the assertion so the event is always released even on early assertion failures.
test/crazyflie/test_log.py:350 - If
create_send_startedis set slightly after the 1s timeout, the assertion can fail before thefinallyunblocksallow_create_send, leaving the started non-daemon thread stuck waiting and potentially hanging the test run. Register a cleanup to always releaseallow_create_send(and/or mark threads as daemon) before the assertion.
This issue also appears on line 395 of the same file.
test/crazyflie/test_log.py:400
- If
create_send_startedis set slightly after the 1s timeout, the assertion can fail before thefinallyunblocksallow_create_send, leaving the started non-daemon thread stuck waiting and potentially hanging the test run. Register a cleanup to always releaseallow_create_send(and/or mark threads as daemon) before the assertion.
self.cf.send_packet.side_effect = send_packet
start_thread = threading.Thread(target=config.start)
start_thread.start()
self.assertTrue(create_send_started.wait(1.0))
disconnect_thread = threading.Thread(target=disconnect)
disconnect_thread.start()
- Files reviewed: 2/2 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
There are confirmed callback-argument and pending-state handling bugs in Log._handle_settings_packet()/LogConfig._create() that can break user callbacks and leave configs stuck in an “active/pending” state after failures.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
cflib/crazyflie/log.py:854
- On create-block failure, the code queues
block.added_cb.call(False)(missing theLogConfigargument) and never clearsblock.pending. This can break callback signatures (most handlers expect(logconf, added)) and can leave the config permanently counted as “active”.
block.err_no = error_status
callbacks.append((block.added_cb, (False,)))
callbacks.append((block.error_cb, (block, msg)))
cflib/crazyflie/log.py:876
- On
CMD_START_LOGGINGerror, the failure callback is queued asblock.started_cb.call(self, False)whereselfis theLoginstance, not theLogConfig. This is inconsistent with the normal callback signature used elsewhere (started_cb.call(logconf, started)) and will break user callbacks expecting aLogConfig.
block.err_no = error_status
callbacks.append((block.started_cb, (self, False)))
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
| @@ -280,63 +325,68 @@ def create(self): | |||
| while not is_done: | |||
There was a problem hiding this comment.
🔵 Needs a closer look
The CREATE_BLOCK acknowledgement handler can leave LogConfig.pending stuck True for already-added blocks, which can incorrectly inflate active-usage tracking and block future registrations.
Review details
Suppressed comments (1)
cflib/crazyflie/log.py:845
- In the CREATE_BLOCK ack handler,
block.pending(and the added-state update) is only cleared/executed insideif not block.added:. IfLogConfig.create()is (re)called while the block is already marked as added (or a duplicate CREATE ack arrives afterblock.addedflipped),pendingcan remain stuckTrue, which then inflates_get_active_config_usage()and can block new configs.
if error_status == 0 or error_status == errno.EEXIST:
if not block.added:
logger.debug('Have successfully added id=%d', id)
pk = CRTPPacket()
- Files reviewed: 2/2 changed files
- Comments generated: 0 new
- Review effort level: Lite
Abstract
Fix repeated
LogConfigcreation and deletion so applications can use logging for more than one byte-sized ID cycle without stale host-side blocks capturing reused IDs. Log block IDs are now managed as acknowledgement-owned leases, keeping host and firmware lifecycle state aligned across deletion, reset, disconnect, and concurrent commands.Closes #577.
Why
The previous implementation incremented an ID counter modulo 255 but never removed acknowledged-deleted configurations from
Log.log_blocks. Once an ID wrapped, reply and log-data lookup found the stale configuration first. This made the firmware appear to run out of log blocks after repeated create/read/delete operations even though it had deleted them correctly.The fix waits for the firmware deletion acknowledgement before releasing an ID. This is important because releasing earlier could route an in-flight reply or data packet to a newer configuration using the same opaque handle.
What changed
0..255ID range with a thread-safe FIFO free-ID pool.DELETEsucceeds or returnsENOENT; failed deletion retains the lease and can be retried.LogConfigspecification to be registered again with a fresh lease.LogConfigErrorfor invalid lifecycle transitions while keeping detached cleanup calls harmless.Verification
uv run python -m unittest discover ./test— 219 tests passed.