A Record is the element stored in a Dataset. There can be up to 1k records or up to 1 MB in size in a Dataset.
 /// <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();
                }
            }
        }
Example #3
0
        /// <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);
                }
            }
        }
Example #4
0
        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;
                }
Example #5
0
 /// <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);
 }