How to wrap the initialization ceremony with a client

Still, experimenting with SwiftNIO, this time with a simple StatsD client:

There is quite a bit of ceremony to initialize a NIO channel: setup event-loop group, bootstrap, create the channel. I was wondering about the best way to encapsulate this within a client library and hide the complexity from the users. The best I came up with is this:

i.e. initialize the group outside of the client and pass it to the constructor. Still not as clean as I would like. Any suggestions?

I believe this pattern is generally the right thing to do, actually.

Libraries should almost never choose to own their own EventLoopGroup. This is because it's highly valuable to have an extremely small number of groups in an application; in fact, for many applications you may want only one.

To understand this better, consider a large application that combines Vapor, your StatsD client, and some other hypothetical protocol clients such as Redis and HTTP. Each of these libraries may be separate, from a separate source.

If each library initialises its own event loop groups, then each library has a completely isolated set of threads to handle its I/O. If one of these libraries is busiest (e.g. Vapor, handling all inbound load), the threads for that library will be extremely busy, while threads for the other libraries will be less busy. In this kind of world, you want one of two things:

  1. You may want separate groups for each library, to isolate them from each other. That's fine, but in this case you'd want more threads for Vapor than for, say, Redis. How can the library know this?
  2. You may want to have one, large group of threads, handling heterogenous load. This means that a particularly "hot" connection (e.g statsD) can potentially starve the connections sharing the loop with it, but the flip side is that quiet connections (e.g. HTTP client connections) can take up small slices of idle time on the otherwise busy loops.

Exactly what distribution of loops is best for an application is not generally possible for a library author to know ahead of time. As a result, the best thing to do is to let the application author decide. You can certainly publish documentation that provides suggested configurations, and potentially even helpers if obtaining this configuration is complex, but in general I think this is the basic API to use.

3 Likes

OK, thank you for the detailed (and reassuring) answer.

1 Like

In my stuff (e.g. swift-nio-redis, swift-nio-irc) I always make the group an option. If it is set, the given group is used, if not, a private group. Since globals are created lazy in Swift, this is pretty easy to do. Example: ConnectOptions.

Another thing to consider is that quite often you want to "stay" on an event loop and avoid loop hopping. I.e. your client object may not want to access the "load balancing" group, but the currently active loop.
For example, if you have a NIO web server which wants to store something in Redis, you'd want to have the Redis client object on the same thread like the request handler. This is why you have this in the sample above:

    self.eventLoopGroup = eventLoopGroup
                       ?? MultiThreadedEventLoopGroup.currentEventLoop
                       ?? onDemandSharedEventLoopGroup

In short: If an event loop group was passed in, use it. If the object is created from within an event loop thread, stay on that specific loop. If neither (e.g. client created from a standalone app) use an own loop group. IMO maximum flexibility paired with maximum convenience ;-)

A funny thing to note is that an EventLoop is also an EventLoopGroup (it yields itself when asked for an EventLoop). Very convenient.

Finally I recommend to split the protocol handling (NIO channel handlers) from the client object. E.g. in swift-nio-redis there are two modules: NIORedis and Redis. NIORedis just contains the protocol parsers and the associated model types. While Redis just does all the convenience hooks to setup the connection and configure the pipeline. Why? Because the protocol stuff can be reused in servers.

4 Likes

Great hints, thank you!

One small question: Who calls

onDemandSharedEventLoopGroup.syncShutdownGracefully()

to properly clean up?

No one. This is a singleton. If you actually need a clean shutdown (e.g. when running things in tests), pass in your own event loop group.

BTW: You could also do this in atexit if you wanted to. But explicit groups are probably the recommended approach when doing production apps.

1 Like