FSTDispatchQueue.mm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. /*
  2. * Copyright 2017 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import <Foundation/Foundation.h>
  17. #import "Firestore/Source/Util/FSTAssert.h"
  18. #import "Firestore/Source/Util/FSTDispatchQueue.h"
  19. NS_ASSUME_NONNULL_BEGIN
  20. /**
  21. * removeDelayedCallback is used by FSTDelayedCallback and so we pre-declare it before the rest of
  22. * the FSTDispatchQueue private interface.
  23. */
  24. @interface FSTDispatchQueue ()
  25. - (void)removeDelayedCallback:(FSTDelayedCallback *)callback;
  26. @end
  27. #pragma mark - FSTDelayedCallback
  28. /**
  29. * Represents a callback scheduled to be run in the future on an FSTDispatchQueue.
  30. *
  31. * It is created via [FSTDelayedCallback createAndScheduleWithQueue].
  32. *
  33. * Supports cancellation (via cancel) and early execution (via skipDelay).
  34. */
  35. @interface FSTDelayedCallback ()
  36. @property(nonatomic, strong, readonly) FSTDispatchQueue *queue;
  37. @property(nonatomic, assign, readonly) FSTTimerID timerID;
  38. @property(nonatomic, assign, readonly) NSTimeInterval targetTime;
  39. @property(nonatomic, copy) void (^callback)();
  40. /** YES if the callback has been run or canceled. */
  41. @property(nonatomic, getter=isDone) BOOL done;
  42. /**
  43. * Creates and returns an FSTDelayedCallback that has been scheduled on the provided queue with the
  44. * provided delay.
  45. *
  46. * @param queue The FSTDispatchQueue to run the callback on.
  47. * @param timerID A FSTTimerID identifying the type of the delayed callback.
  48. * @param delay The delay before the callback should be scheduled.
  49. * @param callback The callback block to run.
  50. * @return The created FSTDelayedCallback instance.
  51. */
  52. + (instancetype)createAndScheduleWithQueue:(FSTDispatchQueue *)queue
  53. timerID:(FSTTimerID)timerID
  54. delay:(NSTimeInterval)delay
  55. callback:(void (^)(void))callback;
  56. /**
  57. * Queues the callback to run immediately (if it hasn't already been run or canceled).
  58. */
  59. - (void)skipDelay;
  60. @end
  61. @implementation FSTDelayedCallback
  62. - (instancetype)initWithQueue:(FSTDispatchQueue *)queue
  63. timerID:(FSTTimerID)timerID
  64. targetTime:(NSTimeInterval)targetTime
  65. callback:(void (^)(void))callback {
  66. if (self = [super init]) {
  67. _queue = queue;
  68. _timerID = timerID;
  69. _targetTime = targetTime;
  70. _callback = callback;
  71. _done = NO;
  72. }
  73. return self;
  74. }
  75. + (instancetype)createAndScheduleWithQueue:(FSTDispatchQueue *)queue
  76. timerID:(FSTTimerID)timerID
  77. delay:(NSTimeInterval)delay
  78. callback:(void (^)(void))callback {
  79. NSTimeInterval targetTime = [[NSDate date] timeIntervalSince1970] + delay;
  80. FSTDelayedCallback *delayedCallback = [[FSTDelayedCallback alloc] initWithQueue:queue
  81. timerID:timerID
  82. targetTime:targetTime
  83. callback:callback];
  84. [delayedCallback startWithDelay:delay];
  85. return delayedCallback;
  86. }
  87. /**
  88. * Starts the timer. This is called immediately after construction by createAndScheduleWithQueue.
  89. */
  90. - (void)startWithDelay:(NSTimeInterval)delay {
  91. dispatch_time_t delayNs = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC));
  92. dispatch_after(delayNs, self.queue.queue, ^{
  93. [self.queue enterCheckedOperation:^{
  94. [self delayDidElapse];
  95. }];
  96. });
  97. }
  98. - (void)skipDelay {
  99. [self.queue dispatchAsyncAllowingSameQueue:^{
  100. [self delayDidElapse];
  101. }];
  102. }
  103. - (void)cancel {
  104. [self.queue verifyIsCurrentQueue];
  105. if (!self.isDone) {
  106. // PORTING NOTE: There's no way to actually cancel the dispatched callback, but it'll be a no-op
  107. // since we set done to YES.
  108. [self markDone];
  109. }
  110. }
  111. - (void)delayDidElapse {
  112. [self.queue verifyIsCurrentQueue];
  113. if (!self.isDone) {
  114. [self markDone];
  115. self.callback();
  116. }
  117. }
  118. /**
  119. * Marks this delayed callback as done, and notifies the FSTDispatchQueue that it should be removed.
  120. */
  121. - (void)markDone {
  122. self.done = YES;
  123. [self.queue removeDelayedCallback:self];
  124. }
  125. @end
  126. #pragma mark - FSTDispatchQueue
  127. @interface FSTDispatchQueue ()
  128. /**
  129. * Callbacks scheduled to be queued in the future. Callbacks are automatically removed after they
  130. * are run or canceled.
  131. */
  132. @property(nonatomic, strong, readonly) NSMutableArray<FSTDelayedCallback *> *delayedCallbacks;
  133. /**
  134. * Flag set while an FSTDispatchQueue operation is currently executing. Used for assertion
  135. * sanity-checks.
  136. */
  137. @property(nonatomic, assign) BOOL operationInProgress;
  138. - (instancetype)initWithQueue:(dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER;
  139. @end
  140. @implementation FSTDispatchQueue
  141. + (instancetype)queueWith:(dispatch_queue_t)dispatchQueue {
  142. return [[FSTDispatchQueue alloc] initWithQueue:dispatchQueue];
  143. }
  144. - (instancetype)initWithQueue:(dispatch_queue_t)queue {
  145. if (self = [super init]) {
  146. _queue = queue;
  147. _delayedCallbacks = [NSMutableArray array];
  148. _operationInProgress = NO;
  149. }
  150. return self;
  151. }
  152. - (void)verifyIsCurrentQueue {
  153. FSTAssert([self onTargetQueue],
  154. @"We are running on the wrong dispatch queue. Expected '%@' Actual: '%@'",
  155. [self targetQueueLabel], [self currentQueueLabel]);
  156. FSTAssert(_operationInProgress,
  157. @"verifyIsCurrentQueue called outside enterCheckedOperation on queue '%@'",
  158. [self currentQueueLabel]);
  159. }
  160. - (void)enterCheckedOperation:(void (^)(void))block {
  161. FSTAssert(!_operationInProgress,
  162. @"enterCheckedOperation may not be called when an operation is in progress");
  163. @try {
  164. _operationInProgress = YES;
  165. [self verifyIsCurrentQueue];
  166. block();
  167. } @finally {
  168. _operationInProgress = NO;
  169. }
  170. }
  171. - (void)dispatchAsync:(void (^)(void))block {
  172. FSTAssert(!_operationInProgress || ![self onTargetQueue],
  173. @"dispatchAsync called when we are already running on target dispatch queue '%@'",
  174. [self targetQueueLabel]);
  175. dispatch_async(self.queue, ^{
  176. [self enterCheckedOperation:block];
  177. });
  178. }
  179. - (void)dispatchAsyncAllowingSameQueue:(void (^)(void))block {
  180. dispatch_async(self.queue, ^{
  181. [self enterCheckedOperation:block];
  182. });
  183. }
  184. - (void)dispatchSync:(void (^)(void))block {
  185. FSTAssert(!_operationInProgress || ![self onTargetQueue],
  186. @"dispatchSync called when we are already running on target dispatch queue '%@'",
  187. [self targetQueueLabel]);
  188. dispatch_sync(self.queue, ^{
  189. [self enterCheckedOperation:block];
  190. });
  191. }
  192. - (FSTDelayedCallback *)dispatchAfterDelay:(NSTimeInterval)delay
  193. timerID:(FSTTimerID)timerID
  194. block:(void (^)(void))block {
  195. // While not necessarily harmful, we currently don't expect to have multiple callbacks with the
  196. // same timerID in the queue, so defensively reject them.
  197. // TODO(b/74749605): If a user change happens while offline we can end up with multiple backoff
  198. // callbacks in the dispatch queue. This is non-harmful so I'm just disabling the assert until we
  199. // get that cleaned up.
  200. // FSTAssert(![self containsDelayedCallbackWithTimerID:timerID],
  201. // @"Attempted to schedule multiple callbacks with id %ld", (unsigned long)timerID);
  202. FSTDelayedCallback *delayedCallback = [FSTDelayedCallback createAndScheduleWithQueue:self
  203. timerID:timerID
  204. delay:delay
  205. callback:block];
  206. [self.delayedCallbacks addObject:delayedCallback];
  207. return delayedCallback;
  208. }
  209. - (BOOL)containsDelayedCallbackWithTimerID:(FSTTimerID)timerID {
  210. NSUInteger matchIndex = [self.delayedCallbacks
  211. indexOfObjectPassingTest:^BOOL(FSTDelayedCallback *obj, NSUInteger idx, BOOL *stop) {
  212. return obj.timerID == timerID;
  213. }];
  214. return matchIndex != NSNotFound;
  215. }
  216. - (void)runDelayedCallbacksUntil:(FSTTimerID)lastTimerID {
  217. dispatch_semaphore_t doneSemaphore = dispatch_semaphore_create(0);
  218. [self dispatchAsync:^{
  219. FSTAssert(lastTimerID == FSTTimerIDAll || [self containsDelayedCallbackWithTimerID:lastTimerID],
  220. @"Attempted to run callbacks until missing timer ID: %ld",
  221. (unsigned long)lastTimerID);
  222. [self sortDelayedCallbacks];
  223. for (FSTDelayedCallback *callback in self.delayedCallbacks) {
  224. [callback skipDelay];
  225. if (lastTimerID != FSTTimerIDAll && callback.timerID == lastTimerID) {
  226. break;
  227. }
  228. }
  229. // Now that the callbacks are queued, we want to enqueue an additional item to release the
  230. // 'done' semaphore.
  231. [self dispatchAsyncAllowingSameQueue:^{
  232. dispatch_semaphore_signal(doneSemaphore);
  233. }];
  234. }];
  235. dispatch_semaphore_wait(doneSemaphore, DISPATCH_TIME_FOREVER);
  236. }
  237. // NOTE: For performance we could store the callbacks sorted (e.g. using std::priority_queue),
  238. // but this sort only happens in tests (if runDelayedCallbacksUntil: is called), and the size
  239. // is guaranteed to be small since we don't allow duplicate TimerIds (of which there are only 4).
  240. - (void)sortDelayedCallbacks {
  241. // We want to run callbacks in the same order they'd run if they ran naturally.
  242. [self.delayedCallbacks
  243. sortUsingComparator:^NSComparisonResult(FSTDelayedCallback *a, FSTDelayedCallback *b) {
  244. return a.targetTime < b.targetTime
  245. ? NSOrderedAscending
  246. : a.targetTime > b.targetTime ? NSOrderedDescending : NSOrderedSame;
  247. }];
  248. }
  249. /** Called by FSTDelayedCallback when a callback is run or canceled. */
  250. - (void)removeDelayedCallback:(FSTDelayedCallback *)callback {
  251. NSUInteger index = [self.delayedCallbacks indexOfObject:callback];
  252. FSTAssert(index != NSNotFound, @"Delayed callback not found.");
  253. [self.delayedCallbacks removeObjectAtIndex:index];
  254. }
  255. #pragma mark - Private Methods
  256. - (NSString *)currentQueueLabel {
  257. return [NSString stringWithUTF8String:dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)];
  258. }
  259. - (NSString *)targetQueueLabel {
  260. return [NSString stringWithUTF8String:dispatch_queue_get_label(self.queue)];
  261. }
  262. - (BOOL)onTargetQueue {
  263. return [[self currentQueueLabel] isEqualToString:[self targetQueueLabel]];
  264. }
  265. @end
  266. NS_ASSUME_NONNULL_END