FreeOpcUa / opcua-asyncio

OPC UA library for python >= 3.7
GNU Lesser General Public License v3.0
1.11k stars 358 forks source link

asyncio.run() consecutively for connection and node browsing doesn't work #1541

Closed Ajinkya-Suranis closed 9 months ago

Ajinkya-Suranis commented 9 months ago

I've written a simple class to connect to Prosys simulation server and browse tags. Following is the sample code:

class UAClient:
    def __init__(self):
        super().__init__()
        self.url = "opc.tcp://DESKTOP-33QTGC2:53530/OPCUA/SimulationServer"
        self.client = None

    async def connect_to_server_async(self):
        self.client = Client(url=self.url)
        await self.client.connect()

    def connect_to_server(self):
        asyncio.run(self.connect_to_server_async())

    async def browse_level1_nodes_async(self):
        obj_node = self.client.get_objects_node()
        children = await obj_node.get_children()
        resp = {"children": []}
        for child in children:
            br_name = await child.read_browse_name()
            node_class = await child.read_node_class()
            node_type = "object" if node_class == NodeClass.Variable else "variable"
            child_data = {"name": br_name, "node_id": br_name.to_string(), "node_type": node_type}
            resp["children"].append(child_data)
        print(resp)

    def browse_level1_nodes(self):
        asyncio.run(self.browse_level1_nodes_async())

if __name__ == "__main__":
    obj = UAClient()
    obj.connect_to_server()
    obj.browse_level1_nodes()

As we can see, first the connection is established with server using connect_to_server() method and it waits till completion using asyncio.run(). Then, level 1 children are browsed using browse_level1_nodes() and again asyncio.run() is used to wait for completion. However when the program is run, following exception is raised:

Error while renewing session
Traceback (most recent call last):
  File "C:\Users\surni\AppData\Roaming\Python\Python37\site-packages\asyncua\client\client.py", line 572, in _renew_channel_loop
    await asyncio.sleep(duration)
  File "C:\Program Files\Itanta\Python37\lib\asyncio\tasks.py", line 595, in sleep
    return await future
concurrent.futures._base.CancelledError
Error in watchdog loop
Traceback (most recent call last):
  File "C:\Users\surni\AppData\Roaming\Python\Python37\site-packages\asyncua\client\client.py", line 549, in _monitor_server_loop
    await asyncio.sleep(timeout)
  File "C:\Program Files\Itanta\Python37\lib\asyncio\tasks.py", line 595, in sleep
    return await future
concurrent.futures._base.CancelledError
Traceback (most recent call last):
  File "browse_nodes_github.py", line 38, in <module>
    obj.browse_level1_nodes()
  File "browse_nodes_github.py", line 32, in browse_level1_nodes
    asyncio.run(self.browse_level1_nodes_async())
  File "C:\Program Files\Itanta\Python37\lib\asyncio\runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "C:\Program Files\Itanta\Python37\lib\asyncio\base_events.py", line 579, in run_until_complete
    return future.result()
concurrent.futures._base.CancelledError

When both connection and node browsing is inside a single function, it works properly.

Expected behavior: Browsing nodes should work after connection is successful, when called one after other.

oroulet commented 9 months ago

That is not how async programming works. Try a few python asyncio tutorial first

Ajinkya-Suranis commented 9 months ago

That is not how async programming works. Try a few python asyncio tutorial first

I've already gone through basic tutorial. My main program is synchronous, and it needs to execute asynchronous coroutines (connect, browse nodes, read values etc) whenever necessary. That's why asyncio.run() is used for execution at those places. I've used similar approach at multiple places in my program (other than opcua) and it works perfectly. My expectation is - Using asyncio.run() should always work in a synchronous program.

AndreasHeine commented 9 months ago

use the sync wrapper if your code is sync

Ajinkya-Suranis commented 9 months ago

use the sync wrapper if your code is sync

asyncio.run() would act like a sync wrapper, if I'm not wrong. Can you suggest an alternative?

oroulet commented 9 months ago

Calling async code from sync is not that easy. You need a loop running in a thread. But luckily for you I have done that work and you can use the sync wrappers. Look at the sync examples in repository

Ajinkya-Suranis commented 9 months ago

Calling async code from sync is not that easy. You need a loop running in a thread. But luckily for you I have done that work and you can use the sync wrappers. Look at the sync examples in repository

Thanks a lot. Being a newbie in asyncio, I didn't quite give a lot of thought in sync->async handling before opening the issue. BTW, the run_until_complete() method of asyncio worked in this case, since it runs event loop till completion.