Skip to content

Commit 51429c6

Browse files
authored
[fix]: resolve memory exclusivity violations in some WorkflowObserver paths (#216)
### Issue - under some circumstances, the action application observation codepath could trigger runtime memory exclusivity exceptions ### Description - added a test case that reproduces the issue with the existing API - removed an unneeded `inout` parameter to resolve the problem
1 parent dd9042c commit 51429c6

File tree

2 files changed

+58
-4
lines changed

2 files changed

+58
-4
lines changed

Workflow/Sources/WorkflowNode.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ final class WorkflowNode<WorkflowType: Workflow> {
8484
/// allowing the underlying conformance to be applied to the Workflow's State
8585
let outputEvent = openAndApply(
8686
action,
87-
to: &state,
8887
isExternal: source == .external
8988
)
9089

@@ -182,13 +181,10 @@ private extension WorkflowNode {
182181
/// Applies an appropriate `WorkflowAction` to advance the underlying Workflow `State`
183182
/// - Parameters:
184183
/// - action: The `WorkflowAction` to apply
185-
/// - state: The `State` to which the action will be applied
186-
/// - observerInfo: Optional observation info to notify registered `WorkflowObserver`s
187184
/// - isExternal: Whether the handled action came from the 'outside world' vs being bubbled up from a child node
188185
/// - Returns: An optional `Output` produced by the action application
189186
func openAndApply<A: WorkflowAction>(
190187
_ action: A,
191-
to state: inout WorkflowType.State,
192188
isExternal: Bool
193189
) -> WorkflowType.Output? where A.WorkflowType == WorkflowType {
194190
let output: WorkflowType.Output?

Workflow/Tests/WorkflowObserverTests.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,35 @@ final class WorkflowObserverTests: XCTestCase {
163163
XCTAssertEqual(actions, [.toggle])
164164
}
165165

166+
// added to exercise a simultaneous memory access bug when observing action
167+
// application for some Workflow's where State == Void
168+
func test_didReceiveActionCallbacks_voidState() {
169+
var actions: [VoidStateWorkflow.Action] = []
170+
observer.onDidReceiveAction = { action, workflow, session in
171+
guard let action = action as? VoidStateWorkflow.Action else {
172+
XCTFail("unexpected action. expecting \(VoidStateWorkflow.Action.self), got \(type(of: action))")
173+
return
174+
}
175+
176+
actions.append(action)
177+
}
178+
179+
let node = WorkflowNode(
180+
workflow: VoidStateWorkflow(),
181+
parentSession: nil,
182+
observer: observer
183+
)
184+
185+
let rendering = node.render()
186+
node.enableEvents()
187+
188+
XCTAssertEqual(actions, [])
189+
190+
rendering.onTapped()
191+
192+
XCTAssertEqual(actions, [.actionValue])
193+
}
194+
166195
func test_didReceiveActionCallbacks_onlyInvokedForExternalEvents() {
167196
var actionsReceived: [InjectableWorkflow.Action] = []
168197
observer.onDidReceiveAction = { action, workflow, session in
@@ -687,3 +716,32 @@ private struct DefaultObservers: ObserversInterceptor {
687716
observers + initialObservers
688717
}
689718
}
719+
720+
// MARK: Void State Observation Crash Example
721+
722+
/// Example that cause a memory exclusivity violation in prior observation code
723+
private struct VoidStateWorkflow: Workflow {
724+
typealias State = Void
725+
typealias Output = Never
726+
727+
enum Action: WorkflowAction {
728+
typealias WorkflowType = VoidStateWorkflow
729+
730+
case actionValue
731+
732+
func apply(toState state: inout VoidStateWorkflow.State) -> VoidStateWorkflow.Output? {
733+
return nil
734+
}
735+
}
736+
737+
struct Rendering {
738+
var onTapped: () -> Void
739+
}
740+
741+
func render(state: VoidStateWorkflow.State, context: RenderContext<VoidStateWorkflow>) -> Rendering {
742+
let sink = context.makeSink(of: VoidStateWorkflow.Action.self)
743+
return Rendering(
744+
onTapped: { sink.send(.actionValue) }
745+
)
746+
}
747+
}

0 commit comments

Comments
 (0)