Skip to content

Async blim.subscribe: remove the synchronous CCCD-write stall #7

Description

@srgg

Summary

Make blim.subscribe non-blocking: register the callback and return the cancel handle immediately, perform the CCCD (Client Characteristic Configuration Descriptor) write in the background, and report write failures asynchronously. This removes the synchronous BLE round-trip stall and makes blim.subscribe safe and non-blocking from any context, including from within a subscription/PTY callback.

Background

To receive notifications, the central must write the characteristic's CCCD (0x2902, 2 bytes 0x0001 for notify) — a round-trip to the device. Today blim.subscribe -> executeSubscription -> connection.Subscribe -> client.Subscribe performs this write synchronously while holding stateMutex.

Consequences:

  • From the main loop: the whole Lua world is frozen for the round-trip duration (usually ms, but seconds on a bad link).
  • From a callback: the notification fan-out is stalled for the round-trip (the callback holds stateMutex during the write).

This is a stall, not corruption — the corruption on the callback path was fixed separately in #3 (subscribe now returns (nil, err) instead of RaiseError). But the stall remains, and it is why a blim.subscribe from a callback is still undesirable.

Proposed behavior

blim.subscribe{...}:

  1. Validate config synchronously (no services / no callback / bad table) -> still return (nil, err) synchronously, as today.
  2. Register the callback locally (in-memory fan-out routing) immediately.
  3. Return the cancel handle immediately (synchronously).
  4. Fire the CCCD write on a background goroutine.
  5. Report a CCCD-write failure asynchronously (device rejected, disconnect) via an error/log callback — since we returned before the write completed.

Result: the caller never waits for the BLE round-trip -> no stall. Notifications begin flowing once the async write completes. blim.subscribe needs no in-callback guard at all — legal and non-blocking from any context.

Trade-offs / design notes

  • Async error reporting: config/validation errors stay synchronous, but the CCCD-write runtime failure becomes asynchronous. Contract change: local cancel, err = blim.subscribe{...} no longer surfaces write failures synchronously. Decide the async error channel (log, an on_error field, or a status callback).
  • Cancel-before-write: cancelling before the background write completes must cancel the pending write; more concurrency in the subscribe path.
  • First-notification latency: small delay before the first notification (until the async write lands).
  • Ordering: two rapid subscribes should keep deterministic CCCD-write ordering per connection.

Relationship to other work

Acceptance

  • blim.subscribe returns without waiting for the CCCD round-trip.
  • Config/validation errors still returned synchronously as (nil, err).
  • CCCD-write failures surfaced asynchronously (documented channel).
  • blim.subscribe from within a callback neither stalls the fan-out nor corrupts state.
  • Cancelling a just-created subscription cancels a still-pending CCCD write.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions