-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Unable to cancel an EffectTask before it's being run #1848
Comments
Hi @Thomvis, thanks for the detailed report! Unless I'm missing something, this does behave how I would expect. You are concatenating the effects, and so the But also, even if you do a This is due to the fact that when we more fully embraced async tools in TCA we started incurring some context switches that were not there previously with Combine. We may be able to clean up some of those though. All that to say, I don't have an immediate solution right now. We would have to look into making |
Just to drive home the difference between Combine and Swift concurrency, the following effect will operate how you want: return .merge(
.task { false }
.eraseToEffect()
.cancellable(id: 1),
.cancel(id: 1)
) This is because although |
I would expect the task to start, but be cancelled almost immediately while it's awaiting the
I figured this out after some debugging. I think it would be nice if I wouldn't have to think about the task's overhead. (Especially since Compose-based effects behave differently.)
This works as expected and is a good enough work-around for now, thanks! |
I'm sorry, I may be missing something obvious, but because you are using I tried delaying the cancellation like you mentioned, but it still works the same way: return .concatenate([
.task {
try await Task.sleep(nanoseconds: NSEC_PER_SEC)
return .increment
}
.cancellable(id: "1"),
.cancel(id: "1")
.delay(for: 0.1, scheduler: DispatchQueue.main)
.eraseToEffect()
]) |
My bad. I hadn't realized concatenate waits for the task to finish (although I know about concatenate in the Rx context). Also it seems that some important details might have gotten lost when I formulated the minimal reproduction case. I'll spend more time to get to the core of the issue. |
Here's my second try at the reproduction case. I got rid of the concatenate and now use the (old) synchronous Uncommenting the func testImmediateCancellationReduced() {
struct Example: ReducerProtocol {
struct State: Equatable {
var count: Int
}
enum Action: Equatable {
case count(Int)
case doCalculation(Int)
case calculationDidFinish(Int)
}
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
switch action {
case .count(let c):
let oldCount = state.count
state.count = c
return .merge([
.cancel(id: "\(oldCount)"),
.init(value: .doCalculation(c))
])
case .doCalculation(let c):
return .task {
try await Task.sleep(for: .seconds(1))
return .calculationDidFinish(c*100)
}
// .eraseToEffect()
.cancellable(id: "\(c)")
case .calculationDidFinish:
return .none
}
}
}
let store = TestStore(initialState: Example.State(count: 0), reducer: Example())
store.send(.count(1)) {
$0.count = 1
}
store.receive(.doCalculation(1))
store.send(.count(2)) {
$0.count = 2
}
store.receive(.doCalculation(2))
let e = expectation(description: "")
Task {
await store.receive(.calculationDidFinish(200), timeout: .seconds(2))
e.fulfill()
}
waitForExpectations(timeout: 3)
} |
Ah yeah, ok that makes more sense. I think this is something we can fix while still staying in the concurrency world with no Combine. We have a note for it and will look at it soon. As a quick aside, you may be interested in this article for an alternative to sharing reducer logic via actions. 🙂 |
Description
If a reducer, in response to an action, emits both an
EffectTask.task
withcancellable
on it and anEffectTask.cancel
(in that order) for the same id, the task is not cancelled.Checklist
main
branch of this package.Expected behavior
I would expect the task not to start because it was emitted and cancelled right away
Actual behavior
The task runs and finished
Steps to reproduce
The following example demonstrates the issue. In it, the
.triggerIssue
action emits two effects. The first is a longer running task that can be cancelled, the second is a cancel effect that I expect to cancel that very same long running task. I would expect that no.increment
action is emitted because the task would be cancelled while sleeping. In reality, the.increment
action is emitted.In real world use, these effects would obviously not be right next to each other, but the issue remains the same.
The Composable Architecture version information
0.49.2
Destination operating system
iOS 16
Xcode version information
Version 14.1 (14B47b)
Swift Compiler version information
The text was updated successfully, but these errors were encountered: