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

Goodbye interrupted...? #144

Closed
andersio opened this issue Dec 7, 2016 · 7 comments
Closed

Goodbye interrupted...? #144

andersio opened this issue Dec 7, 2016 · 7 comments
Labels
Milestone

Comments

@andersio
Copy link
Member

andersio commented Dec 7, 2016

Goodbye, our beloved interrupted.

TL;DR

By removing, we would have:

  1. A more compact API; and
  2. One less Signal to be created when starting a SignalProducer.
    Composed properties would benefit significantly - in particular the use of replayLazily - in the same way as Less hops in operator lifting. #140 on SignalProducer.

Rationale

Back in ReactiveSwift -2.0 ReactiveCocoa 3.0, Signal has a different lifetime semantic. That is, holding the event emitter of a Signal would indefinitely retain the Signal unless any terminal event is sent. So interrupted was introduced to help SignalProducer interrupt the leaves of its Signal graph (the ultimate upstreams) without hassle.

In ReactiveSwift 1.0, a new Signal lifetime semantic was introduced. Let's call it Automatic Observer and Reference Counting (AORC), which is pretty predictable in when a Signal would dispose of itself:

  1. Nobody holds a strong reference to it; and
  2. There is no active observer.

Alright. So why does it matter?

Look closer to the conditions, and the event emitter is off the list. That means for a Signal graph tigerMom.map(a).map(b), the two transformed Signals are not unilaterally forced to live as long as the ultimate upstream of theirs, i.e. tigerMom. They can be disposed of, as long as the two AORC conditions are met. At that point, they would also cease their observations to the upstream Signal.

Okay. But... how is it relevant to interrupted?

We just mentioned that a Signal can live on its own, and ceases its observation to an upstream when disposed. Doesn't this sound like what interrupted is designed for?

Now, the most important part is that the Signal graph created by a SignalProducer generally comprises of only one-observer Signal nodes (except the root and any multitcasting done by a custom operator through startWithSignal). That means if the root node is disposed of, the disposal would definitely propagate along the graph to the leaves.

(Well, unless some of our operators being naughty.)

It is exactly what interrupted does for us, isn't it? Interrupting one level by one level, from the root node to the leaves.

Note that the existing startWithSignal

The current startWithSignal, should it remains unchanged, would break the assumptions that the removal of interrupted relies on. Specifically, it permits multicasting and grabbing an external reference to the produced Signal. The migration plan below suggests introducing a internal "unsafe" variant of it to circumvent the issue.

The Migration Plan

The proposal here is to remove the interrupted event. But since it would not make in 1.0, it should be staged out.

Phase 1: Internal Refactoring @ 1.x

  1. Change SignalProducer to depend on the disposal of its produced Signal;
  2. Remove all internal dependencies on interrupted;
  3. Review all test cases that involves interrupted; and
  4. Introduce an internal startWithUnicastSignal.

startWithUnicastSignal assumes that it would ever have at most one observer, and the Signal reference would not escape the setUp closure scope. We can ensure these at runtime with assertions on the observer count and isKnownUniquelyReferenced. But it may not be ideal to be part of the public API. Anyhow, it should be able to replace most use of startWithSignal in our codebase, including all the start(.+?) methods.

startWithSignal would be rebuilt on top of startWithUnicastSignal without changing its behaviour.

Since the interrupted event cannot be removed, SignalProducer still has to emit the event for compatibility. To achieve this, any Signals created by SignalProducer would have to emit an interrupted event when disposed without a terminal event. It can be done by a special flag in Signal that would be consumed by the deinitializer.

Notes on the new start handler format (#140).

The new internal start handler format would no longer include an interrupter (cancel disposable) in its returning tuple.

Phase 2: Deprecation @ 1.x

Deprecate sendInterrupted, observeInterrupted, startWithInterrupted and Observer(interrupted:) in the public API.

As the internal refactoring has been done, our codebase should be free from deprecation warnings.

Phase 3: Removal @ 2.0

Remove Event.interrupted, all interrupted related APIs and test cases, and the workaround in Signal.

@NachoSoto
Copy link
Member

I'm pretty busy these days and honestly can't think about the specifics in detail, but I'm 100% happy to see this implementation detail go away and simplify the API :) especially since most of us (I at least) were never confident with its nuances.

@andersio andersio modified the milestone: 2.0 Dec 18, 2016
@mdiep
Copy link
Contributor

mdiep commented Dec 22, 2016

If you cancel a SignalProducer by disposing it's Disposable, what event is sent to its observers?

Interrupted seems useful as a way to communicate intent.

@andersio
Copy link
Member Author

andersio commented Dec 22, 2016

Hmm, you raise a valid point for observers relying on a terminal event to clean up resources. In the proposal, no events would be sent, because no work that yields event has been committed. The same also applies to observing a terminated Signal (it also returns a nil tho).

Seems like we should leave the public API as is — just refactoring. Sorry @NachoSoto, it seems it gonna stay. But at least now we found it useful (?).

Edit: Other alternatives that could be a step backward:

  1. Add observeDisposed (or Lifetime) to Signal. Only available in startWithSignal though.
  2. on(disposed:).
  3. Meld interrupted into failed: Introduce ProducerError<E> like ActionError<E>.
  4. Use ProducerEvent<U, E> with ProducerObserver<U, E> — ehm...

Edit: Though having an interrupted event in parallel to completed and failed would definitely be safer for those stateful observers that assert multiple kinds of events, because the switch requires it to be handled.

@andersio andersio changed the title Goodbye, interrupted. Refactor the propagation of producer interruption (interrupted is staying.) Dec 22, 2016
@andersio
Copy link
Member Author

andersio commented Dec 22, 2016

Regardless of removing it or not, I think it would still be nice to refactor it by establishing an explicit reversed channel, which can be reused for #163. Then ideally we'd still achieve the goal on one less Signals.

I have a wild idea on how to hide it completely from the public API, but I haven't got the time to iterate on it yet.

@andersio andersio changed the title Refactor the propagation of producer interruption (interrupted is staying.) Goodbye interrupted. Dec 22, 2016
@andersio andersio changed the title Goodbye interrupted. Goodbye interrupted...? Dec 22, 2016
@andersio
Copy link
Member Author

andersio commented Dec 26, 2016

It seems making SignalProducer<U, E> take Observer<U, FailureContext<E>> in start(_:) would not be a bad option. We can still have all the shorthands and the type signature intact, while for those who supplies a full observer, it would be a difference from:

case .interrupted:
    // cleanup

case let .failed(error):
    // handle error
    // cleanup

to:

case let .failure(context):
    if let error = context.error {
        // handle error
    }
    // context.isInterrupted: Bool
    // cleanup

@mdiep
Copy link
Contributor

mdiep commented Dec 28, 2016

That doesn't seem very user friendly to me. The current API seems much better—at least for our users.

@andersio
Copy link
Member Author

andersio commented Dec 28, 2016

Have the same feel after skimming through the codebase again. Anyway, the refactoring gonna be done in #163.

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

No branches or pull requests

3 participants