@@ -121,13 +121,21 @@ - (instancetype)initWithService:(GTLRService *)service
121121@property (nonatomic , assign ) UIBackgroundTaskIdentifier backgroundTaskIdentifier;
122122#endif // GTM_BACKGROUND_TASK_FETCHING
123123
124+ // Dispatch group enabling waitForTicket: to delay until async callbacks and notifications
125+ // related to the ticket have completed.
126+ @property (nonatomic , readonly ) dispatch_group_t callbackGroup;
127+
124128// startBackgroundTask and endBackgroundTask do nothing if !GTM_BACKGROUND_TASK_FETCHING
125129- (void )startBackgroundTask ;
126130- (void )endBackgroundTask ;
127131
128132- (void )notifyStarting : (BOOL )isStarting ;
129133- (void )releaseTicketCallbacks ;
130134
135+ // Posts a notification on the main queue using the ticket's dispatch group.
136+ - (void )postNotificationOnMainThreadWithName : (NSString *)name
137+ object : (id )object
138+ userInfo : (NSDictionary *)userInfo ;
131139@end
132140
133141#if !defined(GTLR_HAS_SESSION_UPLOAD_FETCHER_IMPORT)
@@ -502,19 +510,23 @@ - (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)targetURL
502510
503511 if (uploadParams.shouldUploadWithSingleRequest ) {
504512 NSData *uploadData = uploadParams.data ;
513+ NSString *uploadMIMEType = uploadParams.MIMEType ;
505514 if (!uploadData) {
506515 GTLR_DEBUG_ASSERT (0 , @" Uploading with a single request requires bytes to upload as NSData" );
507516 } else {
508517 if (uploadParams.shouldSendUploadOnly ) {
509- contentType = uploadParams. MIMEType ;
510- dataToPost = uploadParams. data ;
518+ contentType = uploadMIMEType ;
519+ dataToPost = uploadData ;
511520 contentLength = @(dataToPost.length ).stringValue ;
512521 } else {
513522 GTMMIMEDocument *mimeDoc = [GTMMIMEDocument MIMEDocument ];
514- [mimeDoc addPartWithHeaders: @{ @" Content-Type" : contentType }
515- body: dataToPost];
516- [mimeDoc addPartWithHeaders: @{ @" Content-Type" : uploadParams.MIMEType }
517- body: uploadParams.data];
523+ if (dataToPost) {
524+ // Include the object as metadata with the upload.
525+ [mimeDoc addPartWithHeaders: @{ @" Content-Type" : contentType }
526+ body: dataToPost];
527+ }
528+ [mimeDoc addPartWithHeaders: @{ @" Content-Type" : uploadMIMEType }
529+ body: uploadData];
518530
519531 dispatch_data_t mimeDispatchData;
520532 unsigned long long mimeLength;
@@ -607,7 +619,7 @@ - (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)targetURL
607619 if (!retryBlock) {
608620 response (suggestedWillRetry);
609621 } else {
610- dispatch_async ( ticket.callbackQueue , ^{
622+ dispatch_group_async (ticket. callbackGroup , ticket.callbackQueue , ^{
611623 if (ticket.cancelled ) {
612624 response (NO );
613625 return ;
@@ -1015,7 +1027,7 @@ - (void)invokeProgressCallbackForTicket:(GTLRServiceTicket *)ticket
10151027
10161028 GTLRServiceUploadProgressBlock block = ticket.uploadProgressBlock ;
10171029 if (block) {
1018- dispatch_async ( ticket.callbackQueue , ^{
1030+ dispatch_group_async (ticket. callbackGroup , ticket.callbackQueue , ^{
10191031 if (ticket.cancelled ) return ;
10201032
10211033 block (ticket, numReadSoFar, total);
@@ -1040,9 +1052,9 @@ - (void)prepareToParseObjectForFetcher:(GTMSessionFetcher *)fetcher
10401052 completionHandler : (GTLRServiceCompletionHandler)completionHandler {
10411053 GTLR_ASSERT_CURRENT_QUEUE_DEBUG (self.parseQueue );
10421054
1043- [[ self class ] postNotificationOnMainThreadWithName: kGTLRServiceTicketParsingStartedNotification
1044- object: ticket
1045- userInfo: nil ];
1055+ [ticket postNotificationOnMainThreadWithName: kGTLRServiceTicketParsingStartedNotification
1056+ object: ticket
1057+ userInfo: nil ];
10461058
10471059 // For unit tests to cancel during parsing, we need a synchronous notification posted.
10481060 // Because this notification is intended only for unit tests, there is no public symbol
@@ -1194,9 +1206,9 @@ - (void)handleParsedObjectForFetcher:(GTMSessionFetcher *)fetcher
11941206
11951207 if (hasSentParsingStartNotification) {
11961208 // we want to always balance the start and stop notifications
1197- [[ self class ] postNotificationOnMainThreadWithName: kGTLRServiceTicketParsingStoppedNotification
1198- object: ticket
1199- userInfo: nil ];
1209+ [ticket postNotificationOnMainThreadWithName: kGTLRServiceTicketParsingStoppedNotification
1210+ object: ticket
1211+ userInfo: nil ];
12001212 }
12011213
12021214 BOOL shouldCallCallbacks = YES ;
@@ -1280,7 +1292,7 @@ - (void)handleParsedObjectForFetcher:(GTMSessionFetcher *)fetcher
12801292 if (!shouldCallCallbacks) {
12811293 // More fetches are happening.
12821294 } else {
1283- dispatch_async ( ticket.callbackQueue , ^{
1295+ dispatch_group_async (ticket. callbackGroup , ticket.callbackQueue , ^{
12841296 // First, call query-specific callback blocks. We do this before the
12851297 // fetch callback to let applications do any final clean-up (or update
12861298 // their UI) in the fetch callback.
@@ -1309,7 +1321,7 @@ - (void)handleParsedObjectForFetcher:(GTMSessionFetcher *)fetcher
13091321 [ticket releaseTicketCallbacks ];
13101322 [ticket endBackgroundTask ];
13111323
1312- // Even if the ticket has been canceled , it should notify that it's stopped.
1324+ // Even if the ticket has been cancelled , it should notify that it's stopped.
13131325 [ticket notifyStarting: NO ];
13141326
13151327 // Release query callback blocks.
@@ -1597,7 +1609,7 @@ - (void)simulateFetchWithTicket:(GTLRServiceTicket *)ticket
15971609 ticket.executingQuery = originalQuery;
15981610
15991611 testBlock (ticket, ^(id testObject, NSError *testError) {
1600- dispatch_async ( ticket.callbackQueue , ^{
1612+ dispatch_group_async (ticket. callbackGroup , ticket.callbackQueue , ^{
16011613 if (testError) {
16021614 // During simulation, we invoke any retry block, but ignore the result.
16031615 const BOOL willRetry = NO ;
@@ -1622,12 +1634,12 @@ - (void)simulateFetchWithTicket:(GTLRServiceTicket *)ticket
16221634 deliveredBytes: (unsigned long long )totalSentSoFar
16231635 totalBytes: (unsigned long long )uploadLength];
16241636 }
1625- [[ self class ] postNotificationOnMainThreadWithName: kGTLRServiceTicketParsingStartedNotification
1626- object: ticket
1627- userInfo: nil ];
1628- [[ self class ] postNotificationOnMainThreadWithName: kGTLRServiceTicketParsingStoppedNotification
1629- object: ticket
1630- userInfo: nil ];
1637+ [ticket postNotificationOnMainThreadWithName: kGTLRServiceTicketParsingStartedNotification
1638+ object: ticket
1639+ userInfo: nil ];
1640+ [ticket postNotificationOnMainThreadWithName: kGTLRServiceTicketParsingStoppedNotification
1641+ object: ticket
1642+ userInfo: nil ];
16311643 }
16321644 }
16331645
@@ -1657,7 +1669,7 @@ - (void)simulateFetchWithTicket:(GTLRServiceTicket *)ticket
16571669 [ticket notifyStarting: NO ];
16581670
16591671 [originalQuery invalidateQuery ];
1660- }); // dispatch_async
1672+ }); // dispatch_group_async
16611673 }); // testBlock
16621674}
16631675
@@ -2113,17 +2125,6 @@ - (void)setUserAgent:(NSString *)userAgent {
21132125 [self setExactUserAgent: str];
21142126}
21152127
2116- + (void )postNotificationOnMainThreadWithName : (NSString *)name
2117- object : (id )object
2118- userInfo : (NSDictionary *)userInfo {
2119- // We always post these async to ensure they remain in order.
2120- dispatch_async (dispatch_get_main_queue (), ^{
2121- [[NSNotificationCenter defaultCenter ] postNotificationName: name
2122- object: object
2123- userInfo: userInfo];
2124- });
2125- }
2126-
21272128#pragma mark -
21282129
21292130+ (NSDictionary <NSString *, Class> *)kindStringToClassMap {
@@ -2241,17 +2242,32 @@ + (instancetype)mockServiceWithFakedObject:(id)objectOrNil
22412242
22422243- (BOOL )waitForTicket : (GTLRServiceTicket *)ticket
22432244 timeout : (NSTimeInterval )timeoutInSeconds {
2244- // Loop until the fetch completes with an object or an error,
2245- // or until the timeout has expired.
2245+ // Loop until the fetch completes or is cancelled, or until the timeout has expired.
22462246 NSDate *giveUpDate = [NSDate dateWithTimeIntervalSinceNow: timeoutInSeconds];
22472247
2248- while (!ticket.hasCalledCallback && giveUpDate.timeIntervalSinceNow > 0 ) {
2248+ BOOL hasTimedOut = NO ;
2249+ while (1 ) {
2250+ int64_t delta = (int64_t )(100 * NSEC_PER_MSEC); // 100 ms
2251+ BOOL areCallbacksPending =
2252+ (dispatch_group_wait (ticket.callbackGroup , dispatch_time (DISPATCH_TIME_NOW, delta)) != 0 );
2253+
2254+ if (!areCallbacksPending && (ticket.hasCalledCallback || ticket.cancelled )) break ;
2255+
2256+ hasTimedOut = (giveUpDate.timeIntervalSinceNow <= 0 );
2257+ if (hasTimedOut) {
2258+ if (areCallbacksPending) {
2259+ // A timeout while waiting for the dispatch group to finish is seriously unexpected.
2260+ GTLR_DEBUG_LOG (@" %s timed out while waiting for the dispatch group" , __PRETTY_FUNCTION__);
2261+ }
2262+ break ;
2263+ }
2264+
22492265 // Run the current run loop 1/1000 of a second to give the networking
22502266 // code a chance to work.
22512267 NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow: 0.001 ];
22522268 [[NSRunLoop currentRunLoop ] runUntilDate: stopDate];
22532269 }
2254- return ticket. hasCalledCallback ;
2270+ return !hasTimedOut ;
22552271}
22562272
22572273@end
@@ -2267,6 +2283,7 @@ @implementation GTLRServiceTicket {
22672283 allowInsecureQueries = _allowInsecureQueries,
22682284 authorizer = _authorizer,
22692285 cancelled = _cancelled,
2286+ callbackGroup = _callbackGroup,
22702287 callbackQueue = _callbackQueue,
22712288 creationDate = _creationDate,
22722289 executingQuery = _executingQuery,
@@ -2338,6 +2355,7 @@ - (instancetype)initWithService:(GTLRService *)service
23382355 _testBlock = params.testBlock ?: service.testBlock ;
23392356
23402357 _callbackQueue = ((_Nonnull dispatch_queue_t )params.callbackQueue ) ?: service.callbackQueue ;
2358+ _callbackGroup = dispatch_group_create ();
23412359
23422360 _apiKey = [service.APIKey copy ];
23432361 _allowInsecureQueries = service.allowInsecureQueries ;
@@ -2367,6 +2385,17 @@ - (NSString *)description {
23672385 [self class ], self , _service, devKeyInfo, authorizerInfo, _objectFetcher];
23682386}
23692387
2388+ - (void )postNotificationOnMainThreadWithName : (NSString *)name
2389+ object : (id )object
2390+ userInfo : (NSDictionary *)userInfo {
2391+ // We always post these async to ensure they remain in order.
2392+ dispatch_group_async (self.callbackGroup , dispatch_get_main_queue (), ^{
2393+ [[NSNotificationCenter defaultCenter ] postNotificationName: name
2394+ object: object
2395+ userInfo: userInfo];
2396+ });
2397+ }
2398+
23702399- (void )pauseUpload {
23712400 GTMSessionFetcher *fetcher = self.objectFetcher ;
23722401 BOOL canPause = [fetcher respondsToSelector: @selector (pauseFetching )];
@@ -2507,9 +2536,9 @@ - (void)notifyStarting:(BOOL)isStarting {
25072536 name = kGTLRServiceTicketStoppedNotification ;
25082537 _needsStopNotification = NO ;
25092538 }
2510- [GTLRService postNotificationOnMainThreadWithName: name
2511- object: self
2512- userInfo: nil ];
2539+ [self postNotificationOnMainThreadWithName: name
2540+ object: self
2541+ userInfo: nil ];
25132542}
25142543
25152544- (id )service {
0 commit comments