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
Dispatch issue with middleware. #271
Comments
Since the order of type member declarations (methods and properties) doesn't matter, I presume you wonder where What is the exact issue you're facing, by the way? |
@DivineDominion Evidently I did not make the original post clear enough. Also my line numbers were wrong because I was using the code inside the GitHubBrowser example, which differs slightly from the current commit here. I have edited it to clarify this issue and I updated the line numbers. The exact issue is that this will always crash if middleware array is not empty and the .reduce function runs. It might be solved by initializing Store's Side note: there is no need to return a value from the initial |
The thing is, line 76 will not actually get executed until you dispatch an action. Then the whole middleware collection is reduce'd and the inner dispatch reference is formed and the passed along. So this confusing loop of function calls is merely the preparation of a call, not the call itself. (To falsify your claim that it will crash:: Does it actually do crash in an app of yours?) The superfluous return is a good catch! Do you want to open a PR? |
The following code will reproduce the crash that I described. It depends on how the middleware is structured. For instance, in StoreMiddlewareTests.swift, change the
This will reproduce the crash. Boom. Also... sure, I'll open a pull request for the superfluous return. |
OK, see pull request 274 for the superfluous return removal. Concerning the crash mentioned above, that bears further discussion, and I'm interested in what your thoughts might be on the issue. |
I should also note, there is also a scenario where Middleware can cause an infinite loop crash by calling its To see this in action, replace
ExplanationUnder the current setup, we make at least two trips through any middleware that calls its DispatchFunction argument (as in The only reason we just make two trips, instead of an infinity of trips, in the existing tests, is because the
The first go-round, Even the best-case scenario gets much worse the more dispatching middleware there are in the array:
It appears to follow the pattern of increase, O(N(N+1)) or basically O(N^2), where N is the number of dispatching middleware in the array. Increasing the number of non-dispatching middleware in the array has a different impact:
It appears here that the pattern of increase is O(N(N+1) + M(N+1)) where N is the number of dispatching middleware and M is the number of non-dispatching middleware. For example if there are 4 dispatching and 2 non-dispatching, the total number of middleware runs (collectively across all of them) is 4(4+1) + 2(4+1) = 30. SummaryTo summarize:
|
Interesting! That sums up my feelings about scaling the extensive use of middlewares. But there are no alternatives I can think of that reduce overhead. Sure, the O(N^2) increase puts load on the call stack; the alternative I have been using involved enqueueing side effects into "pending" states, then dispatch completion actions to dequeue. That keeps the call stack flat, but increases coding overhead. Maybe it can make sense to collect services that performs side effects into a collection and use 1 middleware to iterate over all of them. This keeps the call stack flatter but does not change the big-O complexity. (I wrote about the code of this experimental approach today: http://cleancocoa.com/posts/2017/09/reswift-middleware-different-target/) |
But should we be making two trips through some of these middleware? It seems like each one should only ever get called once. |
I wonder why not. Is it that in some Middleware cases you already know the next logical step is to start the chain of reducers? Then instead of |
Sorry I have been swamped on a huge project at work. I will revisit this question with a fresh mind soon, but I would personally want middleware to simply pass through like a serial queue, where each middleware runs in the order it was added, and its result goes to the next middleware, etc. I think what I had seen in here looked like an “abuse of reduce” and something much simpler and possible to reason about would seem desirable. Just my 0.02. But I will take another look and get back to you. |
Any news here? |
I make heavy use of middlewares in my app (all side effects, in fact) and I have 0 crashes and never had a problem regarding this. Can you share an example of a crashing middleware? |
Sorry for the late answer and unfortunately I didn't have the time to try to reproduce this with a demo app yet. The only thing that currently "fixes" the issue is, to identify "problematic" actions that are dispatched in middlewares and dispatch them using Anyway, I don't like that kind of "fix" and really want to get down to the root cause of the problem. @dani-mp How do you use the middlewares? Just some questions that come to my mind...
I already created a question on SO, but it's really hard to provide the necessary information needed, as I can't share the whole app code (which in fact may be necessary). So maybe I really need to get a demo working, IDK. Anyway here's the SO link: https://stackoverflow.com/questions/61793803/strange-memory-issues-thread-1-exc-bad-access-code-2-address-0x16d09aa00 |
Interesting.
It does matter -- but not for any safety reasons. Calling
I've never tested this. |
What do y'all think about making |
I've needed reference to it a couple times -- for explicitly skipping middleware behaviour on that action. There may be a better way to resolve that need, though. |
Ok, I'm pretty sure I finally got the issue. We have a quite large data structure (according to Using those "dispatch" workaround breaks the middleware "recursion" and should considerably lower the stack usage here. I would love to directly confirm this, but unfortunately I was not able to find a method to observe the actual stack load. If anyone knows how to achieve this, please let me know! I could also reproduce this behaviour in a completely stripped down demo project, and therefore I'm quite sure now, this is it. If you're interested, I can put it here on GitHub somewhere. |
Yes, please. It'd be great to have a demo project to troubleshoot this. We should also try to find information in the Swift community and see if this is a Swift issue or something that we're doing wrong in ReSwift. I'm still not sure what you mean with Using those "dispatch" workaround breaks the middleware "recursion". If there is any recursion it's probably because the same action is being dispatched without any guard in a middleware, otherwise, the number of middleware passes should be finite. @DivineDominion about |
It's not like an endless recursion, but as long as you dispatch actions in the middlewares over and over again (before actually getting out of it) it's indeed (as intended) running through the middlewares again and again which produces quite larges call stacks. I think @gistya provided some very nice insights on this already here: #271 (comment) From my understanding (I may be wrong) stacks work like this: Each function call gets placed on the stack with its parameters and any "static" allocated memory (value types, or just references to reference types on the heap) and it's return address (doesn't actually matter how/what exactly goes on the stack). What matters is, when it gets popped off the stack again, and that's (in my understanding), when a function returns, which it only does in this case, when the "recursion" ends. As for |
Here's the demo project. It's somewhat similar to our app, but indeed not exactly the same. The data structure is a bit bigger in the demo project (adjustable in commenting in/out stuff in the dummy data structs, you'll see what I mean) and there is less (almost nothing) happening in terms of reducers etc. which in the end leads to similar results (crashes). For sake of simplicity it should suffice like this I guess. https://github.com/d4rkd3v1l/ReSwift-StackOverflowDemo/blob/master/reswiftcrash/ReSwiftDemo.swift |
I understand what you're saying in regards to call stack. Thats definitely a possibility given a large enough state and numerous middleware & reducers. I do find it surprising that you've ran into this, though, I've worked with some fairly large ReSwift systems without encountering it. Thanks for the demo project. Does this crash on simulator as well or just on device? |
We have some data structures in the state that are insanely large. 😬🙈
Just on device |
I "solved" it for now, by moving large structs (in fact all the sub-states) onto the heap, by wrapping them inside arrays. Now the AppState went down from ~11kb to 160b and everything runs smooth again. 🥳 Using @propertyWrappers, it feels at least like a partly elegant solution to me. |
That's super useful, @d4rkd3v1l, thanks! Is this a known problem in Swift? It seems to me that ReSwift is not doing anything wrong here, but I'm not sure if it can still do something to avoid this or if that would make sense. |
I would not say ReSwift is doing anything wrong here. I just think it kinda facilitates running into such issues, due to it's very nature (large/many state structs). But for me, a proper state handling, as ReSwift provides completely outweighs such issues. I rather live with such "hackarounds" (like the wrappers explained above) than living without proper state handling :-) I just fell way too deep in love with ReSwift 😍(so my thoughts on this may not be objective at all 🙈🤣) |
The pattern of dispatching another action in a Middleware and the problems that stem from it (see @gistya's overview far at the top) became weird in a demo project I started recently, let movementMiddleware: Middleware<RootState> = { dispatch, getState in
return { next in
return { action in
next(action)
// Only move once per tick to have a constant movement speed, bound to the FPS.
guard action is Tick else { return }
guard let keysHeld = getState()?.keysHeld else { return }
if keysHeld.contains(.left) {
dispatch(Walking.left)
} else if keysHeld.contains(.right) {
dispatch(Walking.right)
}
}
}
} I found it works equally well and introduces far fewer passes of dispatching actions when the stimulus action's reducer takes care of this. For the example of walking, see https://github.com/CleanCocoa/ReSwift-Mario/blob/3f7e9b5fc56bbea31b6c52589ea82400e89d8c6f/State/Tick.swift internal struct Tick: ReSwift.Action {
init() {}
}
func reduce(_ tick: Tick, state: RootState) -> RootState {
var state = state
// ...
state = walk(state)
return state
}
let walkingSpeed: Double = 2
func walk(_ state: RootState) -> RootState {
let offset: Double = {
if state.keysHeld.contains(.left) {
return -walkingSpeed
} else if state.keysHeld.contains(.right) {
return +walkingSpeed
} else {
return 0
}
}()
var state = state
state.x += offset
return state
} The bottom line: For my real-world apps, I am now reconsidering my approach to using Middleware, and if some of them could just as well be reducers that I didn't see at first. |
Yeah, @DivineDominion, that's a good example. You want to use middleware to feed/put data from/to the outside world, via side effects. If the information is just derived from a combination of current/next state and the action, it's better to use reducers because there is no side effect involved. |
I have discovered a likely crash scenario. Please see below.
Store's
dispatchFunction
var gets assigned in ReSwift/CoreTypes/Store.swift ll. 69–79:However on line 76 we have:
let dispatch: DispatchFunction = { [weak self] in self?.dispatch($0) }
Now,
self?.dispatch($0)
in turn calls this function on ll. 146-148:In turn, the Store's
dispatchFunction
function var gets called here. However, at this point,dispatchFunction
may be nil because ll. 69–79 may not have completed its task of assigning a value to it yet, and it has no default value. See it's declaration on line 35:public var dispatchFunction: DispatchFunction!
This may crash when the
middleware
array is not empty coming into the init function and one of them callsdispatch
, since if the .reduce function runs, it may call Store's instance vardispatchFunction
before it has been initialized.EDIT: This will not always happen, but it can happen depending on how middleware is structured. There is also an infinite loop crash that's possible; see below.
The text was updated successfully, but these errors were encountered: