GithubHelp home page GithubHelp logo

Deadlock about reactiveswift HOT 20 CLOSED

reactivecocoa avatar reactivecocoa commented on April 29, 2024
Deadlock

from reactiveswift.

Comments (20)

inamiy avatar inamiy commented on April 29, 2024 3
pipe.output.filterMap { $0 == .a ? Event.b : nil }.observe(pipe.input)

Nice example :)
I also think this kind of data-flow should be accepted, i.e. let's use recursive lock.

from reactiveswift.

chuganzy avatar chuganzy commented on April 29, 2024 1

Let me give you more concrete example:

class View {
    enum Event {
        case textChanged(text: String)
        case buttonPressed
    }
    private let eventIO = Signal<Event, NoError>.pipe() // or view model
    private let textField = UITextField()
    private let button = UIButton()

    private func bind() {
        textField.reactive.continuousTextValues
            .map { Event.textChanged(text: $0) }
            .observe(eventIO.input)
        button.reactive.controlEvents(.touchUpInside)
            .map { _ in Event.buttonPressed }
            .observe(eventIO.input)
    }

    func observe(action: @escaping (Event) -> Void) {
        eventIO.output.observeValues {
            action($0)
        }
    }
}

class ViewController {
    let someView = View()

    private func bind() {
        someView.observe { [weak self] event in
            switch event {
                case .textChanged(let text): 
                    // do something
                case .buttonPressed:
                    // do something that eventually invoke below
                    self?.view.endEditing(false)
            }
        }
    }
}

When it invokes self?.view.endEditing(false) (or just call textField.resignFirstResponder()) UIKit sometimes sends UITextView.textDidChangeNotification (custom keyboard is related I am assuming) so continuousTextValues sends new value, and that ends up with deadlock. This kind of things can easily happen (ex: Apple changes the event cycle) and it's too risky to make it deadlock and forcing app to crash.

I do understand pipe.output.filterMap { $0 == .a ? Event.b : nil }.observe(pipe.input) kind of cases should be discouraged though, it actually happens in UI world easily (and sometimes unexpectedly), and I do not see the reason to force them to crash while it works.

What do you think?

from reactiveswift.

inamiy avatar inamiy commented on April 29, 2024 1

FYI

  • Queue-drain model without RecursiveLock: #540
    • IMO interesting approach, but impl is quite complicated so I still prefer simple RecursiveLock
  • RxSwift impl: Rx.swift#L89-L122
    • Uses RecursiveLock
    • fatalError with friendly message when #if DEBUG
    • Prints warning message at console when non-DEBUG

Since detecting cyclic dependency is hard in general and requires a lot of runtime test, I prefer the approach RxSwift is taking: prompt a message and use RecursiveLock.

And IMO "discouraged" only works when the problem is predictable and preventable beforehand.
But as far as I see many unexpected deadlock examples here, I would say the problem is overall "unpredictable".

Also, without reentrancy, I think many other potential operators e.g. #308 retryWhen can't be implemented in ReactiveSwift (unless explicitly using scheduler to move on to async).

from reactiveswift.

codyrobb avatar codyrobb commented on April 29, 2024

After setting a breakpoint on _NSLockError(), it looks like it is line 66 in Signal.swift causing the lock and eventual breakdown.

This is one of my first bigger stabs at RAC4 after coming from RAC2.X. I might be misusing some things. Please let me know!

from reactiveswift.

codyrobb avatar codyrobb commented on April 29, 2024

Another note: I am using ReactiveCocoa 4.2.2, but this seemed to be a better place to post the issue as it appeared you guys were trying to get the Swift API oriented issues over here.

If I need to move this just let me know.

from reactiveswift.

codyrobb avatar codyrobb commented on April 29, 2024

Hmmm... interesting.... The root of the problem appears to be this code:

        viewModel.undoEnabled.producer.startWithNext { [unowned self] enabled in
            if enabled {
                self.viewModel.undoCurrentAction()
            }
        }

If I do this:

