Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions ios/Tests/ConcurrencyLimiterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,45 @@ final class ConcurrencyLimiterTests: XCTestCase {
let finalPeak = await counter.peak
XCTAssertEqual(finalPeak, 1)
}

// Cold start: a process started by the OS has not run Dart initialize(), so the limiter it
// builds comes from the persisted value. Simulated with a throwaway defaults suite.
func test_issue_84_persisted_limit_survives_a_fresh_limiter() async throws {
let suite = "nwm.test.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }

XCTAssertEqual(ConcurrencyLimiter.persistedLimit(defaults: defaults), 4, "nothing persisted -> default")

await ConcurrencyLimiter(max: 4).configure(max: 2, persist: true, defaults: defaults)
XCTAssertEqual(ConcurrencyLimiter.persistedLimit(defaults: defaults), 2)

// "Process restart": brand new limiter built only from what was persisted.
let cold = ConcurrencyLimiter(max: ConcurrencyLimiter.persistedLimit(defaults: defaults))
let counter = Counter()
try await withThrowingTaskGroup(of: Void.self) { group in
for _ in 0..<12 {
group.addTask {
try await cold.acquire()
await counter.enter()
try await Task.sleep(nanoseconds: 10_000_000)
await counter.leave()
await cold.release()
}
}
try await group.waitForAll()
}
let peak = await counter.peak
XCTAssertEqual(peak, 2, "restored limit must be enforced without any initialize() call")
}

func test_issue_84_invalid_persisted_values_fall_back_to_default() {
let suite = "nwm.test.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defer { defaults.removePersistentDomain(forName: suite) }
for bad: Any in [0, -3, "7", 2.5] {
defaults.set(bad, forKey: ConcurrencyLimiter.preferenceKey)
XCTAssertEqual(ConcurrencyLimiter.persistedLimit(defaults: defaults), 4, "bad value \(bad)")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@ import Foundation
/// Reconfiguration preserves active permits. Cancellation removes queued waiters.
actor ConcurrencyLimiter {
static let preferenceKey = "native_workmanager.maxConcurrentTasks"
static let shared = ConcurrencyLimiter(
max: UserDefaults.standard.object(forKey: preferenceKey) as? Int ?? 4
)
static let defaultLimit = 4

/// The limit persisted by the last `initialize()`, or the default. This is what a process
/// started cold by the OS (BGTask, no Dart `initialize()` yet) will use.
static func persistedLimit(defaults: UserDefaults = .standard) -> Int {
guard let value = defaults.object(forKey: preferenceKey) as? Int, value > 0 else { return defaultLimit }
return value
}

static let shared = ConcurrencyLimiter(max: persistedLimit())

private var max: Int
private var running = 0
private var waiting: [(UUID, CheckedContinuation<Void, Error>)] = []

init(max: Int) { self.max = Swift.max(1, max) }

func configure(max: Int, persist: Bool = false) {
func configure(max: Int, persist: Bool = false, defaults: UserDefaults = .standard) {
precondition(max > 0)
self.max = max
if persist { UserDefaults.standard.set(max, forKey: Self.preferenceKey) }
if persist { defaults.set(max, forKey: Self.preferenceKey) }
drain()
}

Expand Down
Loading