async Task CommitUpdateAsync(RecordItemChange change) { Debug.Assert(change.HasLocalData); if (await this.DetectDuplicateAsync(change)) { // Already applied this change return; } try { change.UpdatedKey = await m_store.RemoteStore.PutAsync(change.LocalData); return; } catch (Exception ex) { if (!m_errorHandler.ShouldCreateNewItemForConflict(change, ex)) { // // Let the calling function decide how to handle all other scenarios // throw; } } // // Conflict resolution is simple. We don't want to lose data. If the data on the server changed from underneath us, // we will write this item as a NEW one, since the user did actually take the trouble to create the entry // await this.CommitNewAsync(change); }
async Task <bool> CommitPutAsync(RecordItemChange change, long itemLockID) { IItemDataTyped item = await m_store.Local.GetByIDAsync(change.ItemID); if (item == null) { return(false); } change.LocalData = item; if (item.Key.IsLocal) { await this.CommitNewAsync(change); } else { await this.CommitUpdateAsync(change); } // // Refetch the item from HealthVault, to get updated dates etc... // await this.RefreshItemAsync(change, itemLockID); return(true); }
// Are there any changes for the given type ID? public IAsyncOperation <bool> HasChangesForTypeAsync(string typeID) { if (string.IsNullOrEmpty(typeID)) { throw new ArgumentException("typeID"); } return(AsyncInfo.Run(async cancelToken => { using (await CrossThreadLockScope.Enter(m_lock)) { await this.EnsureIndexAsync(); foreach (string itemID in m_itemIDIndex) { RecordItemChange change = await this.GetChangeAsync(itemID); if (change != null && change.IsChangeForType(typeID)) { return true; } } return false; } })); }
// Get ids of items that changed for a given type ID public IAsyncOperation <IList <string> > GetIDsOfChangedItemsForTypeAsync(string typeID) { if (string.IsNullOrEmpty(typeID)) { throw new ArgumentException("typeID"); } return(AsyncInfo.Run(async cancelToken => { List <string> ids = new List <string>(); using (await CrossThreadLockScope.Enter(m_lock)) { await this.EnsureIndexAsync(); foreach (string itemID in m_itemIDIndex) { RecordItemChange change = await this.GetChangeAsync(itemID); if (change != null && change.IsChangeForType(typeID)) { ids.Add(itemID); } } return (IList <string>)ids; } })); }
public bool ShouldCreateNewItemForConflict(RecordItemChange change, Exception ex) { ServerException se = ex as ServerException; if (se != null) { return (this.IsItemKeyNotFound(se)); // Sometimes when the item is really missing, we get this error.. } return false; }
public static int Compare(RecordItemChange x, RecordItemChange y) { int cmp = x.Timestamp.CompareTo(y.Timestamp); if (cmp == 0) { cmp = x.ItemID.CompareTo(y.ItemID); } return(cmp); }
public bool ShouldCreateNewItemForConflict(RecordItemChange change, Exception ex) { ServerException se = ex as ServerException; if (se != null) { return(this.IsItemKeyNotFound(se)); // Sometimes when the item is really missing, we get this error.. } return(false); }
async Task <bool> DetectDuplicateAsync(RecordItemChange change) { IList <RecordItem> results = await this.LookupCommitsInRemoteStoreAsync(change.ChangeID); if (results.IsNullOrEmpty()) { return(false); } change.UpdatedKey = results[0].Key; return(true); }
async Task UpdateChangeAttemptCount(RecordItemChange change) { try { change.Attempt++; await m_changeTable.SaveChangeAsync(change); } catch (Exception ex) { this.NotifyError(ex); } }
async Task RefreshItemAsync(RecordItemChange change, long lockID) { change.UpdatedItem = null; try { change.UpdatedItem = await m_store.RemoteStore.GetItemAsync(change.UpdatedKey, m_store.SectionsToFetch); } catch (Exception ex) { this.NotifyError(ex); } }
async Task NotifySynchronizedTypesAsync(RecordItemChange change) { try { if (this.SynchronizedTypes != null) { await this.SynchronizedTypes.OnChangeCommittedAsync(change); } } catch { } }
// Assumes that you've acquire the item lock earlier! // Critical that the item lock be acquired async Task <bool> CommitChange(RecordItemChange change, long itemLockID) { bool dequeue = false; try { await this.UpdateChangeAttemptCount(change); if (change.ChangeType == RecordItemChangeType.Remove) { await this.CommitRemoveAsync(change, itemLockID); } else { if (await this.CommitPutAsync(change, itemLockID)) { await this.NotifySynchronizedTypesAsync(change); } } this.NotifyCommitSuccess(change); dequeue = true; } catch (Exception ex) { if (m_errorHandler.IsHaltingError(ex)) { this.NotifyError(ex); return(false); // Stop processing items right away because of major Halting errors, such as Network } if (m_errorHandler.ShouldRetryCommit(change, ex)) { // Leave the change in the commit queue. try again later this.NotifyError(ex); } else { this.NotifyCommitFailed(change); dequeue = true; } } if (dequeue) { await m_changeTable.RemoveChangeAsync(change.ItemID); } return(true); }
internal async Task OnPutCommittedAsync(RecordItemChange change) { Debug.Assert(change.HasUpdatedItem || change.HasLocalData); RecordItem updatedItem = change.UpdatedItem; if (updatedItem == null) { updatedItem = change.LocalData.Item; updatedItem.Key = change.UpdatedKey; } await this.Data.Local.PutItemAsync(updatedItem); await this.UpdateKeyAsync(change.ItemID, updatedItem); }
public bool ShouldRetryCommit(RecordItemChange change, Exception ex) { if (this.IsClientValidationError(ex) || this.IsSerializationError(ex) || !this.IsHttpError(ex) ) { return false; } if (this.MaxAttemptsPerChange > 0 && change.Attempt >= this.MaxAttemptsPerChange) { return false; } return true; }
public bool ShouldRetryCommit(RecordItemChange change, Exception ex) { if (this.IsClientValidationError(ex) || this.IsSerializationError(ex) || !this.IsHttpError(ex) ) { return(false); } if (this.MaxAttemptsPerChange > 0 && change.Attempt >= this.MaxAttemptsPerChange) { return(false); } return(true); }
// Returns a list of item IDs... in "queue" order...with the oldest change first... public IAsyncOperation <IList <string> > GetChangeQueueAsync() { return(AsyncInfo.Run(async cancelToken => { using (await CrossThreadLockScope.Enter(m_lock)) { List <RecordItemChange> changes = await this.GetAllChangesAsync(); changes.Sort((x, y) => RecordItemChange.Compare(x, y)); List <string> queue = ( from change in changes select change.ItemID ).ToList(); return (IList <string>)queue; } })); }
internal async Task <List <RecordItemChange> > GetAllChangesAsync() { List <RecordItemChange> changes = new List <RecordItemChange>(); await this.EnsureIndexAsync(); foreach (string itemID in m_itemIDIndex) { RecordItemChange change = await this.GetChangeAsync(itemID); if (change != null) { changes.Add(change); } } return(changes); }
async Task CommitRemoveAsync(RecordItemChange change, long itemLockID) { try { if (change.Key.IsLocal) { return; } await m_store.RemoteStore.RemoveItemAsync(change.Key); } catch (ServerException se) { if (!m_errorHandler.IsItemKeyNotFound(se)) { throw; } } }
internal async Task OnChangeCommittedAsync(RecordItemChange change) { if (change.ChangeType != RecordItemChangeType.Put) { return; } try { SynchronizedType sType = await this.GetAsync(change.TypeID); await sType.OnPutCommittedAsync(change); this.TypeUpdated.SafeInvokeInUIThread(this, change.TypeID); } catch { } }
/// <summary> /// Commit the given change batch. /// /// For simplicity and robustness, the current implementation commits each each change individually. /// /// This should be fine as the # of pending changes at any time should be moderate. /// And and most changes will be committed as they are made - ONLINE. In fact, the majority of batches will contain /// exactly 1 item. /// /// If necessary, we can optimize this behavior later by reducing round trips: /// - Dupe Detection in batches /// - New Items in a batch /// However, since a single bad egg can ruin the entire batch, the error handling will be more complex. /// /// We can also consider introducing new platform methods that can essentially do all of this in a single call. /// /// </summary> async Task CommitChangesAsync(IList <string> changedItems) { foreach (string itemID in changedItems) { if (!this.IsCommitEnabled) { break; } long itemLockID = 0; if ((itemLockID = this.AcquireItemLock(itemID)) == 0) { // Item is being edited.. we'll wait until we try to commit the next batch continue; } try { bool shouldContinue = true; RecordItemChange change = await m_changeTable.GetChangeForItemAsync(itemID); if (change != null) { shouldContinue = await this.CommitChange(change, itemLockID); } if (!shouldContinue) // Was there a halting error? { return; } } finally { this.ReleaseItemLock(itemID, itemLockID); } } }
// Remove all pending changes for a given type ID public IAsyncAction RemoveAllChangesForTypeAsync(string typeID) { if (string.IsNullOrEmpty(typeID)) { throw new ArgumentException("typeID"); } return(AsyncInfo.Run(async cancelToken => { using (await CrossThreadLockScope.Enter(m_lock)) { await this.EnsureIndexAsync(); string[] allIDs = m_itemIDIndex.ToArray(); foreach (string itemID in allIDs) { RecordItemChange change = await this.GetChangeAsync(itemID); if (change != null && change.IsChangeForType(typeID)) { await this.DeleteChangeAsync(itemID); } } } })); }
public IAsyncOperation <IList <RecordItemChange> > GetChangesForItemsAsync(IList <string> itemIDs) { if (itemIDs.IsNullOrEmpty()) { throw new ArgumentException("itemID"); } return(AsyncInfo.Run <IList <RecordItemChange> >(async cancelToken => { using (await CrossThreadLockScope.Enter(m_lock)) { List <RecordItemChange> changes = new List <RecordItemChange>(); foreach (string id in itemIDs) { RecordItemChange change = await this.GetChangeAsync(id); if (change != null) { changes.Add(change); } } return changes; } })); }
// TRACK CHANGES for the given typeID & key public IAsyncAction TrackChangeAsync(string typeID, ItemKey key, RecordItemChangeType changeType) { if (string.IsNullOrEmpty(typeID)) { throw new ArgumentException("typeID"); } if (key == null) { throw new ArgumentException("key"); } return(AsyncInfo.Run(async cancelToken => { using (await CrossThreadLockScope.Enter(m_lock)) { RecordItemChange change = await this.GetChangeAsync(key.ID); change = RecordItemChange.UpdateChange(typeID, key, changeType, change); await this.SaveChangeAsync(change); this.UpdateIndex(key.ID); } })); }
// Assumes that you've acquire the item lock earlier! // Critical that the item lock be acquired async Task<bool> CommitChange(RecordItemChange change, long itemLockID) { bool dequeue = false; try { await this.UpdateChangeAttemptCount(change); if (change.ChangeType == RecordItemChangeType.Remove) { await this.CommitRemoveAsync(change, itemLockID); } else { if (await this.CommitPutAsync(change, itemLockID)) { await this.NotifySynchronizedTypesAsync(change); } } this.NotifyCommitSuccess(change); dequeue = true; } catch (Exception ex) { if (m_errorHandler.IsHaltingError(ex)) { this.NotifyError(ex); return false; // Stop processing items right away because of major Halting errors, such as Network } if (m_errorHandler.ShouldRetryCommit(change, ex)) { // Leave the change in the commit queue. try again later this.NotifyError(ex); } else { this.NotifyCommitFailed(change); dequeue = true; } } if (dequeue) { await m_changeTable.RemoveChangeAsync(change.ItemID); } return true; }
async Task<bool> CommitPutAsync(RecordItemChange change, long itemLockID) { IItemDataTyped item = await m_store.Local.GetByIDAsync(change.ItemID); if (item == null) { return false; } change.LocalData = item; if (item.Key.IsLocal) { await this.CommitNewAsync(change); } else { await this.CommitUpdateAsync(change); } // // Refetch the item from HealthVault, to get updated dates etc... // await this.RefreshItemAsync(change, itemLockID); return true; }
async Task CommitNewAsync(RecordItemChange change) { Debug.Assert(change.HasLocalData); change.UpdatedKey = await m_store.RemoteStore.NewAsync(change.LocalData); }
async Task CommitRemoveAsync(RecordItemChange change, long itemLockID) { try { if (change.Key.IsLocal) { return; } await m_store.RemoteStore.RemoveItemAsync(change.Key); } catch(ServerException se) { if (!m_errorHandler.IsItemKeyNotFound(se)) { throw; } } }
internal async Task SaveChangeAsync(RecordItemChange change) { await m_changeStore.PutAsync(change.ItemID, change); }
void NotifyCommitSuccess(RecordItemChange change) { Debug.WriteLine("Change Committed {0}", change); this.CommitSuccess.SafeInvokeInUIThread(this, change); }
void NotifyCommitFailed(RecordItemChange change) { Debug.WriteLine("Change Commit failed {0}", change); this.CommitFailed.SafeInvokeInUIThread(this, change); }
public static RecordItemChange UpdateChange(string typeID, ItemKey key, RecordItemChangeType changeType, RecordItemChange existingChange) { if (existingChange == null) { return new RecordItemChange(typeID, key, changeType); } if (existingChange.ChangeType == RecordItemChangeType.Remove && changeType == RecordItemChangeType.Put) { // Can't put an item already marked for deletion throw new StoreException(StoreErrorNumber.ItemAlreadyDeleted); } if (existingChange.TypeID != typeID) { // Defensive code. Should never happen throw new StoreException(StoreErrorNumber.TypeIDMismatch); } existingChange.InitChange(key, changeType); return existingChange; }
async Task UpdateChangeAttemptCount(RecordItemChange change) { try { change.Attempt++; await m_changeTable.SaveChangeAsync(change); } catch(Exception ex) { this.NotifyError(ex); } }
public static RecordItemChange UpdateChange(string typeID, ItemKey key, RecordItemChangeType changeType, RecordItemChange existingChange) { if (existingChange == null) { return(new RecordItemChange(typeID, key, changeType)); } if (existingChange.ChangeType == RecordItemChangeType.Remove && changeType == RecordItemChangeType.Put) { // Can't put an item already marked for deletion throw new StoreException(StoreErrorNumber.ItemAlreadyDeleted); } if (existingChange.TypeID != typeID) { // Defensive code. Should never happen throw new StoreException(StoreErrorNumber.TypeIDMismatch); } existingChange.InitChange(key, changeType); return(existingChange); }
async Task<bool> DetectDuplicateAsync(RecordItemChange change) { IList<RecordItem> results = await this.LookupCommitsInRemoteStoreAsync(change.ChangeID); if (results.IsNullOrEmpty()) { return false; } change.UpdatedKey = results[0].Key; return true; }
public static int Compare(RecordItemChange x, RecordItemChange y) { int cmp = x.Timestamp.CompareTo(y.Timestamp); if (cmp == 0) { cmp = x.ItemID.CompareTo(y.ItemID); } return cmp; }
async Task RefreshItemAsync(RecordItemChange change, long lockID) { change.UpdatedItem = null; try { change.UpdatedItem = await m_store.RemoteStore.GetItemAsync(change.UpdatedKey, m_store.SectionsToFetch); } catch(Exception ex) { this.NotifyError(ex); } }