// // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // import Foundation public class OWS110SortIdMigration: OWSDatabaseMigration { // increment a similar constant for each migration. @objc class func migrationId() -> String { // append char "x" because we want to rerun on some internal devices which // have already run this migration. return "110x" } override public func runUp(completion: @escaping OWSDatabaseMigrationCompletion) { Logger.debug("") BenchAsync(title: "Sort Migration") { completeBenchmark in self.doMigration { completeBenchmark() completion() } } } private func doMigration(completion: @escaping OWSDatabaseMigrationCompletion) { // TODO batch this? self.dbReadWriteConnection().readWrite { transaction in var archivedThreads: [TSThread] = [] // get archived threads before migration TSThread.enumerateCollectionObjects(with: transaction) { (object, _) in guard let thread = object as? TSThread else { owsFailDebug("unexpected object: \(type(of: object))") return } if thread.isArchivedByLegacyTimestampForSorting { archivedThreads.append(thread) } } guard let legacySorting: YapDatabaseAutoViewTransaction = transaction.extension(TSMessageDatabaseViewExtensionName_Legacy) as? YapDatabaseAutoViewTransaction else { owsFailDebug("legacySorting was unexpectedly nil") return } let totalCount: UInt = legacySorting.numberOfItemsInAllGroups() var completedCount: UInt = 0 var allGroups = [String]() legacySorting.enumerateGroups { group, _ in allGroups.append(group) } var seenGroups: Set = Set() for group in allGroups { autoreleasepool { // Sanity Check #1 // Make sure our enumeration is monotonically increasing. // Note: sortIds increase monotonically WRT timestampForLegacySorting, but only WRT the interaction's thread. // // e.g. When we migrate the **next** thread, we start with that thread's oldest interaction. So it's possible and expected // that thread2's oldest interaction will have a smaller timestampForLegacySorting, but a greater sortId then an interaction // in thread1. That's OK because we only sort messages with respect to the thread they belong in. We don't have any sort of // "global sort" of messages across all threads. var previousTimestampForLegacySorting: UInt64 = 0 // Sanity Check #2 // Ensure we only process a DB View's group (i.e. threadId) once. guard !seenGroups.contains(group) else { owsFail("unexpectedly seeing a repeated group: \(group)") } seenGroups.insert(group) var groupKeys = [String]() legacySorting.enumerateKeys(inGroup: group, using: { (_, key, _, _) in groupKeys.append(key) }) let groupKeyBatchSize: Int = 1024 for batch in groupKeys.chunked(by: groupKeyBatchSize) { autoreleasepool { for uniqueId in batch { guard let interaction = TSInteraction.fetch(uniqueId: uniqueId, transaction: transaction) else { owsFailDebug("Could not load interaction: \(uniqueId)") return } if interaction.timestampForLegacySorting() < previousTimestampForLegacySorting { owsFailDebug("unexpected object ordering previousTimestampForLegacySorting: \(previousTimestampForLegacySorting) interaction.timestampForLegacySorting: \(interaction.timestampForLegacySorting())") } previousTimestampForLegacySorting = interaction.timestampForLegacySorting() interaction.saveNextSortId(transaction: transaction) completedCount += 1 if completedCount % 100 == 0 { // Legit usage of legacy sorting for migration to new sorting Logger.info("thread: \(interaction.uniqueThreadId), timestampForLegacySorting:\(interaction.timestampForLegacySorting()), sortId: \(interaction.sortId) totalCount: \(totalCount), completedcount: \(completedCount)") } } } } } } Logger.info("re-archiving \(archivedThreads.count) threads which were previously archived") for archivedThread in archivedThreads { archivedThread.archiveThread(with: transaction) } self.save(with: transaction) } completion() } }