i have channel handlers that can experience a variety of errors that require the channel to be closed. for example:
public
func errorCaught(context:ChannelHandlerContext, error:any Error)
{
context.fireErrorCaught(error)
self.state = .perished
}
public
func write(context:ChannelHandlerContext, data:NIOAny,
promise:EventLoopPromise<Void>?)
{
switch self.unwrapOutboundIn(data)
{
case .interrupt:
context.channel.close(mode: .all, promise: nil)
self.state = .perished
...
since there is no way to (synchronously) access the state of an individual channel handler within an any Channel, i’ve just been checking Channel.isWritable to determine if a channel should be disposed of or returned to its parent pool after running a network request.
but because context.channel.close(mode:promise:) doesn’t close the channel immediately, there is still a possibility to checking in a perished channel to the connection pool, which the application believes is usable but will never be able to run a successful network request.
If you are not on the Channel event loop then you cannot be confident that the Channel is healthy at any given point in time, because absent synchronisation there is no shared point in time. More importantly, if you're pooling channels, you already need to tolerate the possibility that the Channel was healthy when you checked, but became unhealthy in the time between checking and handing it off to the pool. That is, you have an unavoidable TOCTOU issue.
Instead, my recommendation is to attach a callback to the Channel's closeFuture that will fire and notify the pool. This will allow the pool to prune connections that die while they're pooled, and it will allow you to keep track of the channel's death/liveness state.
Sidebar: you should flip your channel handler's state first, and call out second. Channel pipeline events are dispatched synchronously, so you can be re-entrantly called, and getting this behaviour wrong can lead to subtle bugs.
// running in an actor-isolated pool method
do
{
self.connections.pending += 1
// ... establish connection ...
let channel:any Channel
switch self.state
{
case .filling:
channel.closeFuture.whenComplete
{
_ in
let _:Task<Void, Never> = .init
{
await self.replace(perished: channel)
}
}
self.connections.insert(channel)
self.connections.pending -= 1
is it safe to capture a reference to a channel in a closure and then store that closure in the channel itself? i am doing this because i am using Channel’s AnyObject constraint to identify the channels by ObjectIdentifier.
Yes, this is safe. The closeFuture will 100% deterministically be invoked when the channel is closed, at which point the closure will be dropped. If the channel was already closed when the closeFuture was attached it will be invoked immediately and then dropped. This will break the created reference cycle.