viewModel.undoEnabled.producer.startWithNext { [unowned self] enabled in
    if enabled {
        // undoPressed simply calls viewModel.undoCurrentAction()
        self.navigationItem.rightBarButtonItem =
            UIBarButtonItem(title: "UNDO", style: .Plain, target: self, action: #selector(self.undoPressed))
    } else {
        self.navigationItem.rightBarButtonItem = nil
    }
}

Everything works fine. Can anyone elaborate on this?

from reactiveswift.

andersio avatar andersio commented on April 29, 2024

It seems you are having an infinite feedback loop. undoCurrentAction is being called when undoEnabled changes, while undoEnabled is bound to accept values from action.undoable, which is being modified in undoCurrentAction.

from reactiveswift.

chuganzy avatar chuganzy commented on April 29, 2024

This is just a simple example (actually leaking), but as you can see it is not infinite loop, so I feel it makes more sense to make Lock recursive.

enum Event {
    case a
    case b
}

let pipe = Signal<Event, NoError>.pipe()
pipe.output.filterMap { $0 == .a ? Event.b : nil }.observe(pipe.input)
pipe.output.observeValues { event in
    print("value: \(event)")
}
pipe.input.send(value: .a) // deadlock

What do you think? @andersio @mdiep

Related PR that makes Lock recursive #308

Especially when we use RAS with RAC, all events are pretty much UI driven, and sometimes UIKit causes this case unexpectedly.

from reactiveswift.

mdiep avatar mdiep commented on April 29, 2024

I think that organization should be discouraged. FRP is all about unidirectional data flow, so I don't think it makes sense to support cycles.

from reactiveswift.

chuganzy avatar chuganzy commented on April 29, 2024

@mdiep @andersio Can we start from reopening this issue? We might be able to gather more opinions.

from reactiveswift.

mdiep avatar mdiep commented on April 29, 2024

In that particular case, the pipe is an unnecessary level of indirection.

This seems like it'd work:

class View {
    enum Event {
        case textChanged(text: String)
        case buttonPressed
    }
    private let textField = UITextField()
    private let button = UIButton()

    func observe(action: @escaping (Event) -> Void) {
        textField.reactive.continuousTextValues
            .map { Event.textChanged(text: $0) }
            .observe(action)
        button.reactive.controlEvents(.touchUpInside)
            .map { _ in Event.buttonPressed }
            .observe(action)
    }
}

class ViewController {
    let someView = View()

    private func bind() {
        someView.observe { [weak self] event in
            switch event {
                case .textChanged(let text): 
                    // do something
                case .buttonPressed:
                    // do something that eventually invoke below
                    self?.view.endEditing(false)
            }
        }
    }
}

I don't think that the RxSwift behavior of not crashing but violating the contract is better.

from reactiveswift.

chuganzy avatar chuganzy commented on April 29, 2024

That is right, but as commented in the code, we do have a view model that has some logics inside, so we do need this kind of pipe.

Also even if we did not use pipe, if want to do something like below ultimately, same thing will happen.

textView.reactive.continuousTextValues
  .filter { $0 == "resign" }
  .observe { [weak textView] _ in textView?.resignFirstResponder() }

I don't think that the RxSwift behavior of not crashing but violating the contract is better.

Agree, since sometimes we cannot prevent this happening..

What do you think of providing a way to switch the lock then?

from reactiveswift.

mdiep avatar mdiep commented on April 29, 2024

It should be possible to detect cycles when you're constructing the signal chain. šŸ¤”

from reactiveswift.

chuganzy avatar chuganzy commented on April 29, 2024

šŸ¤”

textView.reactive.continuousTextValues
  .filter { $0 == "resign" }
  .observe { [weak textView] _ in textView?.resignFirstResponder() }

How can you solve this? resignFirstResponder() sometimes sends new value though. I assume there is no way other than switching the scheduler..

from reactiveswift.

mdiep avatar mdiep commented on April 29, 2024

Oh, good point! šŸ¤”

from reactiveswift.

chuganzy avatar chuganzy commented on April 29, 2024

Yeah..
It's not UITextView specific, this kind of thing can easily happen as some methods have unexpected side effect sometimes..

from reactiveswift.

inamiy avatar inamiy commented on April 29, 2024

FYI here's a flipflop example that won't crash our real world:
https://gist.github.com/inamiy/bd7d82a77a4df70b3b4a367a2d014ca9

from reactiveswift.

RuiAAPeres avatar RuiAAPeres commented on April 29, 2024

Hello. šŸ‘‹ Thanks for opening this issue. Due to inactivity, we will soft close the issue. If you feel that it should remain open, please let us know. šŸ˜„

from reactiveswift.

chuganzy avatar chuganzy commented on April 29, 2024

Just for sharing, we have been patching RAS internally to workaround this.

from reactiveswift.

RuiAAPeres avatar RuiAAPeres commented on April 29, 2024

@chuganzy do you think you could open a PR with the fix?

from reactiveswift.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    šŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. šŸ“ŠšŸ“ˆšŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ā¤ļø Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.