Merge branch 'charlesmchen/orphanCleanup'

pull/1/head
Matthew Chen 8 years ago
commit 640ec13b2e

@ -21,7 +21,8 @@ post_install do |installer|
existing_definitions = "$(inheritied)"
end
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = "#{existing_definitions} SSK_BUILDING_FOR_TESTS=1"
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = "#{existing_definitions} SSK_BUILDING_FOR_TESTS=1"
config.build_settings['OTHER_SWIFT_FLAGS'] = ['$(inherited)', '-DSSK_BUILDING_FOR_TESTS']
end
end
end

@ -111,7 +111,7 @@ EXTERNAL SOURCES:
AxolotlKit:
:git: https://github.com/WhisperSystems/SignalProtocolKit.git
SignalServiceKit:
:path: "../../SignalServiceKit.podspec"
:path: ../../SignalServiceKit.podspec
SocketRocket:
:git: https://github.com/facebook/SocketRocket.git
@ -140,6 +140,6 @@ SPEC CHECKSUMS:
UnionFind: c33be5adb12983981d6e827ea94fc7f9e370f52d
YapDatabase: cd911121580ff16675f65ad742a9eb0ab4d9e266
PODFILE CHECKSUM: 124c05542083fefccb75f4b7afbdd839e27ff5ab
PODFILE CHECKSUM: a0f4507b6b4e6f9da3250901b06187a67236e083
COCOAPODS: 1.2.1

@ -28,7 +28,7 @@
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1D0826C97B09E58C83B3C228FBC535FA"
BlueprintIdentifier = "FF3F51F81980908EDE1836B76AA3A1EC"
BuildableName = "libSignalServiceKit.a"
BlueprintName = "SignalServiceKit"
ReferencedContainer = "container:Pods/Pods.xcodeproj">

@ -46,7 +46,6 @@ NS_ASSUME_NONNULL_BEGIN
+ (void)deleteAttachments;
+ (NSString *)attachmentsFolder;
+ (NSUInteger)numberOfItemsInAttachmentsFolder;
- (CGSize)imageSizeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
- (CGSize)imageSizeWithoutTransaction;

