|
| 1 | +--- |
| 2 | +layout: page |
| 3 | +title: Using Structured Concurrency in server applications |
| 4 | +--- |
| 5 | + |
| 6 | +# Using Structured Concurrency in server applications |
| 7 | + |
| 8 | +Swift Concurrency enables writing safe concurrent, asynchronous and data race |
| 9 | +free code using native language features. Server systems are often highly |
| 10 | +concurrent to handle many different connections at the same time. This makes |
| 11 | +Swift Concurrency a perfect fit for use in server systems since it reduces the |
| 12 | +cognitive burden to write correct concurrent systems while spreading the work |
| 13 | +across all available cores. |
| 14 | + |
| 15 | +Structured Concurrency allows the developer to organize their code into |
| 16 | +high-level tasks and their child component tasks. These tasks are the primary |
| 17 | +unit of concurrency and enable the flow of information up and down the task |
| 18 | +hierarchy. |
| 19 | + |
| 20 | +This guide covers best practices around how Swift Concurrency should be used in |
| 21 | +server side applications and libraries. Importantly, this guide assumes a |
| 22 | +_perfect_ world where all libraries and applications are fully bought into |
| 23 | +Structured Concurrency. In reality, there are a lot of places where one has to |
| 24 | +bridge currently unstructured systems. Depending on how those unstructured |
| 25 | +systems are shaped there are various ways to bridge them (maybe include a |
| 26 | +section in the with common patterns). The goal of this guide is to define a |
| 27 | +target for the ecosystem which can be referred to when talking about the |
| 28 | +architecture of libraries and applications. |
| 29 | + |
| 30 | +## Structuring your application |
| 31 | + |
| 32 | +One can think of Structured Concurrency as a tree of task where the initial task |
| 33 | +is rooted at the `main()` entry point of the application. From this entry point |
| 34 | +onwards more and more child tasks are added to the tree to form the logical flow |
| 35 | +of data in the application. Organizing the whole program into a single task tree |
| 36 | +unlocks the full potential of Structured Concurrency such as: |
| 37 | + |
| 38 | +- Automatic task cancellation propagation |
| 39 | +- Propagation of task locals down the task tree |
| 40 | + |
| 41 | +When looking at a typical application it is often comprised out of multiple |
| 42 | +smaller components such as an HTTP server handling incoming traffic, |
| 43 | +observability backends sending events to external systems, and clients to |
| 44 | +databases. All of those components probably require one or more tasks to run |
| 45 | +their work and those tasks should be child tasks of the application's main task |
| 46 | +tree for the above-mentioned reasons. Broadly speaking libraries expose two |
| 47 | +kinds of APIs short lived almost request response like APIs e.g. |
| 48 | +`HTTPClient.get("http://example.com)` and long-lived APIs such as an HTTP server |
| 49 | +that accepts inbound connections. In reality, libraries often expose both since |
| 50 | +they need to have some long-lived connections and then request-response like |
| 51 | +behavior to interact with those connections, e.g. an `HTTPClient` will have a |
| 52 | +pool of connections and then dispatch requests onto them. |
| 53 | + |
| 54 | +The recommended pattern for those components and libraries is to expose a `func |
| 55 | +run() async throws` method on their types, such as an `HTTPClient`, inside this |
| 56 | +`run()` method libraries can schedule their long running work and spawn as many |
| 57 | +child tasks as they need. It is expected that those `run()` methods are often |
| 58 | +not returning until their parent task is cancelled or they receive a shutdown |
| 59 | +signal through some other mean. The other way that libraries could handle their |
| 60 | +long running work is by spawning unstructured tasks using the `Task.init(_:)` or |
| 61 | +`Task.detached(_:)`; however, this comes with significant downsides such as |
| 62 | +manual implementation of cancellation of those tasks or incorrect propagation of |
| 63 | +task locals. Moreover, unstructured tasks are often used in conjunction with |
| 64 | +`deinit`-based cleanup, i.e. the unstructured tasks are retained by the type and |
| 65 | +then cancelled in the `deinit`. Since `deinit`s of classes and actors are run at |
| 66 | +arbitrary times it becomes impossible to tell when resources created by those |
| 67 | +unstructured tasks are released. Since, the `run()` method pattern has come up a |
| 68 | +lot while migrating more libraries to take advantage of Structured Concurrency |
| 69 | +which lead the SSWG to update the [swift-service-lifecycle |
| 70 | +package](https://github.com/swift-server/swift-service-lifecycle) to embrace |
| 71 | +this pattern. In general, the SSWG recommends to adopt `ServiceLifecycle` and |
| 72 | +conform types of libraries to the `Service` protocol which makes it easier for |
| 73 | +application developers to orchestrate the various components that form the |
| 74 | +application's business logic. `ServiceLifecycle` provides the `ServiceGroup` |
| 75 | +which allows developers to orchestrate a number of `Service`s. Additionally, the |
| 76 | +`ServiceGroup` has built-in support for signal handling to implement a graceful |
| 77 | +shutdown of applications. Gracefully shutting down applications is often |
| 78 | +required in modern cloud environments during roll out of new application |
| 79 | +versions or infrastructure reformations. |
| 80 | + |
| 81 | +The goal of structuring libraries and applications like this is enabling a |
| 82 | +seamless integration between them and have a coherent interface across the |
| 83 | +ecosystem which makes adding new components to applications as easy as possible. |
| 84 | +Furthermore, since everything is inside the same task tree and task locals |
| 85 | +propagate down the tree we unlock new APIs in libraries such as `swift-log` or |
| 86 | +`swift-tracing`. |
| 87 | + |
| 88 | +After adopting this structure a common question question that comes up is how to |
| 89 | +model communication between the various components. This can be achieved in a |
| 90 | +couple of ways. Either by using dependency injection to pass one component to |
| 91 | +the other or by inverting the control between components using `AsyncSequence`s. |
| 92 | + |
| 93 | +## Resource management |
| 94 | + |
| 95 | +Applications often have to manage some kind of resource. Those could be file |
| 96 | +descriptors, sockets or something like virtual machines. Most resources need |
| 97 | +some kind of cleanup such as making a syscall to close a file descriptor or |
| 98 | +deleting a virtual machine. Resource management ties in closely with Structured |
| 99 | +Concurrency since it allows to express the lifetime of those resources in |
| 100 | +concurrent and asynchronous applications. The recommended pattern to provide |
| 101 | +access to resources is to use `with`-style methods such as the `func |
| 102 | +withTaskGroup(of:returning:body:)` method from the standard library. |
| 103 | +`with`-style methods allow to provide scoped access to a resource while making |
| 104 | +sure the resource is currently cleaned up at the end of the scope. For example a |
| 105 | +method providing scoped access to a file descriptor might look like this: |
| 106 | + |
| 107 | +```swift |
| 108 | +func withFileDescriptor<R>(_ body: (FileDescriptor) async throws -> R) async throws -> R { ... } |
| 109 | +``` |
| 110 | + |
| 111 | +Importantly, the file descriptor is only valid inside the `body` closure; hence, |
| 112 | +escaping the file descriptor in an unstructured task or into a background thread |
| 113 | +is not allowed. |
| 114 | + |
| 115 | +> Note: With future language features such as `~Escapable` types it might be |
| 116 | +possible to encode this constraint in the language itself. |
| 117 | + |
| 118 | +## Task executors in Swift on Server applications |
| 119 | + |
| 120 | +Most of the Swift on Server ecosystem is build on top of |
| 121 | +[swift-nio](https://github.com/apple/swift-nio) - a high performant event-driven |
| 122 | +networking library. `NIO` has its own concurrency model that predates Swift |
| 123 | +Concurrency; however, `NIO` offers the `NIOAsyncChannel` abstraction to bridge a |
| 124 | +`Channel` into a construct that can be interacted with from Swift Concurrency. |
| 125 | +One of the goals of the `NIOAsyncChannel`, is to enable developers to implement |
| 126 | +their business logic using Swift Concurrency instead of using `NIO`s lower level |
| 127 | +`Channel` and `ChannelHandler` types, hence, making `NIO` an implementation |
| 128 | +detail. You can read more about the `NIOAsyncChannel` in [NIO's Concurrency |
| 129 | +documentation](https://swiftpackageindex.com/apple/swift-nio/2.61.1/documentation/niocore/swift-concurrency). |
| 130 | + |
| 131 | +Highly performant server application often rely on handling incoming connections |
| 132 | +and requests almost synchronously without incurring unnecessary allocations or |
| 133 | +context switches. Swift Concurrency is by default executing any non-isolated |
| 134 | +method on the global concurrent executor. On the other hand, `NIO` has its own |
| 135 | +thread pool in the shape of an `EventLoopGroup`. `NIO` picks an `EventLoop` out |
| 136 | +of the `EventLoopGroup` for any `Channel` and executes all of the `Channel`s IO |
| 137 | +on that `EventLoop`. When bridging from `NIO` into Swift Concurrency by default |
| 138 | +the execution has to context switch between the `Channel`s `EventLoop` and one |
| 139 | +of the threads of the global concurrent executor. To avoid this context switch |
| 140 | +Swift Concurrency introduced the concept of preferred task executors in |
| 141 | +[SE-XXX](). When interacting with the `NIOAsyncChannel` the preferred task |
| 142 | +executor can be set to the `Channel`s `EventLoop`. If this is beneficial or |
| 143 | +disadvantageous for the performance of the application depends on a couple of |
| 144 | +factors: |
| 145 | + |
| 146 | +- How computationally intensive is the logic executed in Swift Concurrency? |
| 147 | +- Does the logic make any asynchronous out calls? |
| 148 | +- How many cores does that application have available? |
| 149 | + |
| 150 | +In the end, each application needs to measure its performance and understand if |
| 151 | +setting a preferred task executor is beneficial. |
0 commit comments