session-ios/SignalServiceKit/src/Messages/OWSBatchMessageProcessor.m

470 lines
14 KiB
Mathematica
Raw Normal View History

//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSBatchMessageProcessor.h"
#import "AppContext.h"
2017-09-20 17:48:37 +02:00
#import "NSArray+OWS.h"
#import "OWSBackgroundTask.h"
#import "OWSMessageManager.h"
#import "OWSQueues.h"
#import "OWSSignalServiceProtos.pb.h"
2017-12-19 04:56:02 +01:00
#import "OWSStorage.h"
#import "TSDatabaseView.h"
#import "TSStorageManager.h"
#import "TSYapDatabaseObject.h"
#import "Threading.h"
2017-12-12 16:31:14 +01:00
#import <YapDatabase/YapDatabaseAutoView.h>
#import <YapDatabase/YapDatabaseConnection.h>
#import <YapDatabase/YapDatabaseTransaction.h>
2017-12-12 16:31:14 +01:00
#import <YapDatabase/YapDatabaseViewTypes.h>
NS_ASSUME_NONNULL_BEGIN
#pragma mark - Persisted data model
@class OWSSignalServiceProtosEnvelope;
2017-09-20 17:48:37 +02:00
@interface OWSMessageContentJob : TSYapDatabaseObject
@property (nonatomic, readonly) NSDate *createdAt;
2017-09-27 17:30:23 +02:00
@property (nonatomic, readonly) NSData *envelopeData;
@property (nonatomic, readonly, nullable) NSData *plaintextData;
- (instancetype)initWithEnvelopeData:(NSData *)envelopeData
plaintextData:(NSData *_Nullable)plaintextData NS_DESIGNATED_INITIALIZER;
2017-09-27 17:30:23 +02:00
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
2017-11-15 19:15:48 +01:00
- (instancetype)initWithUniqueId:(NSString *_Nullable)uniqueId NS_UNAVAILABLE;
- (OWSSignalServiceProtosEnvelope *)envelopeProto;
@end
#pragma mark -
2017-09-20 17:48:37 +02:00
@implementation OWSMessageContentJob
+ (NSString *)collection
{
return @"OWSBatchMessageProcessingJob";
}
- (instancetype)initWithEnvelopeData:(NSData *)envelopeData plaintextData:(NSData *_Nullable)plaintextData
{
OWSAssert(envelopeData);
self = [super initWithUniqueId:[NSUUID new].UUIDString];
if (!self) {
return self;
}
_envelopeData = envelopeData;
_plaintextData = plaintextData;
_createdAt = [NSDate new];
return self;
}
2017-09-27 17:30:23 +02:00
- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
return [super initWithCoder:coder];
}
- (OWSSignalServiceProtosEnvelope *)envelopeProto
{
return [OWSSignalServiceProtosEnvelope parseFromData:self.envelopeData];
}
@end
#pragma mark - Finder
NSString *const OWSMessageContentJobFinderExtensionName = @"OWSMessageContentJobFinderExtensionName2";
NSString *const OWSMessageContentJobFinderExtensionGroup = @"OWSMessageContentJobFinderExtensionGroup2";
2017-09-20 17:48:37 +02:00
@interface OWSMessageContentJobFinder : NSObject
@end
#pragma mark -
2017-09-20 17:48:37 +02:00
@interface OWSMessageContentJobFinder ()
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@end
#pragma mark -
2017-09-20 17:48:37 +02:00
@implementation OWSMessageContentJobFinder
- (instancetype)initWithDBConnection:(YapDatabaseConnection *)dbConnection
{
OWSSingletonAssert();
self = [super init];
if (!self) {
return self;
}
_dbConnection = dbConnection;
return self;
}
2017-09-20 17:48:37 +02:00
- (NSArray<OWSMessageContentJob *> *)nextJobsForBatchSize:(NSUInteger)maxBatchSize
{
2017-09-20 17:48:37 +02:00
NSMutableArray<OWSMessageContentJob *> *jobs = [NSMutableArray new];
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
2017-09-20 17:48:37 +02:00
YapDatabaseViewTransaction *viewTransaction = [transaction ext:OWSMessageContentJobFinderExtensionName];
OWSAssert(viewTransaction != nil);
2017-09-20 17:48:37 +02:00
[viewTransaction enumerateKeysAndObjectsInGroup:OWSMessageContentJobFinderExtensionGroup
usingBlock:^(NSString *_Nonnull collection,
NSString *_Nonnull key,
id _Nonnull object,
NSUInteger index,
BOOL *_Nonnull stop) {
OWSMessageContentJob *job = object;
[jobs addObject:job];
if (jobs.count >= maxBatchSize) {
*stop = YES;
}
}];
}];
2017-09-20 17:48:37 +02:00
return [jobs copy];
}
- (void)addJobWithEnvelopeData:(NSData *)envelopeData plaintextData:(NSData *_Nullable)plaintextData
{
// We need to persist the decrypted envelope data ASAP to prevent data loss.
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
OWSMessageContentJob *job =
[[OWSMessageContentJob alloc] initWithEnvelopeData:envelopeData plaintextData:plaintextData];
[job saveWithTransaction:transaction];
}];
}
2017-09-20 17:48:37 +02:00
- (void)removeJobsWithIds:(NSArray<NSString *> *)uniqueIds
{
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
2017-09-20 17:48:37 +02:00
[transaction removeObjectsForKeys:uniqueIds inCollection:[OWSMessageContentJob collection]];
}];
}
+ (YapDatabaseView *)databaseExtension
{
YapDatabaseViewSorting *sorting =
[YapDatabaseViewSorting withObjectBlock:^NSComparisonResult(YapDatabaseReadTransaction *transaction,
NSString *group,
NSString *collection1,
NSString *key1,
id object1,
NSString *collection2,
NSString *key2,
id object2) {
2017-09-20 17:48:37 +02:00
if (![object1 isKindOfClass:[OWSMessageContentJob class]]) {
OWSFail(@"Unexpected object: %@ in collection: %@", [object1 class], collection1);
return NSOrderedSame;
}
2017-09-20 17:48:37 +02:00
OWSMessageContentJob *job1 = (OWSMessageContentJob *)object1;
2017-09-20 17:48:37 +02:00
if (![object2 isKindOfClass:[OWSMessageContentJob class]]) {
OWSFail(@"Unexpected object: %@ in collection: %@", [object2 class], collection2);
return NSOrderedSame;
}
2017-09-20 17:48:37 +02:00
OWSMessageContentJob *job2 = (OWSMessageContentJob *)object2;
return [job1.createdAt compare:job2.createdAt];
}];
YapDatabaseViewGrouping *grouping =
[YapDatabaseViewGrouping withObjectBlock:^NSString *_Nullable(YapDatabaseReadTransaction *_Nonnull transaction,
NSString *_Nonnull collection,
NSString *_Nonnull key,
id _Nonnull object) {
2017-09-20 17:48:37 +02:00
if (![object isKindOfClass:[OWSMessageContentJob class]]) {
OWSFail(@"Unexpected object: %@ in collection: %@", object, collection);
return nil;
}
// Arbitrary string - all in the same group. We're only using the view for sorting.
2017-09-20 17:48:37 +02:00
return OWSMessageContentJobFinderExtensionGroup;
}];
YapDatabaseViewOptions *options = [YapDatabaseViewOptions new];
2017-09-20 17:48:37 +02:00
options.allowedCollections =
[[YapWhitelistBlacklist alloc] initWithWhitelist:[NSSet setWithObject:[OWSMessageContentJob collection]]];
2017-12-12 16:31:14 +01:00
return [[YapDatabaseAutoView alloc] initWithGrouping:grouping sorting:sorting versionTag:@"1" options:options];
}
+ (void)asyncRegisterDatabaseExtension:(OWSStorage *)storage
{
YapDatabaseView *existingView = [storage registeredExtension:OWSMessageContentJobFinderExtensionName];
if (existingView) {
2017-09-20 17:48:37 +02:00
OWSFail(@"%@ was already initialized.", OWSMessageContentJobFinderExtensionName);
// already initialized
return;
}
[storage asyncRegisterExtension:[self databaseExtension] withName:OWSMessageContentJobFinderExtensionName];
}
#pragma mark Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
2017-11-08 20:04:51 +01:00
return self.class.logTag;
}
@end
#pragma mark - Queue Processing
2017-09-20 17:48:37 +02:00
@interface OWSMessageContentQueue : NSObject
@property (nonatomic, readonly) OWSMessageManager *messagesManager;
@property (nonatomic, readonly) YapDatabaseConnection *dbReadWriteConnection;
2017-09-20 17:48:37 +02:00
@property (nonatomic, readonly) OWSMessageContentJobFinder *finder;
@property (nonatomic) BOOL isDrainingQueue;
- (instancetype)initWithMessagesManager:(OWSMessageManager *)messagesManager
storageManager:(TSStorageManager *)storageManager
2017-09-20 17:48:37 +02:00
finder:(OWSMessageContentJobFinder *)finder NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
@end
#pragma mark -
2017-09-20 17:48:37 +02:00
@implementation OWSMessageContentQueue
- (instancetype)initWithMessagesManager:(OWSMessageManager *)messagesManager
storageManager:(TSStorageManager *)storageManager
2017-09-20 17:48:37 +02:00
finder:(OWSMessageContentJobFinder *)finder
{
OWSSingletonAssert();
self = [super init];
if (!self) {
return self;
}
_messagesManager = messagesManager;
_dbReadWriteConnection = [storageManager newDatabaseConnection];
_finder = finder;
_isDrainingQueue = NO;
[[NSNotificationCenter defaultCenter] addObserver:self
2017-12-19 04:56:02 +01:00
selector:@selector(storageIsReady)
name:StorageIsReadyNotification
object:nil];
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
2017-12-19 04:56:02 +01:00
- (void)storageIsReady
{
[self drainQueue];
}
#pragma mark - instance methods
- (dispatch_queue_t)serialQueue
{
static dispatch_queue_t queue = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
queue = dispatch_queue_create("org.whispersystems.message.process", DISPATCH_QUEUE_SERIAL);
});
return queue;
}
- (void)enqueueEnvelopeData:(NSData *)envelopeData plaintextData:(NSData *_Nullable)plaintextData
{
OWSAssert(envelopeData);
// We need to persist the decrypted envelope data ASAP to prevent data loss.
[self.finder addJobWithEnvelopeData:envelopeData plaintextData:plaintextData];
}
- (void)drainQueue
{
// Don't process incoming messages in app extensions.
if (!CurrentAppContext().isMainApp) {
return;
}
dispatch_async(self.serialQueue, ^{
2017-12-19 04:56:02 +01:00
if (![OWSStorage isStorageReady]) {
// We don't want to process incoming messages until storage is ready.
return;
}
if (self.isDrainingQueue) {
return;
}
self.isDrainingQueue = YES;
[self drainQueueWorkStep];
});
}
- (void)drainQueueWorkStep
{
AssertOnDispatchQueue(self.serialQueue);
// We want a value that is just high enough to yield perf benefits.
2017-09-29 20:38:32 +02:00
const NSUInteger kIncomingMessageBatchSize = 32;
2017-09-20 17:48:37 +02:00
NSArray<OWSMessageContentJob *> *jobs = [self.finder nextJobsForBatchSize:kIncomingMessageBatchSize];
OWSAssert(jobs);
if (jobs.count < 1) {
self.isDrainingQueue = NO;
2017-11-08 20:04:51 +01:00
DDLogVerbose(@"%@ Queue is drained", self.logTag);
return;
}
2017-12-15 19:03:03 +01:00
OWSBackgroundTask *backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
[self processJobs:jobs];
[self.finder removeJobsWithIds:jobs.uniqueIds];
backgroundTask = nil;
DDLogVerbose(@"%@ completed %zd jobs. %zd jobs left.",
2017-11-08 20:04:51 +01:00
self.logTag,
jobs.count,
[OWSMessageContentJob numberOfKeysInCollection]);
// Wait a bit in hopes of increasing the batch size.
// This delay won't affect the first message to arrive when this queue is idle,
// so by definition we're receiving more than one message and can benefit from
// batching.
2017-09-29 20:38:32 +02:00
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5f * NSEC_PER_SEC)), self.serialQueue, ^{
[self drainQueueWorkStep];
});
}
- (void)processJobs:(NSArray<OWSMessageContentJob *> *)jobs
{
AssertOnDispatchQueue(self.serialQueue);
[self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
for (OWSMessageContentJob *job in jobs) {
[self.messagesManager processEnvelope:job.envelopeProto
plaintextData:job.plaintextData
transaction:transaction];
}
}];
}
#pragma mark Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
2017-11-08 20:04:51 +01:00
return self.class.logTag;
}
@end
#pragma mark - OWSBatchMessageProcessor
@interface OWSBatchMessageProcessor ()
2017-09-20 17:48:37 +02:00
@property (nonatomic, readonly) OWSMessageContentQueue *processingQueue;
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@end
#pragma mark -
@implementation OWSBatchMessageProcessor
- (instancetype)initWithDBConnection:(YapDatabaseConnection *)dbConnection
messagesManager:(OWSMessageManager *)messagesManager
storageManager:(TSStorageManager *)storageManager
{
OWSSingletonAssert();
self = [super init];
if (!self) {
return self;
}
2017-09-20 17:48:37 +02:00
OWSMessageContentJobFinder *finder = [[OWSMessageContentJobFinder alloc] initWithDBConnection:dbConnection];
OWSMessageContentQueue *processingQueue = [[OWSMessageContentQueue alloc] initWithMessagesManager:messagesManager
storageManager:storageManager
finder:finder];
_processingQueue = processingQueue;
return self;
}
- (instancetype)initDefault
{
// For concurrency coherency we use the same dbConnection to persist and read the unprocessed envelopes
YapDatabaseConnection *dbConnection = [[TSStorageManager sharedManager] newDatabaseConnection];
OWSMessageManager *messagesManager = [OWSMessageManager sharedManager];
TSStorageManager *storageManager = [TSStorageManager sharedManager];
return [self initWithDBConnection:dbConnection messagesManager:messagesManager storageManager:storageManager];
}
+ (instancetype)sharedInstance
{
static OWSBatchMessageProcessor *sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] initDefault];
});
return sharedInstance;
}
#pragma mark - class methods
+ (void)asyncRegisterDatabaseExtension:(OWSStorage *)storage
{
[OWSMessageContentJobFinder asyncRegisterDatabaseExtension:storage];
}
#pragma mark - instance methods
- (void)handleAnyUnprocessedEnvelopesAsync
{
[self.processingQueue drainQueue];
}
- (void)enqueueEnvelopeData:(NSData *)envelopeData plaintextData:(NSData *_Nullable)plaintextData
{
OWSAssert(envelopeData);
// We need to persist the decrypted envelope data ASAP to prevent data loss.
[self.processingQueue enqueueEnvelopeData:envelopeData plaintextData:plaintextData];
[self.processingQueue drainQueue];
}
@end
NS_ASSUME_NONNULL_END