“Actor-isolated property can not be referenced from a Sendable closure”

I have the following code in an actor singleton, in an async method:

actor FurnaceManager {
    var modbus: …
    func start() async throws {
			self.app.eventLoopGroup.next().scheduleRepeatedTask(initialDelay: .zero, delay: .seconds(5))
			{ inTask in
				
				do
				{
					//	Connect MODBUS…
					
					self.app.logger.info("Attempting to connect to MODBUS")
					try self.modbus.connect()
					self.app.logger.info("Connected to MODBUS")
					inTask.cancel()				//	Don’t try to connect again
				}
				
				catch let e
				{
					self.app.logger.critical("Unabled to connect to MODBUS, \(e), trying again after delay…")
				}
			}
    }

I get a warning "Actor-isolated property 'modbus' can not be referenced from a Sendable closure" on the try self.modbus.connect() call. modbus is a class instance.

What's the best way to address this? Should I make another method on FurnaceManager to wrap the connect() call, and call that with await?

You could do that, if it's safe to await such a method from your NIO (?) event loop.

Do you need to use the event loop, though? If you can just run your task on the actor's executor, it'll have direct, safe access to modbus. e.g.:

actor FurnaceManager {
    var modbus: …
    var connectTask: Task<Void, any Error>? = nil

    func start() async {
        guard nil == connectTask else { return }

        connectTask = Task {
            while nil != connectTask && !Task.isCancelled {
                do {
                    self.app.logger.info("Attempting to connect to MODBUS")
                    try modbus.connect()
                    self.app.logger.info("Connected to MODBUS")
                    connectTask = nil
                } catch {
                    self.app.logger.critical("Unabled to connect to MODBUS, \(error), trying again after delay…")
                    try? await Task.sleep(nanoseconds: 5_000_000_000)
                }
            }
        }
    }
}
1 Like

Yes, there’s no reason I need the event loop. Your proposed solution works very well, thank you. IIUC, the only need for the connectTask property is to avoid creating two tasks if start() is called multiple times, is that right?

Correct. I don't know your expected behaviour or requirements, so adapt to suit.

1 Like

When I first wrote that bit of code, structured concurrency didn't exist, and so I chose the event loop timer as a way to periodically do a thing. Alternatives would've been Timer or Dispatch, I guess. Not sure why I chose, that; maybe just exploring Vapor/NIO.