Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lifetime-based producer resource management #334

Merged
merged 2 commits into from May 22, 2017
Merged

Conversation

andersio
Copy link
Member

@andersio andersio commented Apr 18, 2017

In RAS 1.0 and earlier, the start handler is passed a CompositeDisposable directly. While it is not an intended way to interrupt the producer, disposing of it can often, but not always, yield similar results as sending terminal events. It generally works only if the produced Signal has an upstream to be interrupted in the first place.

Related test case: 5d01764#diff-9cd786f9e52e786771d96cf8c42d3befL34

The PR proposes to close this unreliable semantic hole by using Lifetime instead.

init(_ startHandler: @escaping (Signal<Value, Error>.Observer, Lifetime) -> Void)

Alternative

The start handler may instead follow Signal.init to be (Observer<Value, Error>) -> Disposable?. While it achieves the same goal without requiring a new type, this however cannot be migrated due to tuple splat messing with overloading.

Possible follow-up

If this gets accepted, we may consider matching the generator closure of Signal.init with this for consistency. However, like the alternative above, such change cannot be migrated. That said it is expected to have less impact, since Signal.pipe is the more popular API while Signal.init is primarily used by operators.

@andersio andersio changed the title Producer disposable SignalProducer resource management Apr 18, 2017
@andersio andersio changed the title SignalProducer resource management CompositeDisposable and SignalProducer resource management Apr 18, 2017
@andersio andersio changed the title CompositeDisposable and SignalProducer resource management DisposableCollector interface for SignalProducer. Apr 19, 2017
@andersio andersio force-pushed the producer-disposable branch 3 times, most recently from 0a42d67 to 5d01764 Compare April 19, 2017 03:49
@andersio andersio modified the milestone: 2.0 May 5, 2017
@mdiep
Copy link
Contributor

mdiep commented May 10, 2017

I definitely think it makes sense to close this hole. I like that you split out #363—that will make this diff more readable.

I don't love the name DisposableCollector though. Collector makes me think of GC, which is a a little confusing in this context. We should think of a simpler name.

Would it make sense to pass an inout [Disposable]?

@andersio
Copy link
Member Author

andersio commented May 11, 2017

Would it make sense to pass an inout [Disposable]?

Swift doesn't seem happy about this.

screen shot 2017-05-11 at 4 49 12 pm

We should think of a simpler name.

Disposables? DisposableBag?

@mdiep
Copy link
Contributor

mdiep commented May 11, 2017

DisposableSink, maybe? It's hard to come up with a good name for this.

@andersio
Copy link
Member Author

andersio commented May 11, 2017

Sink sounds like a point of no return, while disposables are removable in our case. Collection on the other hand sounds weird since it cannot be traversed. That's why I picked Collector.

