<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import Foundation
import Shared
import Storage

private let log = Logger.syncLogger
let BookmarksStorageVersion = 2

/**
 * This is like a synchronizer, but it downloads records bit by bit, eventually
 * notifying that the local storage is up to date with the server contents.
 *
 * Because batches might be separated over time, it's possible for the server
 * state to change between calls. These state changes might include:
 *
 * 1. New changes arriving. This is fairly routine, but it's worth noting that
 *    changes might affect existing records that have been batched!
 * 2. Wipes. The collection (or the server as a whole) might be deleted. This
 *    should be accompanied by a change in syncID in meta/global; it's the caller's
 *    responsibility to detect this.
 * 3. A storage format change. This should be unfathomably rare, but if it happens
 *    we must also be prepared to discard our existing batched data.
 * 4. TTL expiry. We need to do better about TTL handling in general, but here
 *    we might find that a downloaded record is no longer live by the time we
 *    come to apply it! This doesn't apply to bookmark records, so we will ignore
 *    it for the moment.
 *
 * Batch downloading without continuation tokens is achieved as follows:
 *
 * * A minimum timestamp is established. This starts as zero.
 * * A fetch is issued against the server for records changed since that timestamp,
 *   ordered by modified time ascending, and limited to the batch size.
 * * If the batch is complete, we flush it to storage and advance the minimum
 *   timestamp to just before the newest record in the batch. This ensures that
 *   a divided set of records with the same modified times will be downloaded
 *   entirely so long as the set is never larger than the batch size.
 * * Iterate until we determine that there are no new records to fetch.
 *
 * Batch downloading with continuation tokens is much easier:
 *
 * * A minimum timestamp is established.
 * * Make a request with limit=N.
 * * Look for an X-Weave-Next-Offset header. Supply that in the next request.
 *   Also supply X-If-Unmodified-Since to avoid missed modifications.
 *
 * We do the latter, because we only support Sync 1.5. The use of the offset
 * allows us to efficiently process batches, particularly those that contain
 * large sets of records with the same timestamp. We still maintain the last
 * modified timestamp to allow for resuming a batch in the case of a conflicting
 * write, detected via X-I-U-S.
 */

public class BookmarksMirrorer {
    private let downloader: BatchingDownloader<BookmarkBasePayload>
    private let storage: BookmarkBufferStorage
    private let batchSize: Int

    public init(storage: BookmarkBufferStorage, client: Sync15CollectionClient<BookmarkBasePayload>, basePrefs: Prefs, collection: String, batchSize: Int=100) {
        self.storage = storage
        self.downloader = BatchingDownloader(collectionClient: client, basePrefs: basePrefs, collection: collection)
        self.batchSize = batchSize
    }

    // TODO
    public func storageFormatDidChange() {
    }

    // TODO
    public func onWipeWasAppliedToStorage() {
    }

    private func applyRecordsFromBatcher() -> Success {
        let retrieved = self.downloader.retrieve()
        let records = retrieved.flatMap { ($0.payload as MirrorItemable).toMirrorItem($0.modified) }
        if records.isEmpty {
            log.debug("Got empty batch.")
            return succeed()
        }

        log.debug("Applying \(records.count) downloaded bookmarks.")
        return self.storage.applyRecords(records)
    }

