Skip to content

Commit 318efc3

Browse files
committed
Improve concurrency for background filter list update
1 parent 478d38a commit 318efc3

4 files changed

Lines changed: 298 additions & 47 deletions

File tree

MaruWeb/Filters/WebFilterListDownloader.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,24 @@ public actor WebFilterListDownloader {
7878
/// flight at once. Returns a per-id outcome map.
7979
@discardableResult
8080
public func refreshAll(force: Bool = false) async -> [UUID: WebFilterListRefreshOutcome] {
81+
guard !Task.isCancelled else { return [:] }
8182
let entries = await MainActor.run { storage.entries.filter(\.isEnabled) }
8283
var outcomes: [UUID: WebFilterListRefreshOutcome] = [:]
8384
var iterator = entries.makeIterator()
8485
await withTaskGroup(of: (UUID, WebFilterListRefreshOutcome).self) { group in
8586
for _ in 0 ..< min(maxConcurrentRefreshes, entries.count) {
87+
guard !Task.isCancelled else { break }
8688
guard let entry = iterator.next() else { break }
8789
group.addTask { [self] in
8890
await (entry.id, refresh(entry: entry, force: force))
8991
}
9092
}
9193
while let result = await group.next() {
9294
outcomes[result.0] = result.1
95+
guard !Task.isCancelled else {
96+
group.cancelAll()
97+
break
98+
}
9399
if let entry = iterator.next() {
94100
group.addTask { [self] in
95101
await (entry.id, refresh(entry: entry, force: force))
@@ -102,6 +108,7 @@ public actor WebFilterListDownloader {
102108

103109
/// Refreshes a single filter list. `force` ignores etag/last-modified headers.
104110
public func refresh(entry: WebFilterListEntry, force: Bool = false) async -> WebFilterListRefreshOutcome {
111+
guard !Task.isCancelled else { return .failed(message: "Cancelled") }
105112
let attemptedAt = Date()
106113
var request = URLRequest(url: entry.sourceURL)
107114
request.httpMethod = "GET"
@@ -116,6 +123,7 @@ public actor WebFilterListDownloader {
116123

117124
do {
118125
let (data, response) = try await session.data(for: request)
126+
guard !Task.isCancelled else { return .failed(message: "Cancelled") }
119127
guard let httpResponse = response as? HTTPURLResponse else {
120128
let message = "Non-HTTP response"
121129
await MainActor.run {
@@ -166,6 +174,8 @@ public actor WebFilterListDownloader {
166174
}
167175
return .failed(message: message)
168176
}
177+
} catch where Task.isCancelled || error is CancellationError {
178+
return .failed(message: "Cancelled")
169179
} catch {
170180
let message = (error as? URLError).map { "URLError(\($0.code.rawValue)): \($0.localizedDescription)" }
171181
?? error.localizedDescription
@@ -178,6 +188,7 @@ public actor WebFilterListDownloader {
178188
}
179189

180190
private func updatePerListMetrics(entry: WebFilterListEntry, contents: String) async {
191+
guard !Task.isCancelled else { return }
181192
let source = WebFilterListSource(
182193
identifier: entry.id.uuidString,
183194
contents: contents,
@@ -186,6 +197,7 @@ public actor WebFilterListDownloader {
186197
guard let result = try? WebFilterListConverter.convert([source]) else { return }
187198
let ruleCount = Int(result.ruleCount)
188199
let convertedCount = Int(result.convertedFilterCount)
200+
guard !Task.isCancelled else { return }
189201
await MainActor.run {
190202
storage.applyCompileMetrics(
191203
id: entry.id,

MaruWeb/Filters/WebFilterListUpdateScheduler.swift

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,38 @@ public final class WebFilterListUpdateScheduler {
3131
private let log = Logger(subsystem: "MaruWeb", category: "filter-list-scheduler")
3232
private let downloader: WebFilterListDownloader
3333
private let storage: WebFilterListStorage
34+
private let submitRequest: (BGTaskRequest) throws -> Void
3435
private var didRegister = false
3536
private var inFlight: Task<Void, Never>?
3637

37-
public init(
38+
public convenience init(
3839
downloader: WebFilterListDownloader = .shared,
3940
storage: WebFilterListStorage = .shared
41+
) {
42+
self.init(
43+
downloader: downloader,
44+
storage: storage,
45+
submitRequest: { try BGTaskScheduler.shared.submit($0) }
46+
)
47+
}
48+
49+
init(
50+
downloader: WebFilterListDownloader,
51+
storage: WebFilterListStorage,
52+
submitRequest: @escaping (BGTaskRequest) throws -> Void
4053
) {
4154
self.downloader = downloader
4255
self.storage = storage
56+
self.submitRequest = submitRequest
4357
}
4458

4559
/// Registers the BGAppRefreshTask handler. Must be called from the host app's `init`
4660
/// (or before scene activation), exactly once per process.
4761
public func registerBackgroundTask() {
4862
guard !didRegister else { return }
49-
didRegister = true
50-
BGTaskScheduler.shared.register(
63+
didRegister = BGTaskScheduler.shared.register(
5164
forTaskWithIdentifier: WebContentBlocker.backgroundRefreshTaskIdentifier,
52-
using: nil
65+
using: DispatchQueue.main
5366
) { [weak self] task in
5467
guard let task = task as? BGAppRefreshTask else {
5568
task.setTaskCompleted(success: false)
@@ -59,6 +72,9 @@ public final class WebFilterListUpdateScheduler {
5972
await self?.handle(backgroundTask: task)
6073
}
6174
}
75+
if !didRegister {
76+
log.error("Failed to register background refresh task")
77+
}
6278
}
6379

6480
/// Triggers a refresh if the most recent successful fetch across enabled lists is
@@ -70,15 +86,22 @@ public final class WebFilterListUpdateScheduler {
7086

7187
/// Manually triggers a refresh. UI calls this from the "Update Now" button.
7288
public func refreshNow() {
73-
if inFlight != nil {
74-
return
89+
_ = startRefresh()
90+
}
91+
92+
@discardableResult
93+
func startRefresh() -> Task<Void, Never> {
94+
if let inFlight {
95+
return inFlight
7596
}
76-
inFlight = Task { @MainActor [weak self] in
97+
let task = Task { @MainActor [weak self] in
7798
guard let self else { return }
99+
defer { self.inFlight = nil }
78100
await self.downloader.refreshAll(force: false)
79101
self.scheduleNextRefresh()
80-
self.inFlight = nil
81102
}
103+
inFlight = task
104+
return task
82105
}
83106

84107
/// Submits the next BGAppRefreshTask request. Safe to call repeatedly.
@@ -88,7 +111,7 @@ public final class WebFilterListUpdateScheduler {
88111
)
89112
request.earliestBeginDate = nextEligibleDate(now: now)
90113
do {
91-
try BGTaskScheduler.shared.submit(request)
114+
try submitRequest(request)
92115
} catch {
93116
log.error("Failed to schedule background refresh: \(String(describing: error), privacy: .public)")
94117
}
@@ -99,10 +122,10 @@ public final class WebFilterListUpdateScheduler {
99122
private func handle(backgroundTask: BGAppRefreshTask) async {
100123
// Always reschedule first so we keep ticking even if this run is curtailed.
101124
scheduleNextRefresh()
125+
storage.start()
126+
storage.seedDefaultsIfNeeded()
102127

103-
let task = Task { @MainActor [weak self] in
104-
_ = await self?.downloader.refreshAll(force: false)
105-
}
128+
let task = startRefresh()
106129
backgroundTask.expirationHandler = {
107130
task.cancel()
108131
}

0 commit comments

Comments
 (0)