Lifetime can be used here too (proposed originally in #143), if it makes sense to you.

The cleanup for producers does not differentiate between cancellation and termination. The collected resources are disposed of when the signal is terminated regardless how it terminates.

So it (adding a disposable to the producer disposable) is actually a synonym of observing the termination of a Signal, just without affecting the Signal's lifetime like how a normal observer would be.

@mdiep
Copy link
Contributor

mdiep commented May 11, 2017

Sink sounds like a point of no return, while disposables are removable in our case

But they aren't removable inside the block? DisposableCollector and DisposableCollecting only have an add method. 😕

@andersio
Copy link
Member Author

add returns a handle to remove them from the collection though.

@mdiep
Copy link
Contributor

mdiep commented May 11, 2017

🤦‍♂️

@mdiep
Copy link
Contributor

mdiep commented May 11, 2017

I guess I'd lean towards DisposableBag. It seems like the least worst option.

@andersio
Copy link
Member Author

Lifetime?

@mdiep
Copy link
Contributor

mdiep commented May 11, 2017

The += operator you mentioned in #143 would call lifetime.observeEnded(disposable.dispose)?

@andersio
Copy link
Member Author

andersio commented May 11, 2017

Yeah, but it isn't essential. += can be obsoleted in favour of observeEnded if we go after the Lifetime way.

I proposed adding += to Lifetime in #229, but @sharplet and you weren't fond of it.

@mdiep
Copy link
Contributor

mdiep commented May 11, 2017

I think I'm 👍 with Lifetime, but I do still think we shouldn't add +=.

@andersio andersio changed the title DisposableCollector interface for SignalProducer. Lifetime-based producer resource management May 13, 2017
@andersio
Copy link
Member Author

+= and add taking a Disposable? are added to Lifetime to assist migration. These are marked as deprecated.

@andersio andersio requested a review from mdiep May 13, 2017 10:32
return (Lifetime(token), token)
internal enum Backing {
case token(ended: Signal<(), NoError>)
case disposable(CompositeDisposable)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to support two different backings?

It's only used in one spot and it could be emulated by holding a reference to the token in an ActionDisposable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see how ActionDisposable can emulate it. But I think it can be backed by just CompositeDisposable.

@mdiep
Copy link
Contributor

mdiep commented May 13, 2017

@ReactiveCocoa/reactiveswift Any opinions on the passing a Lifetime to SignalProducer.init closures instead of a Disposable? This eliminates inappropriate disposals of the disposable from inside the closure.

@andersio andersio force-pushed the producer-disposable branch 3 times, most recently from 6ff11ac to 92dccf5 Compare May 14, 2017 08:58
@andersio
Copy link
Member Author

andersio commented May 14, 2017

A side note: we may unify Signal.init under the same pattern.

let signal = Signal { observer, lifetime in
    let disposable = self.observe { ... }
    if let d = disposable { lifetime.observeEnded(d) }
}

@nalexn
Copy link

nalexn commented Nov 11, 2017

It seems like this change has broken the ability to interrupt the running RequestProducer via disposing of the Disposable, in a way that original CompositeDisposable would immediately propagate cancellation, while lifetime seems to not reflect the disposing of the Disposable:

let mySignalProducer = SignalProducer<Void, NoError> { observer, lifetime in
      lifetime.observeEnded {
        print("> lifetime ended")
      }
      async ... {
          print("> completed")
          observer.sendCompleted()
      }
}
let disposable = mySignalProducer.start()
print("> started")
disposable.dispose()
print("> disposed")

So the log for the example above would look like this:

> started
> disposed
> completed
> lifetime ended

So the lifetime inside the ResponseProducer's init closure remains alive until Producer completes.

I should mention this worked differently in ReactiveSwift v1.*, where SignalProducer would be interrupted immediately upon disposing...

@andersio
Copy link
Member Author

andersio commented Nov 11, 2017

@nalexn

The producer doesn't get to complete. It is interrupted immediately. The problem here is that the piece of async work is always going to execute, because it is not bound to the Lifetime.

observer.sendCompleted in this code path is effectively a no-op, because at that point the produced Signal has been terminated due to the interruption.

A slightly tweaked snippet to illustrate this:

let mySignalProducer = SignalProducer<Void, NoError> { observer, lifetime in
    lifetime.observeEnded {
        print("> lifetime ended")
    }

    // MARK: A
    QueueScheduler.main.schedule {

    // MARK: B
    // lifetime += QueueScheduler.main.schedule {
        print("> complete?")
        observer.sendCompleted()
    }
}
let disposable = mySignalProducer
    .logEvents()
    .start()

disposable.dispose()

Output:

[] starting fileName: ReactiveSwift.playground, functionName: __lldb_expr_19, lineNumber: 34
[] started fileName: ReactiveSwift.playground, functionName: __lldb_expr_19, lineNumber: 34
[] interrupted fileName: ReactiveSwift.playground, functionName: __lldb_expr_19, lineNumber: 34
[] terminated fileName: ReactiveSwift.playground, functionName: __lldb_expr_19, lineNumber: 34
[] disposed fileName: ReactiveSwift.playground, functionName: __lldb_expr_19, lineNumber: 34
> lifetime ended
> complete?

Now comment A and uncomment B. > complete? would be gone.

@nalexn
Copy link

nalexn commented Nov 11, 2017

@andersio thank you for the explanation, now I understand it better.

Is that possible to bound the piece of work to the lifetime without appealing to closure in a scheduler (basically how to do the same but syncronously)? It looks like unless I bound other disposable to the lifetime it'll disregard the fact of disposing as lifetime.hasEnded is still false even after calling dispose()

@andersio
Copy link
Member Author

lifetime.hasEnded being false during the course is the expected behavior. The starting side effect runs synchronously when one invokes start(), and the produced Signal is being disposed of only after start() returns.

@nalexn
Copy link

nalexn commented Nov 11, 2017

Yep, that totally makes sense. Thank you

@andersio
Copy link
Member Author

If you wish to be able to cancel it immediately and synchronously, you would have to adopt operators like take(until:) (with a producer) or take(during:), which would be able to terminate the produced Signal prior to the starting side effect being triggered.

e.g. Try to apply take(until: SignalProducer(value: ())), and you should be able to observe lifetime.hasEnded being true in the starting side effect.

@andersio
Copy link
Member Author

Note that this also means that the decision to cancel has to be made before start() given the synchronous scenario.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants