/// <summary> /// Constructs a SyncConflict object. /// </summary> /// <param name="remoteRecord">record from remote storage</param> /// <param name="localRecord">record from local storage</param> public SyncConflict(Record remoteRecord, Record localRecord) { if (remoteRecord == null || localRecord == null) { throw new ArgumentException("Record can't be null"); } if (!remoteRecord.Key.Equals(localRecord.Key)) { throw new ArgumentException("The keys of remote record and local record don't match"); } this._key = remoteRecord.Key; this._remoteRecord = remoteRecord; this._localRecord = localRecord; }
private void UpdateOrInsertRecord(string identityId, string datasetName, Record record) { lock (sqlite_lock) { SQLiteStatement stmt = null; try { stmt = db.Prepare( RecordColumns.BuildQuery( RecordColumns.IDENTITY_ID + " = @whereIdentityId AND " + RecordColumns.DATASET_NAME + " = @whereDatasetName AND " + RecordColumns.KEY + " = @whereKey " )); stmt.BindText(1, identityId); stmt.BindText(2, datasetName); stmt.BindText(3, record.Key); bool recordsFound = false; while (stmt.Read()) { recordsFound = true; } stmt.FinalizeStm(); if (recordsFound) { stmt = db.Prepare( RecordColumns.BuildUpdate( new string[] { RecordColumns.VALUE, RecordColumns.SYNC_COUNT, RecordColumns.MODIFIED, RecordColumns.LAST_MODIFIED_TIMESTAMP, RecordColumns.LAST_MODIFIED_BY, RecordColumns.DEVICE_LAST_MODIFIED_TIMESTAMP }, RecordColumns.IDENTITY_ID + " = @whereIdentityId AND " + RecordColumns.DATASET_NAME + " = @whereDatasetName AND " + RecordColumns.KEY + " = @whereKey " )); stmt.BindText(1, record.Value); stmt.BindInt(2, record.SyncCount); stmt.BindInt(3, record.IsModified ? 1 : 0); stmt.BindDateTime(4, record.LastModifiedDate); stmt.BindText(5, record.LastModifiedBy); stmt.BindDateTime(6, record.DeviceLastModifiedDate); stmt.BindText(7, identityId); stmt.BindText(8, datasetName); stmt.BindText(9, record.Key); stmt.Step(); } else { stmt = db.Prepare(RecordColumns.BuildInsert()); stmt.BindText(1, identityId); stmt.BindText(2, datasetName); stmt.BindText(3, record.Key); stmt.BindText(4, record.Value); stmt.BindInt(5, record.SyncCount); stmt.BindDateTime(6, record.LastModifiedDate); stmt.BindText(7, record.LastModifiedBy); stmt.BindDateTime(8, record.DeviceLastModifiedDate); stmt.BindInt(9, record.IsModified ? 1 : 0); stmt.Step(); } } finally { stmt.FinalizeStm(); } } }
/// <summary> /// Puts a list of raw records into that dataset if /// the local version hasn't changed (to be used in /// synchronizations). /// </summary> /// <param name="identityId">Identity id.</param> /// <param name="datasetName">Dataset name.</param> /// /// <param name="records">A list of remote records to compare with</param> /// <param name="localRecords">A list of records to check for changes.</param> public void ConditionallyPutRecords(String identityId, String datasetName, List<Record> records, List<Record> localRecords) { /* * Grab an instance of the record from the local store with the remote change's * key and the snapshot version. * 1) If both are null the remote change is new and we should save. * 2) If both exist but the value has changed locally we shouldn't overwrite with the remote changes, * which will still exist in remote, but should update the sync count to avoid a false-conflict later. * 3) If both exist and the values have not changed, we should save the remote change. * 4) If the current check exists but it wasn't in the snapshot, we should save. */ Dictionary<string, Record> localRecordMap = new Dictionary<string, Record>(); foreach (Record record in localRecords) { localRecordMap[record.Key] = record; } foreach (Record record in records) { Record databaseRecord = this.GetRecord(identityId, datasetName, record.Key); Record oldDatabaseRecord = localRecordMap.ContainsKey(record.Key) ? localRecordMap[record.Key] : null; if (databaseRecord != null && oldDatabaseRecord != null) { // The record exists both before and after the update locally, but has it changed? if (databaseRecord.SyncCount != oldDatabaseRecord.SyncCount || !StringUtils.Equals(databaseRecord.LastModifiedBy, oldDatabaseRecord.LastModifiedBy)) { continue; } if (!StringUtils.Equals(databaseRecord.Value, oldDatabaseRecord.Value)) { if (StringUtils.Equals(record.Value, oldDatabaseRecord.Value)) { // The value has changed, so this is a local change during the push record operation. // Avoid a future conflict by updating the metadata so that it looks like the modifications that // occurred during the put record operation happened after the put operation completed. Record resolvedRecord = new Record( record.Key, databaseRecord.Value, record.SyncCount, record.LastModifiedDate, record.LastModifiedBy, databaseRecord.DeviceLastModifiedDate, true ); UpdateOrInsertRecord(identityId, datasetName, resolvedRecord); } else { continue; } } else { UpdateOrInsertRecord(identityId, datasetName, record); } } else { UpdateOrInsertRecord(identityId, datasetName, record); } } }
private async Task RunSyncOperationAsync(int retry, CancellationToken cancellationToken) #endif { long lastSyncCount = Local.GetLastSyncCount(IdentityId, DatasetName); #if !(BCL35 || UNITY) ExceptionDispatchInfo capturedException = null; #endif // if dataset is deleted locally, push it to remote if (lastSyncCount == -1) { try { #if BCL35||UNITY Remote.DeleteDataset(DatasetName); #else await Remote.DeleteDatasetAsync(DatasetName, cancellationToken).ConfigureAwait(false); #endif } catch (DatasetNotFoundException) { //Ignore the exception here, since the dataset was local only } catch (Exception e) { _logger.InfoFormat("{0} , dataset : {1}", e.Message, this.DatasetName); EndSynchronizeAndCleanup(); #if UNITY FireSyncFailureEvent(e, options); #else FireSyncFailureEvent(e); #endif return; } Local.PurgeDataset(IdentityId, DatasetName); _logger.InfoFormat("OnSyncSuccess: dataset delete is pushed to remote - {0}", this.DatasetName); EndSynchronizeAndCleanup(); #if UNITY FireSyncSuccessEvent(new List<Record>(), options); #else FireSyncSuccessEvent(new List<Record>()); #endif return; } // get latest modified records from remote _logger.InfoFormat("Get latest modified records since {0} for dataset {1}", lastSyncCount, this.DatasetName); DatasetUpdates datasetUpdates = null; try { #if BCL35||UNITY datasetUpdates = Remote.ListUpdates(DatasetName, lastSyncCount); #else datasetUpdates = await Remote.ListUpdatesAsync(DatasetName, lastSyncCount, cancellationToken).ConfigureAwait(false); #endif } catch (Exception listUpdatesException) { _logger.Error(listUpdatesException, string.Empty); EndSynchronizeAndCleanup(); #if UNITY FireSyncFailureEvent(listUpdatesException, options); #else FireSyncFailureEvent(listUpdatesException); #endif return; } if (datasetUpdates != null && datasetUpdates.MergedDatasetNameList.Count != 0 && this.OnDatasetMerged != null) { bool resume = this.OnDatasetMerged(this, datasetUpdates.MergedDatasetNameList); if (resume) { if (retry == 0) { EndSynchronizeAndCleanup(); #if UNITY FireSyncFailureEvent(new SyncManagerException("Out of retries"), options); #else FireSyncFailureEvent(new SyncManagerException("Out of retries")); #endif } else { #if BCL35 this.RunSyncOperation(--retry); #elif UNITY this.RunSyncOperation(--retry, options); #else await this.RunSyncOperationAsync(--retry, cancellationToken).ConfigureAwait(false); #endif } return; } else { _logger.InfoFormat("OnSyncFailure: Manual Cancel"); EndSynchronizeAndCleanup(); #if UNITY FireSyncFailureEvent(new SyncManagerException("Manual cancel"), options); #else FireSyncFailureEvent(new SyncManagerException("Manual cancel")); #endif return; } } // if the dataset doesn't exist or is deleted, trigger onDelete if (lastSyncCount != 0 && !datasetUpdates.Exists || datasetUpdates.Deleted && this.OnDatasetDeleted != null) { bool resume = this.OnDatasetDeleted(this); if (resume) { // remove both records and metadata Local.DeleteDataset(IdentityId, DatasetName); Local.PurgeDataset(IdentityId, DatasetName); _logger.InfoFormat("OnSyncSuccess"); EndSynchronizeAndCleanup(); #if UNITY FireSyncSuccessEvent(new List<Record>(), options); #else FireSyncSuccessEvent(new List<Record>()); #endif return; } else { _logger.InfoFormat("OnSyncFailure"); EndSynchronizeAndCleanup(); #if UNITY FireSyncFailureEvent(new SyncManagerException("Manual cancel"), options); #else FireSyncFailureEvent(new SyncManagerException("Manual cancel")); #endif return; } } lastSyncCount = datasetUpdates.SyncCount; List<Record> remoteRecords = datasetUpdates.Records; if (remoteRecords.Count != 0) { // if conflict, prompt developer/user with callback List<SyncConflict> conflicts = new List<SyncConflict>(); List<Record> conflictRecords = new List<Record>(); foreach (Record remoteRecord in remoteRecords) { Record localRecord = Local.GetRecord(IdentityId, DatasetName, remoteRecord.Key); // only when local is changed and its value is different if (localRecord != null && localRecord.IsModified && !StringUtils.Equals(localRecord.Value, remoteRecord.Value)) { conflicts.Add(new SyncConflict(remoteRecord, localRecord)); conflictRecords.Add(remoteRecord); } } // retaining only non-conflict records remoteRecords.RemoveAll(t => conflictRecords.Contains(t)); if (conflicts.Count > 0) { _logger.InfoFormat("{0} records in conflict!", conflicts.Count); bool syncConflictResult = false; if (this.OnSyncConflict == null) { // delegate is not implemented so the conflict resolution is applied syncConflictResult = this.ResolveConflictsWithDefaultPolicy(conflicts); } else { syncConflictResult = this.OnSyncConflict(this, conflicts); } if (!syncConflictResult) { _logger.InfoFormat("User cancelled conflict resolution"); EndSynchronizeAndCleanup(); #if UNITY FireSyncFailureEvent(new OperationCanceledException("User cancelled conflict resolution"), options); #else FireSyncFailureEvent(new OperationCanceledException("User cancelled conflict resolution")); #endif return; } } // save to local if (remoteRecords.Count > 0) { _logger.InfoFormat("Save {0} records to local", remoteRecords.Count); Local.PutRecords(IdentityId, DatasetName, remoteRecords); } // new last sync count _logger.InfoFormat("Updated sync count {0}", datasetUpdates.SyncCount); Local.UpdateLastSyncCount(IdentityId, DatasetName, datasetUpdates.SyncCount); } // push changes to remote List<Record> localChanges = this.ModifiedRecords; long minPatchSyncCount = lastSyncCount; foreach (Record r in localChanges) { //track the max sync count if (r.SyncCount < minPatchSyncCount) { minPatchSyncCount = r.SyncCount; } } if (localChanges.Count != 0) { _logger.InfoFormat("Push {0} records to remote", localChanges.Count); try { #if BCL35 || UNITY List<Record> result = Remote.PutRecords(DatasetName, localChanges, datasetUpdates.SyncSessionToken); #else List<Record> result = await Remote.PutRecordsAsync(DatasetName, localChanges, datasetUpdates.SyncSessionToken, cancellationToken).ConfigureAwait(false); #endif // update local meta data Local.ConditionallyPutRecords(IdentityId, DatasetName, result, localChanges); // verify the server sync count is increased exactly by one, aka no // other updates were made during this update. long newSyncCount = 0; foreach (Record record in result) { newSyncCount = newSyncCount < record.SyncCount ? record.SyncCount : newSyncCount; } if (newSyncCount == lastSyncCount + 1) { _logger.InfoFormat("Updated sync count {0}", newSyncCount); Local.UpdateLastSyncCount(IdentityId, DatasetName, newSyncCount); } _logger.InfoFormat("OnSyncSuccess"); EndSynchronizeAndCleanup(); #if UNITY FireSyncSuccessEvent(remoteRecords, options); #else FireSyncSuccessEvent(remoteRecords); #endif return; } catch (DataConflictException e) { _logger.InfoFormat("Conflicts detected when pushing changes to remote: {0}", e.Message); if (retry == 0) { EndSynchronizeAndCleanup(); #if UNITY FireSyncFailureEvent(e, options); #else FireSyncFailureEvent(e); #endif } else { //it's possible there is a local dirty record with a stale sync count this will fix it if (lastSyncCount > minPatchSyncCount) { Local.UpdateLastSyncCount(IdentityId, DatasetName, minPatchSyncCount); } #if BCL35 RunSyncOperation(--retry); } return; } #elif UNITY RunSyncOperation(--retry, options); } return; }
/// <summary> /// Retrieves the status of a record. /// </summary> /// <returns><c>true</c> if it is modified locally; otherwise, <c>false</c>.</returns> /// <param name="key">Key identifying a record</param> public bool IsModified(string key) { Record record = Local.GetRecord(IdentityId, DatasetName, DatasetUtils.ValidateRecordKey(key)); return (record != null && record.IsModified); }