I am trying to write an Edge Driver for a device which emits Server-sent events.
If you are new to Server-sent-events you can find more on wikipedia, but in essence you open an http GET connection with the device and it sends you a stream of events forever. I.e. the TCP socket never closes.
I have a function which opens an http GET connection with the device and loops forever, calling socket.receive() to get the next line of the Server-sent eevnt stream, parses it and emits the appropriate ST event depending on the data received. This all works fine.
My question is how do I initiate the routine? Do I run it in its own thread? Do I initiate it with device.thread:call_with_delay()?, Do I use register_socket()?
My concern is it hogs all the CPU and does not allow the main thread to run, or it blocks the main thread altogether. Any thoughts would be appreciated.
You can run it in a loop using cosock.spawn. Your receive would keep it from running out of control since the receive call will wait for data. You only need to worry about the loop running wide open when the receive calls fail. Make sure the loop terminates if your device is removed or some other disconnect status occurs. ST has an implementation for this in one of their drivers. I ended up doing something a little different after experiencing a few minor issues with their implementation.
Thanks @blueyetisoftware! this is VERYhelpful advice. I will give it a go.
Occasionally the device closes the connection which causes the receive to error. How do I catch that in Lua. Do I use the try Clause?
@Tim99 As @Automated_House mentioned, you can see ST implementation of SSE in that repo. It works well if there are no issues. Where I ran into trouble with theirs is when there were issues with the connection. The basics are all there, it was just missing a bunch of error handling and there was room for optimization. For example, if the connection was dropped by the other devices, the loop would spin out of control trying to reconnect. You can check for errors on a tcp receive like this:
local recv, err, partial = sock:receive()
If there are errors, then err will be not nil. Depending on the error, you can decide if you want to reconnect, terminate your loop, or do something else. For example if err == 'closed' you would want to reconnect the socket and continue your loop, but only after waiting for a reconnect interval, otherwise it would go out of control.
Thanks @blueyetisoftware, SmartThings need to add extensive error handling.
I ended up writing my own SSE listener, based on the socket library and run it in its own thread.
It is essential to have extensive error handling as the sse listener runs autonomously.
For my device the SSE connection closes 1-2 time a day and I received the occasional timeout.
I caught the error from sock:receive() as you rightly identified above and if an error is identified I re-open a new connection and resend the Http request after sleeping for 30 seconds in case the device has to reboot.
I also implemented a setting/preference to manually stop/restart the sse listener.
Sounds about right. You may also want to check if your device gives you a preferred retry time by looking for a retry attribute in addition to the event, data and id attributes. That retry should become the new retry time after receiving that attribute. You also probably want to look at the status code of your initial http request for a temporary retry interval as well if that call fails.
if recv.status == 429 or recv.status == 503 then
local retry_after = recv:get_headers():get_one('Retry-After')
local retry = retry_after and tonumber(retry_after, 10)
...
end