chrysn / aiocoap

The Python CoAP library
Other
266 stars 120 forks source link

Process of observer cancellation #186

Open pauluap opened 4 years ago

pauluap commented 4 years ago

I'm trying to understand what is supposed to happen during an observer cancellation.

Let's say I have the following code:

async def make_observer(self, key, callback):
        message = aiocoap.Message(mtype=aiocoap.NON, code=aiocoap.GET, uri=f'coap://{self.address}/model?id={key}')
        message.opt.observe = 0
        observation_is_over = asyncio.Future()
        requester = self.context.request(message)

        self.requesters.append(requester)
        requester.observation.register_errback(observation_is_over.set_result)
        requester.observation.register_callback(lambda data, key=key: callback(key, data))

        response_data = await requester.response
        callback(key, response_data)

       await observation_is_over

Things are moving fine and dandy, and some time later, I want to cancel the observation. I do this:

   requester.observation.cancel()

At this point, what is supposed to happen? I rather thought that the client would transmit a GET request with the observe option set to 1, but that doesn't appear to happen. Is the client waiting for a new update from the server so that it can reply with a RST?

I had been collecting the observation tasks and gathering them at the end like so

tasks = [asyncio.create_task(self.make_observer(k, v) for k, v in obs.items()]
...
# cancel all observers
...
await asyncio.gather(*tasks, return_exceptions=True)

The gathering seems to never complete, the run methods of Request don't seem to be returned to so that it can check the cancellation state and call self._plumbing_request.stop_interest() - that is if that's what's supposed to eventually happen

chrysn commented 4 years ago

There is an old issue around #13: aiocoap doesn't cancel observations but just starts ignoring them. Eventually, the server might send another update and the client would RST it, but it the client isn't keeping state around. This is not particularly nice to the server, but so far I didn't have the impression that it doesn't do much harm (so never priorized it).

In a usage pattern like this, where you expect something error-like to come back from the observation cancellation, this does bite you.

The cancellation in itself does not produce an error (as is documented), and does not necessarily produce a final event either (it might in future).

For a case like this, I'd recommend that when observation.cancel is called, you either set the observation_is_over, or cancel the task.

Alternatively (but that interface still has issues so you'll probably stick with the above), the observation object also supports asynchronous iteration, so you might want to replace the callback registrations and last line with an async for o in requester.observation: callback(key, o) -- but that does not terminate on cancellation either (which is a bug tracked in #187 now), and either way you'll need to cancel the task manually.

pauluap commented 4 years ago

Hmm. The server is under my control too, so I was thinking along the lines of recovering resources used for observations, but then again, I can't rely on clients being nice either :)

However, on the client side, I think that I still do have some issues - I establish a nontrivial number of observations, and during the lifetime of my client application, I can imagine that happening multiple times, so I feel that this is a memory leak as well, I'd have a lot of Request._run tasks floating around. Correct me if I'm wrong on any of those points.

I'd be okay with cancelling the tasks, but I believe that I don't have the ability to do so(?) In Request.__init__ it calls loop.create_task(self._run()) and doesn't preserve the returned task - and even if it did, Context.request() hides the actual Request object.

pauluap commented 4 years ago

Also, to clarify, I'm using the master branch, not the 0.3 PyPi release, so I'm not iterating over the observation, but rather using the interface on the master branch where I wait on observation cancellation and the message passing is done through a callback.