fix: wrong sync status
This commit is contained in:
1 parent
5e75c9dfcd
commit
f4a7e2c30f
3 files changed
+99
-42
No files matched your search
@@ -173,6 +173,53 @@ struct HistorySyncTests {
|
||||
#expect(fixture.record(fixture.event.key) != nil)
|
||||
}
|
||||
|
||||
@Test func sourceQuarantineUpdatesAnAlreadySuccessfulStatusWithoutNewUploads() async throws {
|
||||
let fixture = try HistoryFixture()
|
||||
defer { fixture.stop() }
|
||||
try await fixture.start()
|
||||
try fixture.persistence.journal.markUploaded(fixture.event.key, version: fixture.event.version)
|
||||
try fixture.persistence.journal.markUploaded(fixture.device.key, version: fixture.device.version)
|
||||
try fixture.context.save()
|
||||
try fixture.coordinator.lifecycle.updateUpToDateStatus()
|
||||
let initialSync = try fixture.persistence.journal.state().lastSuccessfulSync
|
||||
#expect(fixture.coordinator.historyStatus == .upToDate(lastSuccessfulSync: initialSync))
|
||||
|
||||
let future = try fixture.legacyRecord(fixture.event)
|
||||
var json = try #require(
|
||||
JSONSerialization.jsonObject(with: VaultSyncCoordinator.payloadData(from: future)) as? [String: Any])
|
||||
json["formatVersion"] = 2
|
||||
CloudSyncSafetyFixture.setPayload(try JSONSerialization.data(withJSONObject: json), in: future)
|
||||
future["formatVersion"] = NSNumber(value: 2)
|
||||
try fixture.coordinator.legacyImporter.beginFetch()
|
||||
try fixture.coordinator.legacyImporter.receive([future])
|
||||
try fixture.coordinator.legacyImporter.finishFetch(succeeded: true)
|
||||
#expect(
|
||||
fixture.coordinator.historyStatus
|
||||
== .partiallyBlocked(pendingCount: 0, blockedCount: 1, requiresUpdate: true))
|
||||
#expect(fixture.coordinator.deviceStatus == .upToDate(lastSuccessfulSync: initialSync))
|
||||
|
||||
try fixture.coordinator.legacyImporter.beginFetch()
|
||||
try fixture.coordinator.legacyImporter.receive([fixture.legacyRecord(fixture.event)])
|
||||
try fixture.coordinator.legacyImporter.finishFetch(succeeded: true)
|
||||
let resumedSync = try fixture.persistence.journal.state().lastSuccessfulSync
|
||||
#expect(fixture.coordinator.historyStatus == .upToDate(lastSuccessfulSync: resumedSync))
|
||||
}
|
||||
|
||||
@Test func unrelatedRecordTypeInLegacyZoneDoesNotQuarantineOrBlockHistory() async throws {
|
||||
let fixture = try HistoryFixture()
|
||||
defer { fixture.stop() }
|
||||
try await fixture.start()
|
||||
let record = CKRecord(
|
||||
recordType: "FutureVaultFeature",
|
||||
recordID: CKRecord.ID(
|
||||
recordName: "v1/futureFeature/\(UUID().uuidString.lowercased())",
|
||||
zoneID: CKRecordZone.ID(zoneName: VaultSyncCoordinator.zoneName, ownerName: CKCurrentUserDefaultName)))
|
||||
try fixture.coordinator.legacyImporter.receive([record])
|
||||
let sourceSafety = CloudSyncSafetyStore(context: fixture.context, scope: fixture.scope.historyImportScope)
|
||||
#expect(try sourceSafety.blockedRecordNames().isEmpty)
|
||||
#expect(try fixture.persistence.journal.envelope(for: fixture.event.key) == fixture.event)
|
||||
}
|
||||
|
||||
@Test func initialImporterFailurePreventsAllDestinationUploadsAndRetryRecovers() async throws {
|
||||
let fixture = try HistoryFixture()
|
||||
defer { fixture.stop() }
|
||||
|
||||
@@ -44,45 +44,6 @@ public final class HistorySyncCoordinator: NSObject, CKSyncEngineDelegate, @unch
|
||||
public var historyStatus: VaultSyncStatus { includesHistory ? status(for: .connectionHistory) : .paused }
|
||||
public var deviceStatus: VaultSyncStatus { status(for: .device) }
|
||||
|
||||
private func status(for kind: HistoryRecordKind) -> VaultSyncStatus {
|
||||
guard let scope = lifecycle.scope else { return lifecycle.status }
|
||||
if case .upToDate = lifecycle.status, !legacyImporter.ready(for: scope) { return .syncing(pendingCount: 0) }
|
||||
switch lifecycle.status {
|
||||
case .syncing, .partiallyBlocked:
|
||||
do {
|
||||
let pending = try journal.uploadablePendingRecordNames(in: scope).filter {
|
||||
HistoryRecordKey(recordName: $0)?.kind == kind
|
||||
}.count
|
||||
var blocked = Set<String>()
|
||||
var requiresUpdate = false
|
||||
for replica in [scope, scope.historyImportScope] {
|
||||
let safety = CloudSyncSafetyStore(context: context, scope: replica)
|
||||
for name in try safety.blockedRecordNames() {
|
||||
let recordKind = HistoryRecordKey(recordName: name)?.kind
|
||||
// Unknown kinds belong to the always-selected portion so an
|
||||
// optional History toggle cannot hide an upgrade requirement.
|
||||
guard recordKind == kind || (recordKind == nil && kind == .device) else { continue }
|
||||
blocked.insert(name)
|
||||
if try safety.entry(recordName: name)?.reasonCode
|
||||
== CloudSyncQuarantineReason.unsupportedFormat.rawValue
|
||||
{
|
||||
requiresUpdate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !blocked.isEmpty {
|
||||
return .partiallyBlocked(
|
||||
pendingCount: pending, blockedCount: blocked.count, requiresUpdate: requiresUpdate)
|
||||
}
|
||||
if case .partiallyBlocked = lifecycle.status, pending == 0 {
|
||||
return .upToDate(lastSuccessfulSync: try journal.state().lastSuccessfulSync)
|
||||
}
|
||||
return .syncing(pendingCount: pending)
|
||||
} catch { return .failed(error.localizedDescription) }
|
||||
default: return lifecycle.status
|
||||
}
|
||||
}
|
||||
|
||||
public convenience init(
|
||||
persistence: HistorySyncPersistence,
|
||||
container: CKContainer = CKContainer(identifier: HistorySyncCoordinator.containerIdentifier),
|
||||
@@ -168,7 +129,13 @@ public final class HistorySyncCoordinator: NSObject, CKSyncEngineDelegate, @unch
|
||||
importer.didApply = { [weak self] keys in self?.didApplyRemote?(keys) }
|
||||
importer.didFinish = { [weak self, weak importer] in
|
||||
guard let self, let importer, let scope = lifecycle.scope, importer.ready(for: scope) else { return }
|
||||
do { lifecycle.noteLocalChanges(try journal.uploadablePendingRecordNames(in: scope)) } catch {
|
||||
do {
|
||||
lifecycle.noteLocalChanges(try journal.uploadablePendingRecordNames(in: scope))
|
||||
// The source may have gained or cleared a quarantine without
|
||||
// producing any destination mutations. Recompute the visible
|
||||
// status after its checkpoint commits, even with no uploads.
|
||||
try lifecycle.updateUpToDateStatus()
|
||||
} catch {
|
||||
lifecycle.failClosed(error)
|
||||
}
|
||||
}
|
||||
@@ -416,5 +383,45 @@ public final class HistorySyncCoordinator: NSObject, CKSyncEngineDelegate, @unch
|
||||
public nonisolated static func systemFields(of record: CKRecord) throws -> Data {
|
||||
try CloudSyncRecordCoding.systemFields(of: record)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension HistorySyncCoordinator {
|
||||
private func status(for kind: HistoryRecordKind) -> VaultSyncStatus {
|
||||
guard let scope = lifecycle.scope else { return lifecycle.status }
|
||||
if case .upToDate = lifecycle.status, !legacyImporter.ready(for: scope) { return .syncing(pendingCount: 0) }
|
||||
switch lifecycle.status {
|
||||
case .syncing, .partiallyBlocked:
|
||||
do {
|
||||
let pending = try journal.uploadablePendingRecordNames(in: scope).filter {
|
||||
HistoryRecordKey(recordName: $0)?.kind == kind
|
||||
}.count
|
||||
var blocked = Set<String>()
|
||||
var requiresUpdate = false
|
||||
for replica in [scope, scope.historyImportScope] {
|
||||
let safety = CloudSyncSafetyStore(context: context, scope: replica)
|
||||
for name in try safety.blockedRecordNames() {
|
||||
let recordKind = HistoryRecordKey(recordName: name)?.kind
|
||||
// Unknown kinds belong to the always-selected portion so an
|
||||
// optional History toggle cannot hide an upgrade requirement.
|
||||
guard recordKind == kind || (recordKind == nil && kind == .device) else { continue }
|
||||
blocked.insert(name)
|
||||
if try safety.entry(recordName: name)?.reasonCode
|
||||
== CloudSyncQuarantineReason.unsupportedFormat.rawValue
|
||||
{
|
||||
requiresUpdate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !blocked.isEmpty {
|
||||
return .partiallyBlocked(
|
||||
pendingCount: pending, blockedCount: blocked.count, requiresUpdate: requiresUpdate)
|
||||
}
|
||||
if case .partiallyBlocked = lifecycle.status, pending == 0 {
|
||||
return .upToDate(lastSuccessfulSync: try journal.state().lastSuccessfulSync)
|
||||
}
|
||||
return .syncing(pendingCount: pending)
|
||||
} catch { return .failed(error.localizedDescription) }
|
||||
default: return lifecycle.status
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,7 +173,10 @@ final class LegacyHistoryImporter: NSObject, CKSyncEngineDelegate, @unchecked Se
|
||||
let safety = CloudSyncSafetyStore(context: context, scope: scope)
|
||||
var decoded: [HistoryEnvelope] = []
|
||||
var rejected = Set<String>()
|
||||
for record in records where record.recordID.zoneID == zoneID {
|
||||
// A separate record type in the legacy zone is not part of the Vault V1
|
||||
// history stream. Do not quarantine unrelated future features.
|
||||
for record in records
|
||||
where record.recordID.zoneID == zoneID && record.recordType == VaultSyncCoordinator.recordType {
|
||||
if let key = VaultRecordKey(recordName: record.recordID.recordName), key.kind.isConfiguration { continue }
|
||||
if let key = HistoryRecordKey(recordName: record.recordID.recordName),
|
||||
key.kind == .connectionHistory && !includesHistory()
|
||||
|
||||
Reference in new issue
Block a user