Skip to content

corebluetooth: peripherals with a dead event_sender are never removed, logging an error on every advertisement #469

Description

@qdot

Summary

In the CoreBluetooth backend, the advertisement handlers look a peripheral up in self.peripherals and send an event to its event_sender. If that send fails because the receiver has been dropped, the error is logged but the peripheral is never removed from the map. Every subsequent advertisement packet from that peripheral repeats the same failing send and the same log line, for the lifetime of the adapter.

Observed as a continuous flood of:

Error sending notification event: send failed because receiver is gone

at sub-millisecond intervals.

Affected code

src/corebluetooth/internal.rs on main, all following the same shape:

  • on_manufacturer_data — error at L581
  • on_service_data — error at L599
  • on_services — error at L612
  • on_services_modified — error at L629
if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) {
    if let Err(e) = p
        .event_sender
        .send(PeripheralEventInternal::ManufacturerData(
            manufacturer_id,
            manufacturer_data,
            rssi,
        ))
        .await
    {
        error!("Error sending notification event: {}", e);
    }
}

on_peripheral_disconnect (L807) and the handler at L919 share the pattern, though those are not advertisement-rate so they are far less visible.

Why it matters

A dead receiver is terminal — nothing will ever read from that channel again — but the send is retried at advertisement rate, which during active scanning is every few milliseconds per peripheral. The cost is not just noise:

  • Each failure formats and emits a log record. Downstream consumers that ship logs somewhere (a UI log view, a file, a crash reporter) do real work per line.
  • Applications with a synchronous or bounded logging pipeline can have the emitting thread stalled by the volume. We hit exactly this in Intiface Central: the flood saturated a bounded log channel whose producer side blocked, which stalled the tokio worker driving the CoreBluetooth event loop and stopped BLE command delivery entirely (iOS stops sending commands when screen turns off intiface/intiface-central#270).

Reproduction

Drop the Peripheral while the physical device is still advertising and scanning is still active. Concretely, in our case: stop the engine, change device configuration, restart the engine — the old Peripheral objects are dropped while CoreBluetooth keeps delivering adverts for the same UUIDs.

Suggested fix

Treat a send error as terminal for that peripheral and drop it from the map, so at most one error is logged per peripheral:

if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) {
    if let Err(e) = p.event_sender.send(/* ... */).await {
        error!("Error sending notification event, dropping peripheral: {}", e);
        self.peripherals.remove(&peripheral_uuid);
    }
}

Whether outright removal is correct depends on whether a peripheral is expected to be re-registered later by UUID; if so, clearing or replacing the sender would be the safer variant. Downgrading these particular sites from error! to debug!/trace! would also help, since a dropped receiver is a normal consequence of the consumer going away rather than a fault.

Environment

  • btleplug 0.12.0, also confirmed unchanged on main
  • macOS / iOS CoreBluetooth backend
  • Reported on iOS 18.x

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions