libtailscale/swift/TailscaleKit/LocalAPI/MessageReader.swift

130 lines
4.3 KiB
Swift
Raw Permalink Normal View History

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import Foundation
enum MessageQueueError: Error {
case queueCongested
}
/// The maximum number of unprocessed messages that can be queued before we start discarding
/// This needs to be large enough to handle the bursty "first time" connection messages but
/// small enough to avoid our memory footprint growing arbitrarily large.
let kMaxQueueSize = 24
/// Provides a queue for incoming messages on the IPN bus. This will keep a maximum of
/// the last kMaxQueueSize inbound messages pending processing. If the queue is congested, we will
/// stop queueing messages and throw an error once the queue has been drained.
final class MessageReader: NSObject, URLSessionDataDelegate, @unchecked Sendable {
/// All mutation and reading of local state happens in workQueue.
let workQueue = OperationQueue()
/// Holds partial incoming messages
var buffer: Data = Data()
var ipnWatchSession: URLSession?
var dataTask: URLSessionDataTask?
var logger: LogSink?
/// FIFO queue for messages awaiting processing
var pendingMessages: [Data] = []
/// Once congested, we will allow the processor to empty the queue, but we will stop queueing messages.
/// consume()ing the last messages will trigger a MessageQueueError.queueCongested error which the
/// upstream consumer can use. Typically, this means we lost messages, so the correct action is to
/// restart the processor and queue with an .initialState flag.
var congested = false
var errorHandler: (@Sendable (Error) -> Void)?
init(logger: LogSink? = nil) {
self.logger = logger
workQueue.maxConcurrentOperationCount = 1
workQueue.name = "io.tailscale.ipn.MessageReader.workQueue"
}
func stop() {
ipnWatchSession?.invalidateAndCancel()
workQueue.cancelAllOperations()
}
func start(_ request: URLRequest, config: URLSessionConfiguration, errorHandler: @escaping @Sendable (Error) -> Void ) {
workQueue.addOperation { [weak self] in
guard let self = self else { return }
self.errorHandler = errorHandler
buffer = Data()
pendingMessages = []
congested = false
dataTask?.cancel()
ipnWatchSession?.invalidateAndCancel()
ipnWatchSession = URLSession(configuration: config,
delegate: self,
delegateQueue: workQueue)
dataTask = ipnWatchSession?.dataTask(with: request)
dataTask?.resume()
}
}
func consume(_ completion: @escaping @Sendable (Data?) -> Void) {
workQueue.addOperation { [weak self] in
guard let self else { return }
if congested && pendingMessages.count == 0 {
errorHandler?(MessageQueueError.queueCongested)
completion(nil)
return
}
guard pendingMessages.count > 0 else {
completion(nil)
return
}
completion(pendingMessages.removeFirst())
}
}
// MARK: - URLSessionDataDelegate
func urlSession(_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?) {
if let error = error {
let nsError = error as NSError
// Ignore cancellation errors, those are deliberate.
if nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorCancelled {
return
}
errorHandler?(error)
}
}
func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive data: Data) {
if congested {
return
}
receiveData(data)
}
func receiveData(_ data: Data) {
workQueue.addOperation { [weak self] in
guard let self else { return }
buffer.append(data)
if buffer[buffer.count - 1] == kJsonNewline {
if pendingMessages.count >= kMaxQueueSize {
congested = true
return
}
pendingMessages.append(buffer)
buffer.removeAll(keepingCapacity: true)
}
}
}
}