Skip to content

Fix crash when executing/canceling effect inside BaseMiddleware#108

Open
IosifSuzuki wants to merge 4 commits into1.5.2from
feature/fix-crash-when-executing-canceling-effect-inside-basemiddleware-guid_869d0m455
Open

Fix crash when executing/canceling effect inside BaseMiddleware#108
IosifSuzuki wants to merge 4 commits into1.5.2from
feature/fix-crash-when-executing-canceling-effect-inside-basemiddleware-guid_869d0m455

Conversation

@IosifSuzuki
Copy link
Copy Markdown
Collaborator

@IosifSuzuki IosifSuzuki commented Apr 22, 2026

Description

This merge request fixes a thread-safety issue in middleware cancellation handling. _BaseMiddleware was storing and mutating its cancellations dictionary from multiple execution contexts without synchronization, which could cause data races when several cancellation actions were dispatched at the same time.

The change is necessary because cancellation can be triggered concurrently from Combine pipelines and async tasks, and the middleware was marked @unchecked Sendable while still relying on unsynchronized mutable state. An alternative would have been to isolate cancellation state behind an actor or move all access onto a single serialized queue, but this MR keeps the current design and adds explicit locking around the shared mutable state.

Changes Made

  • Made middleware cancellation storage thread-safe by replacing direct access to:

    public var cancellations: [AnyHashable: CancellableTask] = [:]

    with lock-protected access backed by:

    private let state = OSAllocatedUnfairLock(initialState: MutableState())
  • Introduced a private MutableState container to encapsulate the cancellations dictionary and provide controlled mutation helpers:

    • set(cancellable:forKey:)
    • deleteCancellation(forKey:)
  • Updated all cancellation lifecycle paths in _BaseMiddleware to use the lock:

    • explicit cancellation via cancel(by:)
    • Combine receiveCancel
    • Combine completion handlers
    • async task completion / cleanup
    • storing newly created cancellables and tasks
  • Preserved the public cancellations API as a computed property that now returns a lock-protected snapshot of the internal state.

  • Added regression tests for concurrent cancellation in both middleware test suites:

    • ConcurrencyMiddlewareCancellationTests.testMiddlewareCancellationDataRace
    • MiddlewareCancellationTests.testMiddlewareCancellationDataRace
  • The new tests dispatch CancelLoading() concurrently from multiple tasks and verify that middleware reaches one of the expected cancellation states without crashing or corrupting state.

ClickUp task

https://app.clickup.com/t/869d0m455

Bogdan Petkanych added 3 commits April 22, 2026 15:36
Comment thread UDF/_Middleware/_BaseMiddleware.swift Outdated
cancellations[key] = cancellable
}

func deleteCancellation(forKey key: AnyHashable) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Давай може краще успадкуємо неймінг по принципу як і removeValue в словнику? Змінемо ім'я на removeCancellation.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+++

Remove usage GCD from swift concurrency code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants