Observe URLSession operation queue count?

664 views Asked by At

I am using a URLSession setup this way:

public struct NetworkSession {
    
    public var session: URLSession
    public let sessionOperationQueue = OperationQueue()
    
    public init() {
        
        let sessionConfiguration = URLSessionConfiguration.default
        sessionConfiguration.timeoutIntervalForRequest = 20
        sessionOperationQueue.maxConcurrentOperationCount = OperationQueue.defaultMaxConcurrentOperationCount
        sessionOperationQueue.qualityOfService = .userInitiated
        session = URLSession(configuration: sessionConfiguration, delegate: nil, delegateQueue:sessionOperationQueue)
            
    }

    .....

}

I would like to observe the count of tasks found in the queue.

I tried using Combine:

    sessionOperationQueue.publisher(for: \.operationCount).sink { count in
        print("operations count: \(count)")
    }
    .store(in: &subscribers)

But that only prints 0 at init and never updates as requests start and complete.

How can I monitor the number of tasks found in the queue?

1

There are 1 answers

0
Rob On BEST ANSWER

tl;dr

Observing the operation count on the session’s queue will not achieve what you want.

  • In traditional URLSession code, the queue is used for the individual delegate methods, not to wrap the whole request-response.
  • If using the new async-await rendition, the operation queue is not used at all (which is moot, given the previous observation).

Bottom line, while URLSession has a method to inquire what pending requests are in progress, it does not, AFAIK, have an observable property for this (unless, of course, you abandon completion handlers and use only the delegate renditions). So, if you want to dynamically track of the count of pending requests, just keep track of this yourself. The asynchronous custom Operation subclass pattern seems like overkill (but is outlined in the Operation section of this answer). It would be easiest to simply route all my network requests through a method that increments a counter as requests come in and decrements it upon completion.


Long answer with code samples

You can use KVO to observe changes of the queue’s operationCount (see below), but that is not going to achieve what you want. This is not an operation that wraps the whole network request and response, but rather individual operations for the individual session delegate and completion handler callbacks.

E.g., consider:

class ViewController: UIViewController {

    lazy var session: URLSession = {
        let configuration = URLSessionConfiguration.default
        configuration.timeoutIntervalForRequest = 20
        return URLSession(configuration: configuration, delegate: nil, delegateQueue: queue)
    }()

    let queue: OperationQueue = {
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1
        queue.qualityOfService = .userInitiated
        return queue
    }()

    var observer: NSKeyValueObservation?

    override func viewDidLoad() {
        super.viewDidLoad()

        observer = queue.observe(\.operationCount, options: .new) { queue, change in
            print("Observer reported operationCount =", change.newValue!)
        }

        for i in 1000..<1010 {
            let url = URL(string: "https://httpbin.org/get?value=\(i)")!
            session.dataTask(with: url) { data, _, error in
                guard let data = data else {
                    print(error ?? URLError(.badServerResponse))
                    return
                }

                print("Request", i, "returned", data.count, "bytes")
            }.resume()
        }
    }
}

That produces:

Observer reported operationCount = 1
Observer reported operationCount = 2
Observer reported operationCount = 3
Request 1000 returned 405 bytes
Observer reported operationCount = 4
Observer reported operationCount = 3
Request 1002 returned 405 bytes
Observer reported operationCount = 2
Request 1004 returned 405 bytes
Observer reported operationCount = 1
Request 1001 returned 405 bytes
Observer reported operationCount = 0
Observer reported operationCount = 1
Observer reported operationCount = 2
Request 1006 returned 405 bytes
Observer reported operationCount = 3
Observer reported operationCount = 2
Observer reported operationCount = 3
Request 1005 returned 405 bytes
Observer reported operationCount = 4
Observer reported operationCount = 3
Observer reported operationCount = 4
Request 1003 returned 405 bytes
Observer reported operationCount = 3
Request 1008 returned 405 bytes
Observer reported operationCount = 2
Request 1007 returned 405 bytes
Observer reported operationCount = 1
Request 1009 returned 405 bytes
Observer reported operationCount = 0

Note, you never see it acknowledge that there are ten requests pending. The operationCount is reporting what’s on the delegate queue, which is not what you are looking for.

By the way, in the above, the delegate queue is serial (as advised in the documentation). The very fact that it is a serial queue allowing concurrent network requests is further evidence that there is not an operation wrapping the whole request, but rather is for the individual delegate callbacks.


As an interesting aside, if you use the new async-await URLSession methods, the operation queue is not used at all. That makes sense (given that it is using the new concurrency system), but it is not noted in the documentation at this point. Anyway, the below will does not trigger any operation count changes:

func startRequests() async throws {
    try await withThrowingTaskGroup(of: Void.self) { group in
        for i in 0..<4 {
            let url = URL(string: "https://httpbin.org/get?value=\(i)")!
            group.addTask {
                let (data, _) = try await self.session.data(from: url)
                print("Request", i, "returned", data.count, "bytes")
            }
        }

        try await group.waitForAll()
    }
}

But this is moot, given that the URLSession operation queue does not achieve what you want, regardless.