-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathSYNQueueTask.swift
More file actions
296 lines (250 loc) · 10.7 KB
/
SYNQueueTask.swift
File metadata and controls
296 lines (250 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
//
// SYNQueueTask.swift
// SYNQueue
//
import Foundation
public typealias SYNTaskCallback = (SYNQueueTask) -> Void
public typealias SYNTaskCompleteCallback = (NSError?, SYNQueueTask) -> Void
public typealias JSONDictionary = [String: AnyObject?]
/**
* Represents a task to be executed on a SYNQueue
*/
@objc
public class SYNQueueTask : NSOperation {
static let MIN_RETRY_DELAY = 0.2
static let MAX_RETRY_DELAY = 60.0
public let queue: SYNQueue
public let taskID: String
public let taskType: String
public let data: AnyObject?
public let created: NSDate
public var started: NSDate?
public var retries: Int
let dependencyStrs: [String]
var lastError: NSError?
var _executing: Bool = false
var _finished: Bool = false
public override var name: String? { get { return taskID } set { } }
public override var asynchronous: Bool { return true }
public override var executing: Bool {
get { return _executing }
set {
willChangeValueForKey("isExecuting")
_executing = newValue
didChangeValueForKey("isExecuting")
}
}
public override var finished: Bool {
get { return _finished }
set {
willChangeValueForKey("isFinished")
_finished = newValue
didChangeValueForKey("isFinished")
}
}
/**
Initializes a new SYNQueueTask with the following options
- parameter queue: The queue that will execute the task
- parameter taskID: A unique identifier for the task, must be unique across app terminations,
otherwise dependencies will not work correctly
- parameter taskType: A type that will be used to group tasks together, tasks have to be generic with respect to their type
- parameter dependencyStrs: Identifiers for tasks that are dependencies of this task
- parameter data: The data that the task needs to operate on
- parameter created: When the task was created
- parameter started: When the task started executing
- parameter retries: Number of times this task has been retried after failing
- parameter queuePriority: The priority
- parameter qualityOfService: The quality of service
- returns: A new SYNQueueTask
*/
private init(queue: SYNQueue, taskID: String? = nil, taskType: String,
dependencyStrs: [String] = [], data: AnyObject? = nil,
created: NSDate = NSDate(), started: NSDate? = nil, retries: Int = 0,
queuePriority: NSOperationQueuePriority = .Normal,
qualityOfService: NSQualityOfService = .Utility)
{
self.queue = queue
self.taskID = taskID ?? NSUUID().UUIDString
self.taskType = taskType
self.dependencyStrs = dependencyStrs
self.data = data
self.created = created
self.started = started
self.retries = retries
super.init()
self.queuePriority = queuePriority
self.qualityOfService = qualityOfService
}
/**
Initializes a new SYNQueueTask with the following options
- parameter queue: The queue that will execute the task
- parameter taskType: A type that will be used to group tasks together, tasks have to be generic with respect to their type
- parameter data: The data that the task needs to operate on
- parameter retries: Number of times this task has been retried after failing
- parameter queuePriority: The priority
- parameter qualityOfService: The quality of service
- returns: A new SYNQueueTask
*/
public convenience init(queue: SYNQueue, type: String, data: AnyObject? = nil, retries: Int = 0, priority: NSOperationQueuePriority = .Normal, quality: NSQualityOfService = .Utility) {
self.init(queue: queue, taskType: type, data: data, retries: retries, queuePriority: priority, qualityOfService: quality)
}
// For objective-c compatibility of convenience initializer
// See: http://sidhantgandhi.com/swift-default-parameter-values-in-convenience-initializers/
public convenience init(queue: SYNQueue, taskType: String) {
self.init(queue: queue, type: taskType)
}
/**
Initializes a SYNQueueTask from a dictionary
- parameter dictionary: A dictionary that contains the data to reconstruct a task
- parameter queue: The queue that the task will execute on
- returns: A new SYNQueueTask
*/
public convenience init?(dictionary: JSONDictionary, queue: SYNQueue) {
if let taskID = dictionary["taskID"] as? String,
let taskType = dictionary["taskType"] as? String,
let dependencyStrs = dictionary["dependencies"] as? [String]? ?? [],
let queuePriority = dictionary["queuePriority"] as? Int,
let qualityOfService = dictionary["qualityOfService"] as? Int,
let data: AnyObject? = dictionary["data"] as AnyObject??,
let createdStr = dictionary["created"] as? String,
let startedStr: String? = dictionary["started"] as? String ?? nil,
let retries = dictionary["retries"] as? Int? ?? 0
{
let created = NSDate(dateString: createdStr) ?? NSDate()
let started = (startedStr != nil) ? NSDate(dateString: startedStr!) : nil
let priority = NSOperationQueuePriority(rawValue: queuePriority) ?? .Normal
let qos = NSQualityOfService(rawValue: qualityOfService) ?? .Utility
self.init(queue: queue, taskID: taskID, taskType: taskType,
dependencyStrs: dependencyStrs, data: data, created: created,
started: started, retries: retries, queuePriority: priority,
qualityOfService: qos)
} else {
self.init(queue: queue, taskID: "", taskType: "")
return nil
}
}
/**
Initializes a SYNQueueTask from JSON
- parameter json: JSON from which the reconstruct the task
- parameter queue: The queue that the task will execute on
- returns: A new SYNQueueTask
*/
public convenience init?(json: String, queue: SYNQueue) {
do {
if let dict = try fromJSON(json) as? [String: AnyObject] {
self.init(dictionary: dict, queue: queue)
} else {
return nil
}
} catch {
return nil
}
}
/**
Setup the dependencies for the task
- parameter allTasks: Array of SYNQueueTasks that are dependencies of this task
*/
public func setupDependencies(allTasks: [SYNQueueTask]) {
dependencyStrs.forEach {
(taskID: String) -> Void in
let found = allTasks.filter({ taskID == $0.name })
if let task = found.first {
self.addDependency(task)
} else {
let name = self.name ?? "(unknown)"
self.queue.log(.Warning, "Discarding missing dependency \(taskID) from \(name)")
}
}
}
/**
Deconstruct the task to a dictionary, used to serialize the task
- returns: A Dictionary representation of the task
*/
public func toDictionary() -> [String: AnyObject?] {
var dict = [String: AnyObject?]()
dict["taskID"] = self.taskID
dict["taskType"] = self.taskType
dict["dependencies"] = self.dependencyStrs
dict["queuePriority"] = self.queuePriority.rawValue
dict["qualityOfService"] = self.qualityOfService.rawValue
dict["data"] = self.data
dict["created"] = self.created.toISOString()
dict["started"] = (self.started != nil) ? self.started!.toISOString() : nil
dict["retries"] = self.retries
return dict
}
/**
Deconstruct the task to a JSON string, used to serialize the task
- returns: A JSON string representation of the task
*/
public func toJSONString() -> String? {
// Serialize this task to a dictionary
let dict = toDictionary()
// Convert the dictionary to an NSDictionary by replacing nil values
// with NSNull
let nsdict = NSMutableDictionary(capacity: dict.count)
for (key, value) in dict {
nsdict[key] = value ?? NSNull()
}
do {
let json = try toJSON(nsdict)
return json
} catch {
return nil
}
}
/**
Starts executing the task
*/
public override func start() {
super.start()
executing = true
run()
}
/**
Cancels the task
*/
public override func cancel() {
lastError = NSError(domain: "SYNQueue", code: -1, userInfo: [NSLocalizedDescriptionKey: "Task \(taskID) was cancelled"])
super.cancel()
queue.log(.Debug, "Canceled task \(taskID)")
finished = true
}
func run() {
if cancelled && !finished { finished = true }
if finished { return }
queue.runTask(self)
}
/**
Call this to mark the task as completed, even if it failed. If it failed, we will use exponential backoff to keep retrying
the task until max number of retries is reached. Once this happens, we cancel the task.
- parameter error: If the task failed, pass an error to indicate why
*/
public func completed(error: NSError?) {
// Check to make sure we're even executing, if not
// just ignore the completed call
if (!executing) {
queue.log(.Debug, "Completion called on already completed task \(taskID)")
return
}
if let error = error {
lastError = error
queue.log(.Warning, "Task \(taskID) failed with error: \(error)")
// Check if we've exceeded the max allowed retries
if ++retries >= queue.maxRetries {
queue.log(.Error, "Max retries exceeded for task \(taskID)")
cancel()
return
}
// Wait a bit (exponential backoff) and retry this task
let exp = Double(min(queue.maxRetries ?? 0, retries))
let seconds:NSTimeInterval = min(SYNQueueTask.MAX_RETRY_DELAY, SYNQueueTask.MIN_RETRY_DELAY * pow(2.0, exp - 1))
queue.log(.Debug, "Waiting \(seconds) seconds to retry task \(taskID)")
runInBackgroundAfter(seconds) { self.run() }
} else {
lastError = nil
queue.log(.Debug, "Task \(taskID) completed")
finished = true
}
}
}