Python asyncua Synchronous Subscription Troubleshooting

Jason IP3 min read
OPC / OPC UAOther ManufacturerTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

The subscription combines asyncua.sync.Client with an async processing loop. The queue separates the synchronous notification callback from application processing, but the calling context, callback thread, and requested subscription period must be verified before using this pattern in Django.

Identify the synchronous and asynchronous boundaries

check_data_acknowledge() is declared with async def, so calling it from synchronous code returns a coroutine; it does not execute the connection or subscription by itself. A synchronous entry point with no active event loop can execute that coroutine with asyncio.run(check_data_acknowledge(payload)). Code already running in an asynchronous Django context must instead use await check_data_acknowledge(payload).

The function uses the synchronous client's connect(), create_subscription(), subscribe_data_change(), and disconnect() methods inside the coroutine. These calls can block the event-loop thread while they execute. The evidence does not establish their duration, so measure event-loop responsiveness during connection, subscription, and disconnection before deploying the design.

Process queued data-change notifications

datachange_notification() places [node, value, data] into an asyncio.Queue. The processing loop drains all currently available items, catches asyncio.QueueEmpty, and waits 0.1 seconds before polling again. This creates a processing delay of up to approximately 100 ms after an item arrives, excluding connection, callback, and application-processing time.

while True:
    await handler.process()
    await asyncio.sleep(0.1)

Confirm whether the synchronous subscription invokes the handler on the same event-loop thread. If it invokes the callback on another thread, direct use of asyncio.Queue.put_nowait() is not a verified cross-thread handoff; schedule the enqueue operation on the owning event loop with its thread-safe callback mechanism.

Validate subscription configuration and cleanup

Item Configured value or behavior Required verification
Client timeout argument 600000 The evidence does not state the unit or intended timeout behavior; verify it against the installed client API.
Subscription period 0 Confirm the effective publishing interval rather than assuming that zero means immediate delivery.
Processing interval 0.1 seconds Measure whether 100 ms polling meets the application's acknowledgment latency.
Node identifier ns={};s={} Verify the configured namespace index and the symbolic identifier after replacing LineName.
Shutdown disconnect() in finally Exercise cancellation and connection-failure paths and confirm that disconnection completes.

The notification type annotation is imported from opcua.common.subscription, while the handler, node, and client come from asyncua. The evidence does not prove that these types are interchangeable. Use the notification type supplied by the installed asyncua API, or remove the annotation until compatibility is confirmed.

Troubleshooting procedure

  1. Determine whether the caller is synchronous or already running in an event loop. Use asyncio.run() only for the former; use await for the latter.
  2. Log the callback thread and event-loop thread. If they differ, replace the direct queue write with a thread-safe handoff to the queue's event loop.
  3. Measure the duration of each synchronous client operation and monitor whether other asynchronous Django work stalls.
  4. Record the effective notification and processing intervals. Do not infer them from period=0; the only explicit application polling interval is 0.1 seconds.
  5. Trigger a node change, verify that the callback queues it, confirm that node.get_path(as_string=True) resolves, and then test exception and cancellation paths to verify disconnection.

FAQ

How do I call an asyncua coroutine from synchronous Django code?

If no event loop is active in that thread, call asyncio.run(check_data_acknowledge(payload)). In an asynchronous Django context, call it with await instead.

Why can an asyncua subscription block other async work?

The coroutine directly calls synchronous client methods, including connect() and disconnect(). Measure those calls because they execute on the event-loop thread in the shown design.

How often does this asyncua handler process notifications?

The loop drains the queue and then sleeps for 0.1 seconds, so polling adds up to approximately 100 ms of delay. The effective OPC UA publishing interval cannot be determined from the supplied period=0 setting alone.

Back to blog