refactor: reenable cloud ids - #30345
Conversation
|
📱 Android release APK (universal) — Download: https://github.com/immich-app/immich/actions/runs/31668778688/artifacts/9169073821 Installs as a separate app (applicationId |
770eab3 to
bbafecf
Compare
agg23
left a comment
There was a problem hiding this comment.
Why is this diff so large to push some IDs in a payload of an existing API call?
| guard #available(iOS 16, *) else { | ||
| return assetIds.map { CloudIdResult(assetId: $0) } | ||
| return assetIds.map { | ||
| CloudIdResult(assetId: $0, error: "Cloud identifiers require iOS 16", errorKind: .unsupported) |
There was a problem hiding this comment.
This error return seems weird to me
There was a problem hiding this comment.
It should be a bit more cleaner now as we do an early throw and handle it on the dart side rather than passing it back as an error kind
| let kind = cloudIdErrorKind(for: error) | ||
| var message = "Error getting Cloud Id: \(error.localizedDescription)" | ||
| if kind == .ambiguous, | ||
| let matches = (error as NSError).userInfo[PHLocalIdentifiersErrorKey] as? [String] { |
There was a problem hiding this comment.
If you're going to do this anyway, it seems like it should be part of the enum somehow
There was a problem hiding this comment.
The enum is auto generated by pigeon and it cannot be a dart enhanced enum (the one with constructors / methods in it). I've moved the error mapping to the helper method now
| import 'package:immich_mobile/platform/native_sync_api.g.dart'; | ||
| import 'package:logging/logging.dart'; | ||
|
|
||
| const kCloudIdChunkSize = 5000; |
| }) async { | ||
| final logger = Logger('resolveCloudIds'); | ||
|
|
||
| for (int offset = 0; offset < assetIds.length; offset += kCloudIdChunkSize) { |
There was a problem hiding this comment.
nit: I imagine Dart has a nice functional way to chunk content, and if not, you could just do a fancy loop over ceil(assetIds.length/kCloudIdChunkSize)
I see this and, while C for loops are obviously incredibly common, I immediately think I have to carefully check your logic
There was a problem hiding this comment.
Welp! Updated it to use slices now
| } | ||
|
|
||
| if (result.errorKind == CloudIdErrorKind.unsupported) { | ||
| logger.warning('Cloud IDs unavailable: ${result.error ?? "unsupported"}'); |
There was a problem hiding this comment.
We always want to abort in this scenario?
There was a problem hiding this comment.
Yes because if the platform does not support cloud ids, none of the others would resolve them as well. But the way this was handled previously was rather ugly with the looping. The new code now throws an error and we match on the specific case which is better than what we had previously
I did consider adding the iOS version check in the dart side before making the native call, but that adds a native call for all iOS versions but the failure is only on iOS 15 which is rather a small portion of devices, so decided to go with the exception approach for now
|
|
||
| final cloudMapping = <String, String>{}; | ||
| for (final result in await nativeSyncApi.getCloudIdForAssetIds(chunk)) { | ||
| if (result.cloudId != null) { |
There was a problem hiding this comment.
I would do this logic the other way. Do your early returns with logging, then at the bottom have your actual work. The continue being used in your base case is the tell
I would also try to do it functionally, but that's OK if you don't want to
There was a problem hiding this comment.
Good idea, updated it as such!
bbafecf to
de28149
Compare
In theory, it should've just been few lines getting uncommented as we already had code for syncing the cloud ids to the database. But the reason this got a bit bigger is that the change also has a bit of refactor as to how we fetch said ids as we've disabled it previously because of performance issues we've faced with it then. Part of it was the DB hang issue we had frequently but that has been fixed since. Still, Apple calls out the call to fetch the ids to be expensive so I thought it'd be best if we did it in batches rather than in a single call. The API batching and DB batching are also part of the same optimisation so as to not send a bulk initial payload to the server The limits I've selected are rather arbitrary. I landed on 5000 so the checkpoints (Native calls / DB update) runs frequently. If you'd rather want us to increase this limit, I can run some benchmarks around the time it takes for the native calls and we can increase it accordingly. |
| mocktail: ^1.0.5 | ||
| # Type safe platform code | ||
| pigeon: ^26.3.4 | ||
| pigeon: ^27.3.0 |
| dartPackageName: 'immich_mobile', | ||
| ), | ||
| ) | ||
| const String kUnSupportedOSError = 'UNSUPPORTED_OS'; |
|
|
||
| // await _localAlbumRepository.updateCloudMapping(cloudMapping); | ||
| Future<void> _mapCloudIds(List<LocalAsset> assets) async { | ||
| if (!CurrentPlatform.isIOS || assets.isEmpty) { |
There was a problem hiding this comment.
Now that I look at it again, this should not be gating resolveCloudIds and in fact all of the call sites should directly call resolveCloudIds.
resolveCloudIds can do the local optimization of only allowing iOS and !isEmpty. Otherwise your optimization is premature, and other callers can call resolveCloudIds in non-optimal scenarios
| }) async { | ||
| final logger = Logger('resolveCloudIds'); | ||
|
|
||
| for (final batch in assetIds.slices(kCloudIdChunkSize)) { |
| db.localAssetEntity, | ||
| db.localAssetEntity.id.isInQuery( | ||
| db.localAssetEntity.selectOnly() | ||
| ..addColumns([db.localAssetEntity.id.min()]) |
There was a problem hiding this comment.
Is there a reason behind id.min() other than selecting a single row?
| ]) | ||
| ..where( | ||
| db.remoteAssetEntity.ownerId.equals(userId) & | ||
| // Skip locked assets as we cannot update them without unlocking first |
There was a problem hiding this comment.
What does this mean? We're sending mappings to the server and it can't receive them because the assets are locked?
| db.localAssetEntity.iCloudId.isNotNull() & | ||
| // Only select assets that have a local cloud ID but either no remote cloud ID or a mismatched eTag | ||
| (db.remoteAssetCloudIdEntity.cloudId.isNull() | | ||
| db.remoteAssetCloudIdEntity.adjustmentTime.isNotExp(db.localAssetEntity.adjustmentTime) | |
There was a problem hiding this comment.
We should not be manually comparing these fields. I imagine this is done in a number of other places too. We need to have a single standard way of comparing
| }); | ||
|
|
||
| @visibleForTesting | ||
| Future<List<CloudIdMapping>> fetchMapping(Drift db, String userId, int limit, String? lastRemoteId) async { |
There was a problem hiding this comment.
I think this is of appropriate complexity that adding docstrings would be good. Particularly for what lastRemoteId is
| ..limit(limit); | ||
|
|
||
| if (lastRemoteId != null) { | ||
| query.where(db.remoteAssetEntity.id.isBiggerThanValue(lastRemoteId)); |
There was a problem hiding this comment.
As per the docstring comment, idk what this means but it looks weird
Also, who uses "bigger than" instead of "greater than"?
| }); | ||
|
|
||
| @visibleForTesting | ||
| Future<List<CloudIdMapping>> fetchMapping(Drift db, String userId, int limit, String? lastRemoteId) async { |
There was a problem hiding this comment.
I see now this method was moved. While the ordering might be nice, ideally we don't want to have large diffs like this where we can't tell if it's new or not
Verified that the cloud ids are pushed to the server on upload & during the sync cloud id flow. Also verified that on a reinstall with no albums selected for backups, the checksums are reconciled based off of the cloud ids