From 8529fe3c8d1bd19ad40f4a2b614e97c624f05a21 Mon Sep 17 00:00:00 2001 From: Simon Whitty Date: Tue, 4 Feb 2025 21:54:32 +1100 Subject: [PATCH 1/3] AsyncBufferedPrefixSequence --- .../Sources/AsyncBufferedPrefixSequence.swift | 79 ++++++++++++++ .../AsyncBufferedPrefixSequenceTests.swift | 101 ++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 FlyingSocks/Sources/AsyncBufferedPrefixSequence.swift create mode 100644 FlyingSocks/Tests/AsyncBufferedPrefixSequenceTests.swift diff --git a/FlyingSocks/Sources/AsyncBufferedPrefixSequence.swift b/FlyingSocks/Sources/AsyncBufferedPrefixSequence.swift new file mode 100644 index 0000000..851eb59 --- /dev/null +++ b/FlyingSocks/Sources/AsyncBufferedPrefixSequence.swift @@ -0,0 +1,79 @@ +// +// AsyncBufferedPrefixSequence.swift +// FlyingFox +// +// Created by Simon Whitty on 04/02/2025. +// Copyright © 2025 Simon Whitty. All rights reserved. +// +// Distributed under the permissive MIT license +// Get the latest version from here: +// +// https://github.com/swhitty/FlyingFox +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +package struct AsyncBufferedPrefixSequence: AsyncBufferedSequence { + private let base: Base + private let count: Int + + package init(base: Base, count: Int) { + self.base = base + self.count = count + } + + package func makeAsyncIterator() -> Iterator { + Iterator(iterator: base.makeAsyncIterator(), remaining: count) + } + + package struct Iterator: AsyncBufferedIteratorProtocol { + private var iterator: Base.AsyncIterator + private var remaining: Int + + init (iterator: Base.AsyncIterator, remaining: Int) { + self.iterator = iterator + self.remaining = remaining + } + + package mutating func next() async throws -> Base.Element? { + guard remaining > 0 else { return nil } + + if let element = try await iterator.next() { + remaining -= 1 + return element + } else { + remaining = 0 + return nil + } + } + + package mutating func nextBuffer(suggested count: Int) async throws -> Base.AsyncIterator.Buffer? { + guard remaining > 0 else { return nil } + + let count = Swift.min(remaining, count) + if let buffer = try await iterator.nextBuffer(suggested: count) { + remaining -= buffer.count + return buffer + } else { + remaining = 0 + return nil + } + } + } +} diff --git a/FlyingSocks/Tests/AsyncBufferedPrefixSequenceTests.swift b/FlyingSocks/Tests/AsyncBufferedPrefixSequenceTests.swift new file mode 100644 index 0000000..de63bf5 --- /dev/null +++ b/FlyingSocks/Tests/AsyncBufferedPrefixSequenceTests.swift @@ -0,0 +1,101 @@ +// +// AsyncBufferedPrefixSequenceTests.swift +// FlyingFox +// +// Created by Simon Whitty on 04/02/2025. +// Copyright © 2025 Simon Whitty. All rights reserved. +// +// Distributed under the permissive MIT license +// Get the latest version from here: +// +// https://github.com/swhitty/FlyingFox +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +@testable import FlyingSocks +import Foundation +import Testing + +struct AsyncBufferedPrefixSequenceTests { + + @Test + func next_terminates_after_count() async throws { + let buffer = AsyncBufferedCollection(["a", "b", "c", "d", "e", "f"]) + var prefix = AsyncBufferedPrefixSequence(base: buffer, count: 4).makeAsyncIterator() + + #expect( + try await prefix.next() == "a" + ) + #expect( + try await prefix.next() == "b" + ) + #expect( + try await prefix.next() == "c" + ) + #expect( + try await prefix.next() == "d" + ) + #expect( + try await prefix.next() == nil + ) + } + + @Test + func nextBuffer_terminates_after_count() async throws { + let buffer = AsyncBufferedCollection(["a", "b", "c", "d", "e", "f"]) + var prefix = AsyncBufferedPrefixSequence(base: buffer, count: 4).makeAsyncIterator() + + #expect( + try await prefix.nextBuffer(suggested: 3) == ["a", "b", "c"] + ) + #expect( + try await prefix.nextBuffer(suggested: 3) == ["d"] + ) + #expect( + try await prefix.nextBuffer(suggested: 3) == nil + ) + } + + @Test + func next_terminates_when_base_terminates() async throws { + let buffer = AsyncBufferedCollection(["a"]) + var prefix = AsyncBufferedPrefixSequence(base: buffer, count: 2).makeAsyncIterator() + + #expect( + try await prefix.next() == "a" + ) + #expect( + try await prefix.next() == nil + ) + } + + @Test + func nextBuffer_terminates_when_base_terminates() async throws { + let buffer = AsyncBufferedCollection(["a"]) + var prefix = AsyncBufferedPrefixSequence(base: buffer, count: 10).makeAsyncIterator() + + #expect( + try await prefix.nextBuffer(suggested: 3) == ["a"] + ) + #expect( + try await prefix.nextBuffer(suggested: 3) == nil + ) + } +} From 8ac0d927eb39fbf6b3c14984316cb4fa75edba9f Mon Sep 17 00:00:00 2001 From: Simon Whitty Date: Tue, 4 Feb 2025 21:55:24 +1100 Subject: [PATCH 2/3] Use AsyncBufferedPrefixSequence for large requests --- FlyingFox/Sources/HTTPDecoder.swift | 11 ++------ FlyingFox/Tests/HTTPServerTests.swift | 37 ++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/FlyingFox/Sources/HTTPDecoder.swift b/FlyingFox/Sources/HTTPDecoder.swift index ee8c60e..3e0649f 100644 --- a/FlyingFox/Sources/HTTPDecoder.swift +++ b/FlyingFox/Sources/HTTPDecoder.swift @@ -122,17 +122,10 @@ struct HTTPDecoder { if length <= sharedRequestReplaySize { return HTTPBodySequence(shared: bytes, count: length, suggestedBufferSize: 4096) } else { - return HTTPBodySequence(from: bytes, count: length, suggestedBufferSize: 4096) + let prefix = AsyncBufferedPrefixSequence(base: bytes, count: length) + return HTTPBodySequence(from: prefix, count: length, suggestedBufferSize: 4096) } } - - func makeBodyData(from bytes: some AsyncBufferedSequence, length: Int) async throws -> Data { - var iterator = bytes.makeAsyncIterator() - guard let buffer = try await iterator.nextBuffer(count: length) else { - throw Error("AsyncBufferedSequence prematurely ended") - } - return Data(buffer) - } } extension HTTPDecoder { diff --git a/FlyingFox/Tests/HTTPServerTests.swift b/FlyingFox/Tests/HTTPServerTests.swift index df1209c..f13f249 100644 --- a/FlyingFox/Tests/HTTPServerTests.swift +++ b/FlyingFox/Tests/HTTPServerTests.swift @@ -184,6 +184,29 @@ actor HTTPServerTests { ) } + @Test + func requests_larger_than_shared_buffer() async throws { + // given + let server = HTTPServer.make(sharedRequestReplaySize: 100) + let port = try await startServerWithPort(server, preferConnectionsDiscarding: true) + + await server.appendRoute("/fish") { req in + let count = try await req.bodyData.count + return HTTPResponse(statusCode: .ok, body: "\(count) bytes".data(using: .utf8)!) + } + + // when + var request = URLRequest(url: URL(string: "http://localhost:\(port)/fish")!) + request.httpMethod = "POST" + request.httpBody = Data(repeating: 0x0, count: 200) + let (body, _) = try await URLSession.shared.data(for: request) + + // then + #expect( + String(data: body, encoding: .utf8) == "200 bytes" + ) + } + @Test func connections_AreHandled_FallbackTaskGroup() async throws { let server = HTTPServer.make() @@ -539,12 +562,18 @@ extension HTTPServer { static func make(port: UInt16 = 0, timeout: TimeInterval = 15, + sharedRequestReplaySize: Int? = nil, logger: some Logging = .disabled, handler: (any HTTPHandler)? = nil) -> HTTPServer { - HTTPServer(port: port, - timeout: timeout, - logger: logger, - handler: handler) + var config = Configuration( + port: port, + timeout: timeout, + logger: logger + ) + if let sharedRequestReplaySize { + config.sharedRequestReplaySize = sharedRequestReplaySize + } + return HTTPServer(config: config, handler: handler) } static func make(port: UInt16 = 0, From 423116511fb5a7386943a481dc65aeba016cd6f3 Mon Sep 17 00:00:00 2001 From: Simon Whitty Date: Tue, 4 Feb 2025 22:12:30 +1100 Subject: [PATCH 3/3] Fix Swift 5 --- FlyingSocks/Sources/AsyncBufferedPrefixSequence.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FlyingSocks/Sources/AsyncBufferedPrefixSequence.swift b/FlyingSocks/Sources/AsyncBufferedPrefixSequence.swift index 851eb59..5a618c1 100644 --- a/FlyingSocks/Sources/AsyncBufferedPrefixSequence.swift +++ b/FlyingSocks/Sources/AsyncBufferedPrefixSequence.swift @@ -30,6 +30,8 @@ // package struct AsyncBufferedPrefixSequence: AsyncBufferedSequence { + package typealias Element = Base.Element + private let base: Base private let count: Int