mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-11 22:55:59 +05:45
* Don't leak upload packages or deadlock on lifecycle events GDTCORUploadPackages would call targetToStorage in -initWithTarget. When this method was running during app terminate, this would cause a deadlock because calling targetToStorage is re-entrant while GDTCORRegistrar is sending lifecycle events to all the instances it's tracking. NSTimer's invalidate method is also called whenever a package is completed, retried, or expired, without regard to the presence of a package handler. A unit test is added to ensure that GDTCORUploadPackage's are not leaked by a retain TSAN caught no issues * Don't mark GDTCORUploadPackage -init as unavailable
194 lines
6.7 KiB
Objective-C
194 lines
6.7 KiB
Objective-C
/*
|
|
* Copyright 2019 Google
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#import "GDTCORLibrary/Public/GDTCORUploadPackage.h"
|
|
|
|
#import <GoogleDataTransport/GDTCORClock.h>
|
|
#import <GoogleDataTransport/GDTCORConsoleLogger.h>
|
|
|
|
#import "GDTCORLibrary/Private/GDTCORRegistrar_Private.h"
|
|
#import "GDTCORLibrary/Private/GDTCORUploadCoordinator.h"
|
|
#import "GDTCORLibrary/Private/GDTCORUploadPackage_Private.h"
|
|
|
|
/** A class that holds a weak reference to an upload package, for use by the package's NSTimer. */
|
|
@interface GDTCORUploadPackageTimerHolder : NSObject
|
|
|
|
/** The upload package. */
|
|
@property(weak, nonatomic) GDTCORUploadPackage *package;
|
|
|
|
@end
|
|
|
|
@implementation GDTCORUploadPackageTimerHolder
|
|
|
|
/** Calls checkIfPackageIsExpired on the package if non-nil. Invalidates if the package is nil.
|
|
*
|
|
* @param timer The timer instance calling this method.
|
|
*/
|
|
- (void)timerFired:(NSTimer *)timer {
|
|
if (_package) {
|
|
[_package checkIfPackageIsExpired];
|
|
} else {
|
|
[timer invalidate];
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation GDTCORUploadPackage {
|
|
/** If YES, the package's -completeDelivery method has been called. */
|
|
BOOL _isDelivered;
|
|
|
|
/** If YES, is being handled by the handler. */
|
|
BOOL _isHandled;
|
|
|
|
/** A timer that will regularly check to see whether this package has expired or not. */
|
|
NSTimer *_expirationTimer;
|
|
}
|
|
|
|
- (instancetype)initWithTarget:(GDTCORTarget)target {
|
|
self = [super init];
|
|
if (self) {
|
|
_target = target;
|
|
_storage = [GDTCORRegistrar sharedInstance].targetToStorage[@(target)];
|
|
_deliverByTime = [GDTCORClock clockSnapshotInTheFuture:180000];
|
|
_handler = [GDTCORUploadCoordinator sharedInstance];
|
|
GDTCORUploadPackageTimerHolder *holder = [[GDTCORUploadPackageTimerHolder alloc] init];
|
|
holder.package = self;
|
|
_expirationTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
|
|
target:holder
|
|
selector:@selector(timerFired:)
|
|
userInfo:nil
|
|
repeats:YES];
|
|
}
|
|
GDTCORLogDebug(@"Upload package created %@", self);
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)copy {
|
|
GDTCORUploadPackage *newPackage = [[GDTCORUploadPackage alloc] init];
|
|
newPackage->_target = _target;
|
|
newPackage->_storage = _storage;
|
|
newPackage->_deliverByTime = _deliverByTime;
|
|
newPackage->_handler = _handler;
|
|
GDTCORUploadPackageTimerHolder *holder = [[GDTCORUploadPackageTimerHolder alloc] init];
|
|
holder.package = newPackage;
|
|
newPackage->_expirationTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
|
|
target:holder
|
|
selector:@selector(timerFired:)
|
|
userInfo:nil
|
|
repeats:YES];
|
|
newPackage->_events = [_events copy];
|
|
GDTCORLogDebug(@"Copying UploadPackage %@ to %@", self, newPackage);
|
|
return newPackage;
|
|
}
|
|
|
|
- (NSUInteger)hash {
|
|
return [_events hash];
|
|
}
|
|
|
|
- (BOOL)isEqual:(id)object {
|
|
return [self hash] == [object hash];
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[_expirationTimer invalidate];
|
|
}
|
|
|
|
- (void)completeDelivery {
|
|
if (_isDelivered) {
|
|
GDTCORLogError(GDTCORMCEDeliverTwice, @"%@",
|
|
@"It's an API violation to call -completeDelivery twice.");
|
|
}
|
|
_isDelivered = YES;
|
|
[_expirationTimer invalidate];
|
|
if (!_isHandled && _handler &&
|
|
[_handler respondsToSelector:@selector(packageDelivered:successful:)]) {
|
|
_isHandled = YES;
|
|
[_handler packageDelivered:[self copy] successful:YES];
|
|
}
|
|
GDTCORLogDebug(@"Upload package delivered: %@", self);
|
|
}
|
|
|
|
- (void)retryDeliveryInTheFuture {
|
|
[_expirationTimer invalidate];
|
|
if (!_isHandled && _handler &&
|
|
[_handler respondsToSelector:@selector(packageDelivered:successful:)]) {
|
|
_isHandled = YES;
|
|
[_handler packageDelivered:[self copy] successful:NO];
|
|
}
|
|
GDTCORLogDebug(@"Upload package will retry in the future: %@", self);
|
|
}
|
|
|
|
- (void)checkIfPackageIsExpired {
|
|
if ([[GDTCORClock snapshot] isAfter:_deliverByTime]) {
|
|
[_expirationTimer invalidate];
|
|
if (_handler && [_handler respondsToSelector:@selector(packageExpired:)]) {
|
|
_isHandled = YES;
|
|
GDTCORLogDebug(@"Upload package expired: %@", self);
|
|
[_handler packageExpired:self];
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - NSSecureCoding
|
|
|
|
/** The keyed archiver key for the events property. */
|
|
static NSString *const kEventsKey = @"GDTCORUploadPackageEventsKey";
|
|
|
|
/** The keyed archiver key for the _isHandled property. */
|
|
static NSString *const kDeliverByTimeKey = @"GDTCORUploadPackageDeliveryByTimeKey";
|
|
|
|
/** The keyed archiver key for the _isHandled ivar. */
|
|
static NSString *const kIsHandledKey = @"GDTCORUploadPackageIsHandledKey";
|
|
|
|
/** The keyed archiver key for the handler property. */
|
|
static NSString *const kHandlerKey = @"GDTCORUploadPackageHandlerKey";
|
|
|
|
/** The keyed archiver key for the target property. */
|
|
static NSString *const kTargetKey = @"GDTCORUploadPackageTargetKey";
|
|
|
|
+ (BOOL)supportsSecureCoding {
|
|
return YES;
|
|
}
|
|
|
|
- (void)encodeWithCoder:(nonnull NSCoder *)aCoder {
|
|
[aCoder encodeObject:_events forKey:kEventsKey];
|
|
[aCoder encodeObject:_deliverByTime forKey:kDeliverByTimeKey];
|
|
[aCoder encodeBool:_isHandled forKey:kIsHandledKey];
|
|
[aCoder encodeObject:_handler forKey:kHandlerKey];
|
|
[aCoder encodeInteger:_target forKey:kTargetKey];
|
|
}
|
|
|
|
- (nullable instancetype)initWithCoder:(nonnull NSCoder *)aDecoder {
|
|
// Sets a global translation mapping to decode GDTCORStoredEvent objects encoded as instances of
|
|
// GDTCOREvent instead.
|
|
[NSKeyedUnarchiver setClass:[GDTCOREvent class] forClassName:@"GDTCORStoredEvent"];
|
|
|
|
GDTCORTarget target = [aDecoder decodeIntegerForKey:kTargetKey];
|
|
self = [self initWithTarget:target];
|
|
if (self) {
|
|
NSSet *classes = [NSSet setWithObjects:[NSSet class], [GDTCOREvent class], nil];
|
|
_events = [aDecoder decodeObjectOfClasses:classes forKey:kEventsKey];
|
|
_deliverByTime = [aDecoder decodeObjectOfClass:[GDTCORClock class] forKey:kDeliverByTimeKey];
|
|
_isHandled = [aDecoder decodeBoolForKey:kIsHandledKey];
|
|
// _handler isn't technically NSSecureCoding, because we don't know the class of this object.
|
|
// but it gets decoded anyway.
|
|
}
|
|
return self;
|
|
}
|
|
|
|
@end
|