diff --git a/src/Messages/OWSMessageSender.m b/src/Messages/OWSMessageSender.m index 70fcef4c5..b45f3ce5c 100644 --- a/src/Messages/OWSMessageSender.m +++ b/src/Messages/OWSMessageSender.m @@ -41,6 +41,164 @@ NS_ASSUME_NONNULL_BEGIN +/** + * OWSSendMessageOperation encapsulates all the work associated with sending a message, e.g. uploading attachments, + * getting proper keys, and retrying upon failure. + * + * Used by `OWSMessageSender` to serialize message sending, ensuring that messages are emitted in the order they + * were sent. + */ +@interface OWSSendMessageOperation : NSOperation + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMessage:(TSOutgoingMessage *)message + messageSender:(OWSMessageSender *)messageSender + success:(void (^)())successHandler + failure:(void (^)(NSError *_Nonnull error))failureHandler NS_DESIGNATED_INITIALIZER; + +@end + +typedef NS_ENUM(NSInteger, OWSSendMessageOperationState) { + OWSSendMessageOperationStateNew, + OWSSendMessageOperationStateExecuting, + OWSSendMessageOperationStateFinished +}; + +@interface OWSMessageSender (OWSSendMessageOperation) + +- (void)attemptToSendMessage:(TSOutgoingMessage *)message + success:(void (^)())successHandler + failure:(void (^)(NSError *error))failureHandler; + +@end + +NSString *const OWSSendMessageOperationKeyIsExecuting = @"isExecuting"; +NSString *const OWSSendMessageOperationKeyIsFinished = @"isFinished"; + +NSUInteger const OWSSendMessageOperationMaxRetries = 4; + +@interface OWSSendMessageOperation () + +@property (nonatomic, readonly) TSOutgoingMessage *message; +@property (nonatomic, readonly) OWSMessageSender *messageSender; +@property (nonatomic, readonly) void (^successHandler)(); +@property (nonatomic, readonly) void (^failureHandler)(NSError *_Nonnull error); +@property (atomic) OWSSendMessageOperationState operationState; + +@end + +@implementation OWSSendMessageOperation + +- (instancetype)initWithMessage:(TSOutgoingMessage *)message + messageSender:(OWSMessageSender *)messageSender + success:(void (^)())aSuccessHandler + failure:(void (^)(NSError *_Nonnull error))aFailureHandler +{ + self = [super init]; + if (!self) { + return self; + } + + _operationState = OWSSendMessageOperationStateNew; + + _message = message; + _messageSender = messageSender; + + __weak typeof(self) weakSelf = self; + _successHandler = ^{ + typeof(self) strongSelf = weakSelf; + if (!strongSelf) { + OWSAssert(NO); + return; + } + DDLogDebug(@"%@ succeeded.", strongSelf.tag); + aSuccessHandler(); + [strongSelf markAsComplete]; + }; + + _failureHandler = ^(NSError *_Nonnull error) { + typeof(self) strongSelf = weakSelf; + if (!strongSelf) { + OWSAssert(NO); + return; + } + + DDLogDebug(@"%@ failed with error: %@", strongSelf.tag, error); + aFailureHandler(error); + [strongSelf markAsComplete]; + }; + + return self; +} + +#pragma mark - NSOperation overrides + +- (BOOL)isExecuting +{ + return self.operationState == OWSSendMessageOperationStateExecuting; +} + +- (BOOL)isFinished +{ + return self.operationState == OWSSendMessageOperationStateFinished; +} + +- (void)start +{ + [self willChangeValueForKey:OWSSendMessageOperationKeyIsExecuting]; + self.operationState = OWSSendMessageOperationStateExecuting; + [self didChangeValueForKey:OWSSendMessageOperationKeyIsExecuting]; + [self main]; +} + +- (void)main +{ + [self tryWithRemainingRetries:OWSSendMessageOperationMaxRetries]; +} + +#pragma mark - methods + +- (void)tryWithRemainingRetries:(NSUInteger)remainingRetries +{ + DDLogDebug(@"%@ remainingRetries: %lu", self.tag, remainingRetries); + + void (^retryableFailureHandler)(NSError *_Nonnull) = ^(NSError *_Nonnull error) { + DDLogInfo(@"%@ Sending failed.", self.tag); + if (remainingRetries > 0) { + [self tryWithRemainingRetries:remainingRetries - 1]; + } else { + DDLogWarn(@"%@ Too many failures. Giving up sending.", self.tag); + self.failureHandler(error); + } + }; + + [self.messageSender attemptToSendMessage:self.message success:self.successHandler failure:retryableFailureHandler]; +} + +- (void)markAsComplete +{ + [self willChangeValueForKey:OWSSendMessageOperationKeyIsExecuting]; + [self willChangeValueForKey:OWSSendMessageOperationKeyIsFinished]; + self.operationState = OWSSendMessageOperationStateFinished; + [self didChangeValueForKey:OWSSendMessageOperationKeyIsExecuting]; + [self didChangeValueForKey:OWSSendMessageOperationKeyIsFinished]; +} + +#pragma mark - Logging + ++ (NSString *)tag +{ + return [NSString stringWithFormat:@"[%@]", self.class]; +} + +- (NSString *)tag +{ + return self.class.tag; +} + +@end + + int const OWSMessageSenderRetryAttempts = 3; NSString *const OWSMessageSenderInvalidDeviceException = @"InvalidDeviceException"; NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; @@ -54,6 +212,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; @property (nonatomic, readonly) id contactsManager; @property (nonatomic, readonly) ContactsUpdater *contactsUpdater; @property (nonatomic, readonly) OWSDisappearingMessagesJob *disappearingMessagesJob; +@property (nonatomic, readonly) NSOperationQueue *sendingQueue; @end @@ -73,6 +232,9 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; _storageManager = storageManager; _contactsManager = contactsManager; _contactsUpdater = contactsUpdater; + _sendingQueue = [NSOperationQueue new]; + _sendingQueue.qualityOfService = NSOperationQualityOfServiceUserInitiated; + _sendingQueue.maxConcurrentOperationCount = 1; _uploadingService = [[OWSUploadingService alloc] initWithNetworkManager:networkManager]; _dbConnection = storageManager.newDatabaseConnection; @@ -84,6 +246,18 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; - (void)sendMessage:(TSOutgoingMessage *)message success:(void (^)())successHandler failure:(void (^)(NSError *error))failureHandler +{ + [self saveMessage:message withState:TSOutgoingMessageStateAttemptingOut]; + OWSSendMessageOperation *sendMessageOperation = [[OWSSendMessageOperation alloc] initWithMessage:message + messageSender:self + success:successHandler + failure:failureHandler]; + [self.sendingQueue addOperation:sendMessageOperation]; +} + +- (void)attemptToSendMessage:(TSOutgoingMessage *)message + success:(void (^)())successHandler + failure:(void (^)(NSError *error))failureHandler { DDLogDebug(@"%@ sending message: %@", self.tag, message.debugDescription); void (^markAndFailureHandler)(NSError *error) = ^(NSError *error) { @@ -167,8 +341,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; [attachmentStream save]; [message.attachmentIds addObject:attachmentStream.uniqueId]; - - message.messageState = TSOutgoingMessageStateAttemptingOut; [message save]; [self sendMessage:message success:successHandler failure:failureHandler]; @@ -266,9 +438,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; || [message isKindOfClass:[OWSOutgoingSyncMessage class]]) { TSContactThread *contactThread = (TSContactThread *)thread; - - [self saveMessage:message withState:TSOutgoingMessageStateAttemptingOut]; - if ([contactThread.contactIdentifier isEqualToString:self.storageManager.localNumber] && ![message isKindOfClass:[OWSOutgoingSyncMessage class]]) { diff --git a/src/Network/API/OWSUploadingService.m b/src/Network/API/OWSUploadingService.m index a509f8804..6dd3b4831 100644 --- a/src/Network/API/OWSUploadingService.m +++ b/src/Network/API/OWSUploadingService.m @@ -41,9 +41,6 @@ NSString *const kAttachmentUploadAttachmentIDKey = @"kAttachmentUploadAttachment success:(void (^)())successHandler failure:(void (^)(NSError *_Nonnull))failureHandler { - outgoingMessage.messageState = TSOutgoingMessageStateAttemptingOut; - [outgoingMessage save]; - if (attachmentStream.serverId) { DDLogDebug(@"%@ Attachment previously uploaded.", self.tag); successHandler(outgoingMessage);