Using Operations to manage imbalances between function calls

51 views Asked by At

I am writing a VideoPlayer() class that has a start() and stop() function to initiate playback for a given video. Each instance of VideoPlayer() manages a single video

The start() and stop() functions are asynchronous and I get notified via a delegate of success/failure. This delegation is done by a third party SDK.

class VideoPlayer {
    func start() {
     // Async call
    }

    func stop() {
     // Async call
    }

    // Callback from third party library with status
    func videoDidStart(view: UIView) {
       // Notify clients
     }
      
     func videoDidStop(stopped: Bool) {
       // Notify clients
     }

}

I have clients calling the start() and stop() function but sometimes it so happens that they are calling start/stop in quick successions. This leads to unexpected behavior. For example, if the clients call start() before a previous call to stop() has finished, the video won't play.

Ideally, the clients would wait till I send success/failure notification that I receive via the delegates.

But of course, that's not happening and I am tasked with making the VideoPlayer() class manage the imbalance's between start() and stop() and queue all the calls so everything executes in order.

I would like the VideoPlayer() class to ensure that every time there is an imbalance between start() and stop(), every start() is matched with a stop() and the rogue calls are put in a queue and execute them after the imbalance has been sorted out.

How do I manage such a thing? I believe I need to use Operations with a dependency between start() and stop(). Should I make start()/stop() a single operation and queue the rogue calls until the operation finishes.

Are there other approaches I could use? I have looked dispatch_groups and dispatch_barriers but I am not sure if they fit my use case.

Any help would be greatly appreciated. Thank you

1

There are 1 answers

0
matt On

I don't see why an operation is needed. As a crude example of the sort of thing you are describing, I made a small dispatcher architecture:

struct Start {
    let callback : () -> ()
    var started = false
}
class Dispatcher {
    var q = [Start]()
    func start(_ f: @escaping () -> ()) {
        q.append(Start(callback:f))
        self.startFirstOne()
    }
    func stop(_ f: @escaping () -> ()) {
        // assume this can only refer to the first one in the queue
        DispatchQueue.main.asyncAfter(deadline: .now()+DispatchTimeInterval.seconds(Int.random(in: 1...10))) {
            self.q.removeFirst()
            f()
            self.startFirstOne()
        }
    }
    private func startFirstOne() {
        guard !q.isEmpty else { return }
        guard !q[0].started else { return }
        print("starting")
        self.q[0].started = true
        DispatchQueue.main.asyncAfter(deadline: .now()+DispatchTimeInterval.seconds(Int.random(in: 1...10))) {
            self.q.first!.callback()
        }
    }
}

The start calls are queued up, and we don't actually start one until we have received sufficient stop calls to bring it to the front of the queue.

I personally don't like this at all, because we are making three very strong assumptions about contract-keeping:

  • we assume that everyone who calls start will also call stop,

  • and we are assuming that someone who calls start will not call stop until after being called back from the start call,

  • and we are assuming that no one who did not call start will ever call stop.

Nevertheless, if everyone does keep to the contract, then everyone's stop will be called back corresponding to that person's start. Here's a rough test bed, using random delays throughout to simulate asynchronicity:

let d = Dispatcher()
DispatchQueue.main.asyncAfter(deadline: .now()+DispatchTimeInterval.seconds(Int.random(in: 1...10))) {
    d.start {
        print("hearts start")
        DispatchQueue.main.asyncAfter(deadline: .now()+DispatchTimeInterval.seconds(Int.random(in: 1...10))) {
            d.stop { print("hearts stop") }
        }
    }
}
DispatchQueue.main.asyncAfter(deadline: .now()+DispatchTimeInterval.seconds(Int.random(in: 1...10))) {
    d.start {
        print("diamonds start")
        DispatchQueue.main.asyncAfter(deadline: .now()+DispatchTimeInterval.seconds(Int.random(in: 1...10))) {
            d.stop { print("diamonds stop") }
        }
    }
}
DispatchQueue.main.asyncAfter(deadline: .now()+DispatchTimeInterval.seconds(Int.random(in: 1...10))) {
    d.start {
        print("clubs start")
        DispatchQueue.main.asyncAfter(deadline: .now()+DispatchTimeInterval.seconds(Int.random(in: 1...10))) {
            d.stop { print("clubs stop") }
        }
    }
}
DispatchQueue.main.asyncAfter(deadline: .now()+DispatchTimeInterval.seconds(Int.random(in: 1...10))) {
    d.start {
        print("spades start")
        DispatchQueue.main.asyncAfter(deadline: .now()+DispatchTimeInterval.seconds(Int.random(in: 1...10))) {
            d.stop { print("spades stop") }
        }
    }
}

Try it, and you'll see that, whatever order and with whatever timing these things run, the start-stop pairs are all maintained by the dispatcher.

As I say, I don't like it. I would prefer to assume that callers might not adhere to the contract and be prepared to do something about it. But there it is, for what it's worth.