Repository: apple/swift-nio
File: Sources/NIOTCPEchoServer/Server.swift (Swift)

Code before:
        let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)

        let server = Server(
            host: "localhost",
            port: 8765,
            eventLoopGroup: eventLoopGroup
        )
        try await server.run()
    }

    /// This method starts the server and handles incoming connections.
    func run() async throws {
        let channel = try await ServerBootstrap(group: eventLoopGroup)
            .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
            .bind(
                host: self.host,
                port: self.port,
                childChannelConfiguration: .init(
                    inboundType: ByteBuffer.self,
                    outboundType: ByteBuffer.self
                )
            )

        // We are handling each incoming connection in a separate child task. It is important
        // to use a discarding task group here which automatically discards finished child tasks
        // otherwise this task group would end up leaking memory of all finished connection tasks.
        try await withThrowingDiscardingTaskGroup { group in
            for try await connectionChannel in channel.inboundStream {
                group.addTask {
                    print("Handling new connection")
                    await self.handleConnection(channel: connectionChannel)
                    print("Done handling connection")
                }
            }
        }
    }

    /// This method handles a single connection by echoing back all inbound data.
    private func handleConnection(channel: NIOAsyncChannel<ByteBuffer, ByteBuffer>) async {
        // Note that this method is non-throwing and we are catching any error.
        // We do this since we don't want to tear down the whole server when a single connection
        // encounters an error.
        do {
            for try await inboundData in channel.inboundStream {
                print("Received request (\(String(buffer: inboundData)))")
                try await channel.outboundWriter.write(inboundData)
            }
        } catch {
            print("Hit error: \(error)")
        }
    }

Review comment (suggestion):
I think we should elaborate on this a bit because it makes it sound like task groups leak! We don't leak memory if we use a task group, we just don't free it until the task group exits or we consume the results. 

Code after:
    /// This method starts the server and handles incoming connections.
    func run() async throws {
        let channel = try await ServerBootstrap(group: self.eventLoopGroup)
            .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
            .bind(
                host: self.host,
                port: self.port
            ) { channel in
                channel.eventLoop.makeCompletedFuture {
                    // We are using two simple handlers here to frame our messages with "\n"
                    try channel.pipeline.syncOperations.addHandler(ByteToMessageHandler(NewlineDelimiterCoder()))
                    try channel.pipeline.syncOperations.addHandler(MessageToByteHandler(NewlineDelimiterCoder()))

                    return try NIOAsyncChannel(
                        synchronouslyWrapping: channel,
                        configuration: NIOAsyncChannel.Configuration(
                            inboundType: String.self,
                            outboundType: String.self
                        )
                    )
                }
            }

        // We are handling each incoming connection in a separate child task. It is important
        // to use a discarding task group here which automatically discards finished child tasks.
        // A normal task group retains all child tasks and their outputs in memory until they are
        // consumed by iterating the group or by exiting the group. Since, we are never consuming
        // the results of the group we need the group to automatically discard them; otherwise, this
        // would result in a memory leak over time.
        try await withThrowingDiscardingTaskGroup { group in
            for try await connectionChannel in channel.inboundStream {
                group.addTask {
                    print("Handling new connection")
                    await self.handleConnection(channel: connectionChannel)
                    print("Done handling connection")
                }
            }
        }
    }

    /// This method handles a single connection by echoing back all inbound data.
    private func handleConnection(channel: NIOAsyncChannel<String, String>) async {
        // Note that this method is non-throwing and we are catching any error.
        // We do this since we don't want to tear down the whole server when a single connection
        // encounters an error.
        do {
            for try await inboundData in channel.inboundStream {
                print("Received request (\(inboundData))")
                try await channel.outboundWriter.write(inboundData)
            }
        } catch {