@ -189,19 +189,6 @@ NS_ASSUME_NONNULL_BEGIN
return attachmentsFolder;
}
+ (NSUInteger)numberOfItemsInAttachmentsFolder
{
NSError *error;
NSUInteger count =
[[[NSFileManager defaultManager] contentsOfDirectoryAtPath:[self attachmentsFolder] error:&error] count];
if (error) {
DDLogError(@"Unable to count attachments in attachments folder. Error: %@", error);
}
return count;
}
- (nullable NSString *)filePath
{
if (!self.localRelativeFilePath) {

@ -30,7 +30,7 @@ extern NSString *const TSNetworkManagerDomain;
- (instancetype)init NS_UNAVAILABLE;
+ (id)sharedManager;
+ (instancetype)sharedManager;
- (void)makeRequest:(TSRequest *)request
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success

@ -22,7 +22,8 @@ typedef void (^failureBlock)(NSURLSessionDataTask *task, NSError *error);
#pragma mark Singleton implementation
+ (id)sharedManager {
+ (instancetype)sharedManager
{
static TSNetworkManager *sharedMyManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

@ -1,10 +1,30 @@
// Copyright (c) 2016 Open Whisper Systems. All rights reserved.
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
// Notes:
//
// * On disk, we only bother cleaning up files, not directories.
// * For code simplicity, we don't guarantee that everything is
// cleaned up in a single pass. If an interaction is cleaned up,
// it's attachments might not be cleaned up until the next pass.
// If an attachment is cleaned up, it's file on disk might not
// be cleaned up until the next pass.
@interface OWSOrphanedDataCleaner : NSObject
/**
* Remove any inaccessible data left behind due to application bugs.
*/
- (void)removeOrphanedData;
- (instancetype)init NS_UNAVAILABLE;
+ (void)auditAsync;
// completion, if present, will be invoked on the main thread.
+ (void)auditAndCleanupAsync:(void (^_Nullable)())completion;
+ (NSSet<NSString *> *)filePathsInAttachmentsFolder;
+ (long long)fileSizeOfFilePaths:(NSArray<NSString *> *)filePaths;
@end
NS_ASSUME_NONNULL_END

@ -9,84 +9,256 @@
#import "TSStorageManager.h"
#import "TSThread.h"
NS_ASSUME_NONNULL_BEGIN
#ifdef SSK_BUILDING_FOR_TESTS
#define CleanupLogDebug NSLog
#define CleanupLogInfo NSLog
#else
#define CleanupLogDebug DDLogDebug
#define CleanupLogInfo DDLogInfo
#endif
@implementation OWSOrphanedDataCleaner
- (void)removeOrphanedData
+ (void)auditAsync
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[OWSOrphanedDataCleaner auditAndCleanup:NO completion:nil];
});
}
+ (void)auditAndCleanupAsync:(void (^_Nullable)())completion
{
// Remove interactions whose threads have been deleted
for (NSString *interactionId in [self orphanedInteractionIds]) {
DDLogWarn(@"Removing orphaned interaction with id: %@", interactionId);
TSInteraction *interaction = [TSInteraction fetchObjectWithUniqueID:interactionId];
[interaction remove];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[OWSOrphanedDataCleaner auditAndCleanup:YES completion:completion];
});
}
// This method finds and optionally cleans up:
//
// * Orphan messages (with no thread).
// * Orphan attachments (with no message).
// * Orphan attachment files (with no attachment).
// * Missing attachment files (cannot be cleaned up).
// These are attachments which have no file on disk. They should be extremely rare -
// the only cases I have seen are probably due to debugging.
// They can't be cleaned up - we don't want to delete the TSAttachmentStream or
// its corresponding message. Better that the broken message shows up in the
// conversation view.
+ (void)auditAndCleanup:(BOOL)shouldCleanup completion:(void (^_Nullable)())completion
{
NSSet<NSString *> *diskFilePaths = [self filePathsInAttachmentsFolder];
long long totalFileSize = [self fileSizeOfFilePaths:diskFilePaths.allObjects];
NSUInteger fileCount = diskFilePaths.count;
TSStorageManager *storageManager = [TSStorageManager sharedManager];
YapDatabaseConnection *databaseConnection = storageManager.newDatabaseConnection;
__block int attachmentStreamCount = 0;
NSMutableSet<NSString *> *attachmentFilePaths = [NSMutableSet new];
NSMutableSet<NSString *> *attachmentIds = [NSMutableSet new];
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
[transaction enumerateKeysAndObjectsInCollection:TSAttachmentStream.collection
usingBlock:^(NSString *key, TSAttachment *attachment, BOOL *stop) {
[attachmentIds addObject:attachment.uniqueId];
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
return;
}
TSAttachmentStream *attachmentStream
= (TSAttachmentStream *)attachment;
attachmentStreamCount++;
NSString *_Nullable filePath = [attachmentStream filePath];
OWSAssert(filePath);
[attachmentFilePaths addObject:filePath];
}];
}];
CleanupLogDebug(@"fileCount: %zd", fileCount);
CleanupLogDebug(@"totalFileSize: %lld", totalFileSize);
CleanupLogDebug(@"attachmentStreams: %d", attachmentStreamCount);
CleanupLogDebug(@"attachmentStreams with file paths: %zd", attachmentFilePaths.count);
NSMutableSet<NSString *> *orphanDiskFilePaths = [diskFilePaths mutableCopy];
[orphanDiskFilePaths minusSet:attachmentFilePaths];
NSMutableSet<NSString *> *missingAttachmentFilePaths = [attachmentFilePaths mutableCopy];
[missingAttachmentFilePaths minusSet:diskFilePaths];
CleanupLogDebug(@"orphan disk file paths: %zd", orphanDiskFilePaths.count);
CleanupLogDebug(@"missing attachment file paths: %zd", missingAttachmentFilePaths.count);
[self printPaths:orphanDiskFilePaths.allObjects label:@"orphan disk file paths"];
[self printPaths:missingAttachmentFilePaths.allObjects label:@"missing attachment file paths"];
NSMutableSet *threadIds = [NSMutableSet new];
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
[transaction enumerateKeysInCollection:TSThread.collection
usingBlock:^(NSString *_Nonnull key, BOOL *_Nonnull stop) {
[threadIds addObject:key];
}];
}];
NSMutableSet<NSString *> *orphanInteractionIds = [NSMutableSet new];
NSMutableSet<NSString *> *messageAttachmentIds = [NSMutableSet new];
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
[transaction enumerateKeysAndObjectsInCollection:TSMessage.collection
usingBlock:^(NSString *key, TSInteraction *interaction, BOOL *stop) {
if (![threadIds containsObject:interaction.uniqueThreadId]) {
[orphanInteractionIds addObject:interaction.uniqueId];
}
if (![interaction isKindOfClass:[TSMessage class]]) {
return;
}
TSMessage *message = (TSMessage *)interaction;
if (message.attachmentIds.count > 0) {
[messageAttachmentIds addObjectsFromArray:message.attachmentIds];
}
}];
}];
CleanupLogDebug(@"attachmentIds: %zd", attachmentIds.count);
CleanupLogDebug(@"messageAttachmentIds: %zd", messageAttachmentIds.count);
NSMutableSet<NSString *> *orphanAttachmentIds = [attachmentIds mutableCopy];
[orphanAttachmentIds minusSet:messageAttachmentIds];
NSMutableSet<NSString *> *missingAttachmentIds = [messageAttachmentIds mutableCopy];
[missingAttachmentIds minusSet:attachmentIds];
CleanupLogDebug(@"orphan attachmentIds: %zd", orphanAttachmentIds.count);
CleanupLogDebug(@"missing attachmentIds: %zd", missingAttachmentIds.count);
CleanupLogDebug(@"orphan interactions: %zd", orphanInteractionIds.count);
// We need to avoid cleaning up new attachments and files that are still in the process of
// being created/written, so we don't clean up anything recent.
#ifdef SSK_BUILDING_FOR_TESTS
const NSTimeInterval kMinimumOrphanAge = 0.f;
#else
const NSTimeInterval kMinimumOrphanAge = 15 * 60.f;
#endif
if (!shouldCleanup) {
return;
}
// Remove any lingering attachments
for (NSString *path in [self orphanedFilePaths]) {
DDLogWarn(@"Removing orphaned file attachment at path: %@", path);
[databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
for (NSString *interactionId in orphanInteractionIds) {
TSInteraction *interaction = [TSInteraction fetchObjectWithUniqueID:interactionId];
if (!interaction) {
// This could just be a race condition, but it should be very unlikely.
OWSFail(@"Could not load interaction: %@", interactionId);
continue;
}
CleanupLogInfo(@"Removing orphan message: %@", interaction.uniqueId);
[interaction removeWithTransaction:transaction];
}
for (NSString *attachmentId in orphanAttachmentIds) {
TSAttachment *attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId];
if (!attachment) {
// This could just be a race condition, but it should be very unlikely.
OWSFail(@"Could not load attachment: %@", attachmentId);
continue;
}
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
continue;
}
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
// Don't delete attachments which were created in the last N minutes.
if (fabs([attachmentStream.creationTimestamp timeIntervalSinceNow]) < kMinimumOrphanAge) {
CleanupLogInfo(@"Skipping orphan attachment due to age: %f",
fabs([attachmentStream.creationTimestamp timeIntervalSinceNow]));
continue;
}
CleanupLogInfo(@"Removing orphan attachment: %@", attachmentStream.uniqueId);
[attachmentStream removeWithTransaction:transaction];
}
}];
for (NSString *filePath in orphanDiskFilePaths) {
NSError *error;
[[NSFileManager defaultManager] removeItemAtPath:path error:&error];
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error];
if (!attributes || error) {
OWSFail(@"Could not get attributes of file at: %@", filePath);
continue;
}
// Don't delete files which were created in the last N minutes.
if (fabs([attributes.fileModificationDate timeIntervalSinceNow]) < kMinimumOrphanAge) {
CleanupLogInfo(@"Skipping orphan attachment file due to age: %f",
fabs([attributes.fileModificationDate timeIntervalSinceNow]));
continue;
}
CleanupLogInfo(@"Removing orphan attachment file: %@", filePath);
[[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
if (error) {
DDLogError(@"Unable to remove orphaned file attachment at path:%@", path);
OWSFail(@"Could not remove orphan file at: %@", filePath);
}
}
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
}
- (NSArray<NSString *> *)orphanedInteractionIds
+ (void)printPaths:(NSArray<NSString *> *)paths label:(NSString *)label
{
NSMutableArray *interactionIds = [NSMutableArray new];
[[TSInteraction dbReadConnection] readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
[TSInteraction enumerateCollectionObjectsWithTransaction:transaction
usingBlock:^(TSInteraction *interaction, BOOL *stop) {
TSThread *thread = [TSThread
fetchObjectWithUniqueID:interaction.uniqueThreadId
transaction:transaction];
if (!thread) {
[interactionIds addObject:interaction.uniqueId];
}
}];
}];
for (NSString *path in [paths sortedArrayUsingSelector:@selector(compare:)]) {
CleanupLogDebug(@"%@: %@", label, path);
}
}
+ (NSSet<NSString *> *)filePathsInAttachmentsFolder
{
NSString *attachmentsFolder = [TSAttachmentStream attachmentsFolder];
CleanupLogDebug(@"attachmentsFolder: %@", attachmentsFolder);
return [interactionIds copy];
return [self filePathsInDirectory:attachmentsFolder];
}
- (NSArray<NSString *> *)orphanedFilePaths
+ (NSSet<NSString *> *)filePathsInDirectory:(NSString *)dirPath
{
NSMutableSet *filePaths = [NSMutableSet new];
NSError *error;
NSMutableArray<NSString *> *filenames =
[[[NSFileManager defaultManager] contentsOfDirectoryAtPath:[TSAttachmentStream attachmentsFolder] error:&error]
mutableCopy];
NSArray<NSString *> *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:&error];
if (error) {
DDLogError(@"error getting orphanedFilePaths:%@", error);
return @[];
OWSFail(@"contentsOfDirectoryAtPath error: %@", error);
return [NSSet new];
}
NSMutableDictionary<NSString *, NSString *> *attachmentIdFilenames = [NSMutableDictionary new];
for (NSString *filename in filenames) {
// Remove extension from (e.g.) 1234.png to get the attachmentId "1234"
NSString *attachmentId = [filename stringByDeletingPathExtension];
attachmentIdFilenames[attachmentId] = filename;
}
[TSInteraction enumerateCollectionObjectsUsingBlock:^(TSInteraction *interaction, BOOL *stop) {
if ([interaction isKindOfClass:[TSMessage class]]) {
TSMessage *message = (TSMessage *)interaction;
if ([message hasAttachments]) {
for (NSString *attachmentId in message.attachmentIds) {
[attachmentIdFilenames removeObjectForKey:attachmentId];
}
}
for (NSString *fileName in fileNames) {
NSString *filePath = [dirPath stringByAppendingPathComponent:fileName];
BOOL isDirectory;
[[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory];
if (isDirectory) {
[filePaths addObjectsFromArray:[self filePathsInDirectory:filePath].allObjects];
} else {
[filePaths addObject:filePath];
}
}];
}
return filePaths;
}
NSArray<NSString *> *filenamesToDelete = [attachmentIdFilenames allValues];
NSMutableArray<NSString *> *absolutePathsToDelete = [NSMutableArray arrayWithCapacity:[filenamesToDelete count]];
for (NSString *filename in filenamesToDelete) {
NSString *absolutePath = [[TSAttachmentStream attachmentsFolder] stringByAppendingFormat:@"/%@", filename];
[absolutePathsToDelete addObject:absolutePath];
+ (long long)fileSizeOfFilePath:(NSString *)filePath
{
NSError *error;
NSNumber *fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error][NSFileSize];
if (error) {
OWSFail(@"attributesOfItemAtPath: %@ error: %@", filePath, error);
return 0;
}
return fileSize.longLongValue;
}
return [absolutePathsToDelete copy];
+ (long long)fileSizeOfFilePaths:(NSArray<NSString *> *)filePaths
{
long long result = 0;
for (NSString *filePath in filePaths) {
result += [self fileSizeOfFilePath:filePath];
}
return result;
}
@end
NS_ASSUME_NONNULL_END

@ -14,6 +14,8 @@
@end
#pragma mark -
@implementation OWSOrphanedDataCleanerTest
- (void)setUp
@ -24,7 +26,7 @@
// Set up initial conditions & Sanity check
[TSAttachmentStream deleteAttachments];
XCTAssertEqual(0, [TSAttachmentStream numberOfItemsInAttachmentsFolder]);
XCTAssertEqual(0, [self numberOfItemsInAttachmentsFolder]);
[TSAttachmentStream removeAllObjectsInCollection];
XCTAssertEqual(0, [TSAttachmentStream numberOfKeysInCollection]);
[TSIncomingMessage removeAllObjectsInCollection];
@ -38,6 +40,11 @@
[super tearDown];
}
- (NSUInteger)numberOfItemsInAttachmentsFolder
{
return [OWSOrphanedDataCleaner filePathsInAttachmentsFolder].count;
}
- (void)testInteractionsWithoutThreadAreDeleted
{
// This thread is intentionally not saved. It's meant to recreate a situation we've seen where interactions exist
@ -53,7 +60,17 @@
[incomingMessage save];
XCTAssertEqual(1, [TSIncomingMessage numberOfKeysInCollection]);
[[OWSOrphanedDataCleaner new] removeOrphanedData];
XCTestExpectation *expectation = [self expectationWithDescription:@"Cleanup"];
[OWSOrphanedDataCleaner auditAndCleanupAsync:^{
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:5.0
handler:^(NSError *error) {
if (error) {
XCTFail(@"Expectation Failed with error: %@", error);
}
}];
XCTAssertEqual(0, [TSIncomingMessage numberOfKeysInCollection]);
}
@ -70,14 +87,24 @@
[incomingMessage save];
XCTAssertEqual(1, [TSIncomingMessage numberOfKeysInCollection]);
[[OWSOrphanedDataCleaner new] removeOrphanedData];
XCTestExpectation *expectation = [self expectationWithDescription:@"Cleanup"];
[OWSOrphanedDataCleaner auditAndCleanupAsync:^{
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:5.0
handler:^(NSError *error) {
if (error) {
XCTFail(@"Expectation Failed with error: %@", error);
}
}];
XCTAssertEqual(1, [TSIncomingMessage numberOfKeysInCollection]);
}
- (void)testFilesWithoutInteractionsAreDeleted
{
// sanity check
XCTAssertEqual(0, [TSAttachmentStream numberOfItemsInAttachmentsFolder]);
XCTAssertEqual(0, [self numberOfItemsInAttachmentsFolder]);
NSError *error;
TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithContentType:@"image/jpeg" sourceFilename:nil];
@ -86,12 +113,25 @@
NSString *orphanedFilePath = [attachmentStream filePath];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:orphanedFilePath];
XCTAssert(fileExists);
XCTAssertEqual(1, [TSAttachmentStream numberOfItemsInAttachmentsFolder]);
XCTAssertEqual(1, [self numberOfItemsInAttachmentsFolder]);
// Do multiple cleanup passes.
for (int i = 0; i < 2; i++) {
XCTestExpectation *expectation = [self expectationWithDescription:@"Cleanup"];
[OWSOrphanedDataCleaner auditAndCleanupAsync:^{
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:5.0
handler:^(NSError *error) {
if (error) {
XCTFail(@"Expectation Failed with error: %@", error);
}
}];
}
[[OWSOrphanedDataCleaner new] removeOrphanedData];
fileExists = [[NSFileManager defaultManager] fileExistsAtPath:orphanedFilePath];
XCTAssertFalse(fileExists);
XCTAssertEqual(0, [TSAttachmentStream numberOfItemsInAttachmentsFolder]);
XCTAssertEqual(0, [self numberOfItemsInAttachmentsFolder]);
}
- (void)testFilesWithInteractionsAreNotDeleted
@ -116,13 +156,22 @@
NSString *attachmentFilePath = [attachmentStream filePath];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:attachmentFilePath];
XCTAssert(fileExists);
XCTAssertEqual(1, [TSAttachmentStream numberOfItemsInAttachmentsFolder]);
[[OWSOrphanedDataCleaner new] removeOrphanedData];
XCTAssertEqual(1, [self numberOfItemsInAttachmentsFolder]);
XCTestExpectation *expectation = [self expectationWithDescription:@"Cleanup"];
[OWSOrphanedDataCleaner auditAndCleanupAsync:^{
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:5.0
handler:^(NSError *error) {
if (error) {
XCTFail(@"Expectation Failed with error: %@", error);
}
}];
fileExists = [[NSFileManager defaultManager] fileExistsAtPath:attachmentFilePath];
XCTAssert(fileExists);
XCTAssertEqual(1, [TSAttachmentStream numberOfItemsInAttachmentsFolder]);
XCTAssertEqual(1, [self numberOfItemsInAttachmentsFolder]);
}
- (void)testFilesWithoutAttachmentStreamsAreDeleted
@ -135,12 +184,22 @@
NSString *orphanedFilePath = [attachmentStream filePath];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:orphanedFilePath];
XCTAssert(fileExists);
XCTAssertEqual(1, [TSAttachmentStream numberOfItemsInAttachmentsFolder]);
XCTAssertEqual(1, [self numberOfItemsInAttachmentsFolder]);
XCTestExpectation *expectation = [self expectationWithDescription:@"Cleanup"];
[OWSOrphanedDataCleaner auditAndCleanupAsync:^{
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:5.0
handler:^(NSError *error) {
if (error) {
XCTFail(@"Expectation Failed with error: %@", error);
}
}];
[[OWSOrphanedDataCleaner new] removeOrphanedData];
fileExists = [[NSFileManager defaultManager] fileExistsAtPath:orphanedFilePath];
XCTAssertFalse(fileExists);
XCTAssertEqual(0, [TSAttachmentStream numberOfItemsInAttachmentsFolder]);
XCTAssertEqual(0, [self numberOfItemsInAttachmentsFolder]);
}
@end

Loading…
Cancel
Save