    public func go(info: InfoCollections, greenLight: () -> Bool) -> SyncResult {
        if !greenLight() {
            log.info("Green light turned red. Stopping mirror operation.")
            return deferMaybe(SyncStatus.NotStarted(.RedLight))
        }

        log.debug("Downloading up to \(self.batchSize) records.")
        return self.downloader.go(info, limit: self.batchSize)
                              .bind { result in
            guard let end = result.successValue else {
                log.warning("Got failure: \(result.failureValue!)")
                return deferMaybe(result.failureValue!)
            }
            switch end {
            case .Complete:
                log.info("Done with batched mirroring.")
                return self.applyRecordsFromBatcher()
                   >>> effect(self.downloader.advance)
                   >>> self.storage.doneApplyingRecordsAfterDownload
                   >>> always(SyncStatus.Completed)
            case .Incomplete:
                log.debug("Running another batch.")
                // This recursion is fine because Deferred always pushes callbacks onto a queue.
                return self.applyRecordsFromBatcher()
                   >>> effect(self.downloader.advance)
                   >>> { self.go(info, greenLight: greenLight) }
            case .Interrupted:
                log.info("Interrupted. Aborting batching this time.")<commit_msg>Bug 1259201 - Part 2: be explicit about not storing malformed bookmarks, and why that's OK.<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import Foundation
import Shared
import Storage

private let log = Logger.syncLogger
let BookmarksStorageVersion = 2

/**
 * This is like a synchronizer, but it downloads records bit by bit, eventually
 * notifying that the local storage is up to date with the server contents.
 *
 * Because batches might be separated over time, it's possible for the server
 * state to change between calls. These state changes might include:
 *
 * 1. New changes arriving. This is fairly routine, but it's worth noting that
 *    changes might affect existing records that have been batched!
 * 2. Wipes. The collection (or the server as a whole) might be deleted. This
 *    should be accompanied by a change in syncID in meta/global; it's the caller's
 *    responsibility to detect this.
 * 3. A storage format change. This should be unfathomably rare, but if it happens
 *    we must also be prepared to discard our existing batched data.
 * 4. TTL expiry. We need to do better about TTL handling in general, but here
 *    we might find that a downloaded record is no longer live by the time we
 *    come to apply it! This doesn't apply to bookmark records, so we will ignore
 *    it for the moment.
 *
 * Batch downloading without continuation tokens is achieved as follows:
 *
 * * A minimum timestamp is established. This starts as zero.
 * * A fetch is issued against the server for records changed since that timestamp,
 *   ordered by modified time ascending, and limited to the batch size.
 * * If the batch is complete, we flush it to storage and advance the minimum
 *   timestamp to just before the newest record in the batch. This ensures that
 *   a divided set of records with the same modified times will be downloaded
 *   entirely so long as the set is never larger than the batch size.
 * * Iterate until we determine that there are no new records to fetch.
 *
 * Batch downloading with continuation tokens is much easier:
 *
 * * A minimum timestamp is established.
 * * Make a request with limit=N.
 * * Look for an X-Weave-Next-Offset header. Supply that in the next request.
 *   Also supply X-If-Unmodified-Since to avoid missed modifications.
 *
 * We do the latter, because we only support Sync 1.5. The use of the offset
 * allows us to efficiently process batches, particularly those that contain
 * large sets of records with the same timestamp. We still maintain the last
 * modified timestamp to allow for resuming a batch in the case of a conflicting
 * write, detected via X-I-U-S.
 */

public class BookmarksMirrorer {
    private let downloader: BatchingDownloader<BookmarkBasePayload>
    private let storage: BookmarkBufferStorage
    private let batchSize: Int

    public init(storage: BookmarkBufferStorage, client: Sync15CollectionClient<BookmarkBasePayload>, basePrefs: Prefs, collection: String, batchSize: Int=100) {
        self.storage = storage
        self.downloader = BatchingDownloader(collectionClient: client, basePrefs: basePrefs, collection: collection)
        self.batchSize = batchSize
    }

    // TODO
    public func storageFormatDidChange() {
    }

    // TODO
    public func onWipeWasAppliedToStorage() {
    }

    private func applyRecordsFromBatcher() -> Success {
        let retrieved = self.downloader.retrieve()
        let invalid = retrieved.filter { !$0.payload.isValid() }

        if !invalid.isEmpty {
            // There's nothing we can do with invalid input. There's also no point in
            // tracking failing GUIDs here yet: if another client reuploads those records
            // correctly, we'll encounter them routinely due to a newer timestamp.
            // The only improvement we could make is to drop records from the buffer if we
            // happen to see a new, invalid one before somehow syncing again, but that's
            // unlikely enough that it's not worth doing.
            //
            // Bug 1258801 tracks recording telemetry for these invalid items, which is
            // why we don't simply drop them on the ground at the download stage.
            //
            // We might also choose to perform certain simple recovery actions here: for example,
            // bookmarks with null URIs are clearly invalid, and could be treated as if they
            // weren't present on the server, or transparently deleted.
            log.warning("Invalid records: \(invalid.map { $0.id }.joinWithSeparator(", ")).")
        }

        let mirrorItems = retrieved.flatMap { record -> BookmarkMirrorItem? in
            guard record.payload.isValid() else {
                return nil
            }

            return (record.payload as MirrorItemable).toMirrorItem(record.modified)
        }

        if mirrorItems.isEmpty {
            log.debug("Got empty batch.")
            return succeed()
        }

        log.debug("Applying \(mirrorItems.count) downloaded bookmarks.")
        return self.storage.applyRecords(mirrorItems)
    }

    public func go(info: InfoCollections, greenLight: () -> Bool) -> SyncResult {
        if !greenLight() {
            log.info("Green light turned red. Stopping mirror operation.")
            return deferMaybe(SyncStatus.NotStarted(.RedLight))
        }

        log.debug("Downloading up to \(self.batchSize) records.")
        return self.downloader.go(info, limit: self.batchSize)
                              .bind { result in
            guard let end = result.successValue else {
                log.warning("Got failure: \(result.failureValue!)")
                return deferMaybe(result.failureValue!)
            }
            switch end {
            case .Complete:
                log.info("Done with batched mirroring.")
                return self.applyRecordsFromBatcher()
                   >>> effect(self.downloader.advance)
                   >>> self.storage.doneApplyingRecordsAfterDownload
                   >>> always(SyncStatus.Completed)
            case .Incomplete:
                log.debug("Running another batch.")
                // This recursion is fine because Deferred always pushes callbacks onto a queue.
                return self.applyRecordsFromBatcher()
                   >>> effect(self.downloader.advance)
                   >>> { self.go(info, greenLight: greenLight) }
            case .Interrupted:
                log.info("Interrupted. Aborting batching this time.")
{'fraction_non_alphanumeric': 0.071285140562249, 'fraction_numerical': 0.00426706827309237, 'mean_word_length': 2.6144541880858783, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 44, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 33, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4507293', 'n_tokens_mistral': 3047, 'n_tokens_neox': 2885, 'n_words': 1311}