-
Notifications
You must be signed in to change notification settings - Fork 121
[Local Catalog] Add incremental sync functionality to POSCatalogSyncCoordinator
#16117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
de3d16f
d8ff294
90d138e
7bb4af6
3d9d468
d54dd2e
a467350
616957e
db3456e
45e7608
d5c7feb
3cb66b6
c6aa59a
54ca153
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,14 @@ public protocol POSCatalogSyncCoordinatorProtocol { | |
| /// - maxAge: Maximum age before a sync is considered stale | ||
| /// - Returns: True if a sync should be performed | ||
| func shouldPerformFullSync(for siteID: Int64, maxAge: TimeInterval) async -> Bool | ||
|
|
||
| /// Performs an incremental sync if applicable based on sync conditions | ||
| /// - Parameters: | ||
| /// - siteID: The site ID to sync catalog for | ||
| /// - forceSync: Whether to bypass age checks and always sync | ||
| /// - Throws: POSCatalogSyncError.syncAlreadyInProgress if a sync is already running for this site | ||
| //periphery:ignore - remove ignore comment when incremental sync is integrated with POS | ||
| func performIncrementalSyncIfApplicable(for siteID: Int64, forceSync: Bool) async throws | ||
| } | ||
|
|
||
| public enum POSCatalogSyncError: Error, Equatable { | ||
|
|
@@ -23,26 +31,23 @@ public enum POSCatalogSyncError: Error, Equatable { | |
|
|
||
| public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol { | ||
| private let fullSyncService: POSCatalogFullSyncServiceProtocol | ||
| private let persistenceService: POSCatalogPersistenceServiceProtocol | ||
| private let incrementalSyncService: POSCatalogIncrementalSyncServiceProtocol | ||
| private let grdbManager: GRDBManagerProtocol | ||
| private let maxIncrementalSyncAge: TimeInterval | ||
|
|
||
| /// Tracks ongoing syncs by site ID to prevent duplicates | ||
| /// Tracks ongoing full syncs by site ID to prevent duplicates | ||
| private var ongoingSyncs: Set<Int64> = [] | ||
| /// Tracks ongoing incremental syncs by site ID to prevent duplicates | ||
| private var ongoingIncrementalSyncs: Set<Int64> = [] | ||
|
|
||
| public init(fullSyncService: POSCatalogFullSyncServiceProtocol, | ||
| grdbManager: GRDBManagerProtocol) { | ||
| self.fullSyncService = fullSyncService | ||
| self.persistenceService = POSCatalogPersistenceService(grdbManager: grdbManager) | ||
| self.grdbManager = grdbManager | ||
| } | ||
|
|
||
| //periphery:ignore - used for tests to inject persistence service | ||
| init(fullSyncService: POSCatalogFullSyncServiceProtocol, | ||
| persistenceService: POSCatalogPersistenceServiceProtocol, | ||
| grdbManager: GRDBManagerProtocol) { | ||
| incrementalSyncService: POSCatalogIncrementalSyncServiceProtocol, | ||
| grdbManager: GRDBManagerProtocol, | ||
| maxIncrementalSyncAge: TimeInterval = 300) { | ||
| self.fullSyncService = fullSyncService | ||
| self.persistenceService = persistenceService | ||
| self.incrementalSyncService = incrementalSyncService | ||
| self.grdbManager = grdbManager | ||
| self.maxIncrementalSyncAge = maxIncrementalSyncAge | ||
| } | ||
|
|
||
| public func performFullSync(for siteID: Int64) async throws { | ||
|
|
@@ -61,7 +66,7 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol { | |
|
|
||
| DDLogInfo("🔄 POSCatalogSyncCoordinator starting full sync for site \(siteID)") | ||
|
|
||
| let catalog = try await fullSyncService.startFullSync(for: siteID) | ||
| _ = try await fullSyncService.startFullSync(for: siteID) | ||
|
|
||
| DDLogInfo("✅ POSCatalogSyncCoordinator completed full sync for site \(siteID)") | ||
| } | ||
|
|
@@ -89,6 +94,40 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol { | |
| return shouldSync | ||
| } | ||
|
|
||
| public func performIncrementalSyncIfApplicable(for siteID: Int64, forceSync: Bool) async throws { | ||
| if ongoingIncrementalSyncs.contains(siteID) { | ||
| DDLogInfo("⚠️ POSCatalogSyncCoordinator: Incremental sync already in progress for site \(siteID)") | ||
| throw POSCatalogSyncError.syncAlreadyInProgress(siteID: siteID) | ||
| } | ||
|
|
||
| guard let lastFullSyncDate = await lastFullSyncDate(for: siteID) else { | ||
| DDLogInfo("📋 POSCatalogSyncCoordinator: No full sync performed yet for site \(siteID), skipping incremental sync") | ||
| return | ||
| } | ||
|
|
||
| if !forceSync, let lastIncrementalSyncDate = await lastIncrementalSyncDate(for: siteID) { | ||
| let age = Date().timeIntervalSince(lastIncrementalSyncDate) | ||
|
|
||
| if age <= maxIncrementalSyncAge { | ||
| return DDLogInfo("📋 POSCatalogSyncCoordinator: Last incremental sync for site \(siteID) was \(Int(age))s ago, sync not needed") | ||
| } | ||
| } | ||
|
Comment on lines
+98
to
+114
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably worth moving this to a I think we should probably use the same approach for both – I don't always enjoy the "if applicable/if needed" uncertainty, but then it is the job of this coordinator to manage when syncs happen, so it's not unreasonable.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think are the use cases / benefits of a separate
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think giving both a private We probably don't ever need to make them public, but some possible future reasons:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sounds good, we can make it public when the use case calls for it. I created an issue WOOMOB-1333 for making this change. |
||
|
|
||
| ongoingIncrementalSyncs.insert(siteID) | ||
|
|
||
| defer { | ||
| ongoingIncrementalSyncs.remove(siteID) | ||
| } | ||
|
|
||
| DDLogInfo("🔄 POSCatalogSyncCoordinator starting incremental sync for site \(siteID)") | ||
|
|
||
| try await incrementalSyncService.startIncrementalSync(for: siteID, | ||
| lastFullSyncDate: lastFullSyncDate, | ||
| lastIncrementalSyncDate: lastIncrementalSyncDate(for: siteID)) | ||
|
|
||
| DDLogInfo("✅ POSCatalogSyncCoordinator completed incremental sync for site \(siteID)") | ||
| } | ||
|
|
||
| // MARK: - Private | ||
|
|
||
| private func lastFullSyncDate(for siteID: Int64) async -> Date? { | ||
|
|
@@ -102,6 +141,17 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol { | |
| } | ||
| } | ||
|
|
||
| private func lastIncrementalSyncDate(for siteID: Int64) async -> Date? { | ||
| do { | ||
| return try await grdbManager.databaseConnection.read { db in | ||
| return try PersistedSite.filter(key: siteID).fetchOne(db)?.lastCatalogIncrementalSyncDate | ||
| } | ||
| } catch { | ||
| DDLogError("⛔️ POSCatalogSyncCoordinator: Error loading site \(siteID) for incremental sync date: \(error)") | ||
| return nil | ||
| } | ||
| } | ||
|
Comment on lines
+144
to
+153
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I changed this, removing the It's worked out to be much simpler IMO, but I wanted to bring it to your attention, so let me know if you think we should go back |
||
|
|
||
| private func siteExistsInDatabase(siteID: Int64) -> Bool { | ||
| do { | ||
| return try grdbManager.databaseConnection.read { db in | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import Foundation | ||
| @testable import Yosemite | ||
|
|
||
| final class MockPOSCatalogIncrementalSyncService: POSCatalogIncrementalSyncServiceProtocol { | ||
| var startIncrementalSyncResult: Result<Void, Error> = .success(()) | ||
|
|
||
| private(set) var startIncrementalSyncCallCount = 0 | ||
| private(set) var lastSyncSiteID: Int64? | ||
| private(set) var lastFullSyncDate: Date? | ||
| private(set) var lastIncrementalSyncDate: Date? | ||
|
|
||
| private var syncContinuation: CheckedContinuation<Void, Never>? | ||
| private var shouldBlockSync = false | ||
|
|
||
| func startIncrementalSync(for siteID: Int64, lastFullSyncDate: Date, lastIncrementalSyncDate: Date?) async throws { | ||
| startIncrementalSyncCallCount += 1 | ||
| lastSyncSiteID = siteID | ||
| self.lastFullSyncDate = lastFullSyncDate | ||
| self.lastIncrementalSyncDate = lastIncrementalSyncDate | ||
|
|
||
| if shouldBlockSync { | ||
| await withCheckedContinuation { continuation in | ||
| syncContinuation = continuation | ||
| } | ||
| } | ||
|
|
||
| switch startIncrementalSyncResult { | ||
| case .success: | ||
| return | ||
| case .failure(let error): | ||
| throw error | ||
| } | ||
| } | ||
| } | ||
|
|
||
| extension MockPOSCatalogIncrementalSyncService { | ||
| func blockNextSync() { | ||
| shouldBlockSync = true | ||
| } | ||
|
|
||
| func resumeBlockedSync() { | ||
| syncContinuation?.resume() | ||
| syncContinuation = nil | ||
| shouldBlockSync = false | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know whether we really want to be able to do a full sync and an incremental sync for the same site simultaneously.
Let's tackle in a separate ticket, as it's an edge case, but WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea good point, incremental sync should be skipped when a full sync is ongoing. Created an issue WOOMOB-1331.