Created
March 22, 2017 15:42
-
-
Save ericbrunner/44fe9d0aeb82227ebc38d2b3b53a32c6 to your computer and use it in GitHub Desktop.
OfflineSyncManager
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// To add offline sync support: add the NuGet package Microsoft.Azure.Mobile.Client.SQLiteStore | |
// to all projects in the solution and uncomment the symbol definition OFFLINE_SYNC_ENABLED | |
// For Xamarin.iOS, also edit AppDelegate.cs and uncomment the call to SQLitePCL.CurrentPlatform.Init() | |
// For more information, see: http://go.microsoft.com/fwlink/?LinkId=620342 | |
using System; | |
using System.Collections.ObjectModel; | |
using System.Linq; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using Acr.UserDialogs; | |
using Microsoft.WindowsAzure.MobileServices; | |
using Microsoft.WindowsAzure.MobileServices.Sync; | |
using Plugin.Connectivity; | |
using trucker_rolsped.Helper; | |
using trucker_rolsped.Models; | |
using Microsoft.WindowsAzure.MobileServices.SQLiteStore; | |
using Newtonsoft.Json.Linq; | |
using Plugin.Connectivity.Abstractions; | |
using Polly; | |
using Polly.Wrap; | |
using trucker_rolsped.Auth; | |
using trucker_rolsped.Exceptions.OfflineSyncManager; | |
using trucker_rolsped.Helper.HockeyApp; | |
using trucker_rolsped.Interfaces.Authentication; | |
using trucker_rolsped.Interfaces.Utils; | |
using trucker_rolsped.Pages; | |
using trucker_rolsped.Workflow; | |
using Xamarin.Forms; | |
namespace trucker_rolsped.StoreManager | |
{ | |
public sealed class OfflineSyncStoreManager | |
{ | |
public readonly MobileServiceClient MobileAppClient; | |
public static readonly OfflineSyncStoreManager Instance = new OfflineSyncStoreManager(); | |
public static bool IsSyncStoreInitialized => Instance.MobileAppClient.SyncContext.IsInitialized; | |
public bool IsOfflineEnabled => true; | |
public IMobileServiceSyncTable<TagSprache> TagSpracheTable; | |
public IMobileServiceSyncTable<TruckFahrer> TruckFahrerTable; | |
public IMobileServiceSyncTable<TruckAuftrag> TruckAuftragTable; | |
public IMobileServiceSyncTable<TruckAuftragLauf> TruckAuftragLaufTable; | |
public IMobileServiceSyncTable<TruckAuftragWorkFlow> TruckAuftragWorkFlowTable; | |
public IMobileServiceSyncTable<TruckWorkFlowStamm> TruckWorkFlowStammTable; | |
public IMobileServiceSyncTable<TruckProblemStamm> TruckProblemStammTable; | |
public IMobileServiceSyncTable<TruckAuftragProblem> TruckAuftragProblemTable; | |
public readonly TagSpracheStore TagSpracheStore; | |
public readonly TruckFahrerStore TruckFahrerStore; | |
public readonly TruckAuftragStore TruckAuftragStore; | |
public readonly TruckAuftragLaufStore TruckAuftragLaufStore; | |
public readonly TruckAuftragOverviewStore TruckAuftragOverviewStore; | |
public readonly TruckAuftragHistoryStore TruckAuftragHistoryStore; | |
public readonly TruckAuftragWorkFlowStore TruckAuftragWorkFlowStore; | |
public readonly TruckWorkFlowStammStore TruckWorkFlowStammStore; | |
public readonly TruckProblemStammStore TruckProblemStammStore; | |
public readonly TruckAuftragProblemStore TruckAuftragProblemStore; | |
public static readonly string TagSpracheQueryAll = "allTagSpracheItems"; | |
public static readonly string TruckFahrerQueryAll = "allTruckFahrerItems"; | |
public static readonly string TruckAuftragQueryAll = "allTruckAuftragItems"; | |
public static readonly string TruckAuftragLaufQueryAll = "allTruckAuftragLaufItems"; | |
public static readonly string TruckAuftragWorkFlowQueryAll = "allTruckAuftragWorkFlowItems"; | |
public static readonly string TruckWorkFlowStammQueryAll = "allTruckWorkFlowStammItems"; | |
public static readonly string TruckProblemStammQueryAll = "allTruckProblemStammItems"; | |
private IMobileServiceTableQuery<TagSprache> _tagSpracheTableQuery; | |
private IMobileServiceTableQuery<TruckFahrer> _truckfahrerTableQuery; | |
private IMobileServiceTableQuery<TruckAuftrag> _truckAuftragTableQuery; | |
private IMobileServiceTableQuery<TruckAuftragLauf> _truckAuftragLaufTableQuery; | |
private IMobileServiceTableQuery<TruckAuftragWorkFlow> _truckAuftragWorkflowTableQuery; | |
private IMobileServiceTableQuery<TruckWorkFlowStamm> _truckWorkflowStammTableQuery; | |
private IMobileServiceTableQuery<TruckProblemStamm> _truckProblemStammTableQuery; | |
private OfflineSyncStoreManager() | |
{ | |
MobileAppClient = new MobileServiceClient(Constants.AppServiceUrl, new AuthenticationDelegatingHandler()); | |
if (Constants.AlternateLoginHost != null) | |
MobileAppClient.AlternateLoginHost = new Uri(Constants.AlternateLoginHost); | |
// CRUD StoreManager (local store) | |
TagSpracheStore = new TagSpracheStore(); | |
TruckFahrerStore = new TruckFahrerStore(); | |
TruckAuftragStore = new TruckAuftragStore(); | |
TruckAuftragLaufStore = new TruckAuftragLaufStore(); | |
TruckAuftragOverviewStore = new TruckAuftragOverviewStore(); | |
TruckAuftragHistoryStore = new TruckAuftragHistoryStore(); | |
TruckAuftragWorkFlowStore = new TruckAuftragWorkFlowStore(); | |
TruckWorkFlowStammStore = new TruckWorkFlowStammStore(); | |
TruckProblemStammStore = new TruckProblemStammStore(); | |
TruckAuftragProblemStore = new TruckAuftragProblemStore(); | |
} | |
public async Task<bool> WaitForSyncStoreToBeInitialized() | |
{ | |
var isInitialized = false; | |
try | |
{ | |
// Retry a specified number of times, using a function to | |
// calculate the duration to wait between retries based on | |
// the current retry attempt (allows for exponential backoff) | |
// In this case will wait for | |
// 2 ^ 1 = 2 seconds then | |
// 2 ^ 2 = 4 seconds then | |
// 2 ^ 3 = 8 seconds then | |
// 2 ^ 4 = 16 seconds then | |
// 2 ^ 5 = 32 seconds | |
const int maxAttempts = 5; | |
var waitAndRetryPolicy = | |
Policy. | |
Handle<Exception>() | |
.WaitAndRetryAsync( | |
retryCount: maxAttempts, | |
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), | |
onRetryAsync: async (exception, timespan) => { await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("Info-SyncStore", exception.Message); } | |
); | |
var fallBackPolicy = Policy<bool> | |
.Handle<Exception>() | |
.FallbackAsync( | |
fallbackAction: async (cancelToken) => await Task.FromResult(false), | |
onFallbackAsync: async (result) => | |
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("Error-SyncStore", | |
$"After {maxAttempts} attempts stopped a to try to initialize the SyncStore. Message: {result?.Exception?.Message}")); | |
PolicyWrap<bool> policyWrap = fallBackPolicy.WrapAsync(waitAndRetryPolicy); | |
isInitialized = await policyWrap.ExecuteAsync(action: InitializeSyncStore); | |
} | |
catch (Exception e) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
return isInitialized; | |
} | |
private async Task<bool> InitializeSyncStore() | |
{ | |
bool isInitialized = OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.IsInitialized; | |
if (isInitialized) | |
return true; | |
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("Info-InitSyncStore", "Waiting for SyncStore to be initialized ..."); | |
var store = new MobileServiceSQLiteStore(Constants.SqliteStorePath); | |
// Real TruckerApp DataModels | |
store.DefineTable<TagSprache>(); | |
store.DefineTable<TruckFahrer>(); | |
store.DefineTable<TruckAuftrag>(); | |
store.DefineTable<TruckAuftragLauf>(); | |
store.DefineTable<TruckAuftragWorkFlow>(); | |
store.DefineTable<TruckWorkFlowStamm>(); | |
store.DefineTable<TruckProblemStamm>(); | |
store.DefineTable<TruckAuftragProblem>(); | |
//Initializes the SyncContext using the default IMobileServiceSyncHandler. | |
await MobileAppClient.SyncContext.InitializeAsync(store); | |
// Rolsped TruckerApp Tables (local store) | |
TagSpracheTable = MobileAppClient.GetSyncTable<TagSprache>(); | |
TruckFahrerTable = MobileAppClient.GetSyncTable<TruckFahrer>(); | |
TruckAuftragTable = MobileAppClient.GetSyncTable<TruckAuftrag>(); | |
TruckAuftragLaufTable = MobileAppClient.GetSyncTable<TruckAuftragLauf>(); | |
TruckAuftragWorkFlowTable = MobileAppClient.GetSyncTable<TruckAuftragWorkFlow>(); | |
TruckWorkFlowStammTable = MobileAppClient.GetSyncTable<TruckWorkFlowStamm>(); | |
TruckProblemStammTable = MobileAppClient.GetSyncTable<TruckProblemStamm>(); | |
TruckAuftragProblemTable = MobileAppClient.GetSyncTable<TruckAuftragProblem>(); | |
// Create Queries for remote-db to local-db data (from backend SQL store) pull request | |
_tagSpracheTableQuery = TagSpracheTable.CreateQuery().IncludeTotalCount(); | |
_truckfahrerTableQuery = TruckFahrerTable.CreateQuery().IncludeTotalCount(); | |
_truckAuftragTableQuery = TruckAuftragTable.CreateQuery().IncludeTotalCount(); | |
_truckAuftragLaufTableQuery = TruckAuftragLaufTable.CreateQuery().IncludeTotalCount(); | |
_truckAuftragWorkflowTableQuery = TruckAuftragWorkFlowTable.CreateQuery().IncludeTotalCount(); | |
_truckWorkflowStammTableQuery = TruckWorkFlowStammTable.CreateQuery().IncludeTotalCount(); | |
_truckProblemStammTableQuery = TruckProblemStammTable.CreateQuery().IncludeTotalCount(); | |
isInitialized = OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.IsInitialized; | |
if (!isInitialized) | |
throw new SyncStoreInitializationException("SyncStore not initialized!.Maybe with next attempt..."); | |
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("Info-InitSyncStore", "Awaited SyncStore Initialization."); | |
return true; | |
} | |
public async Task<bool> IsRemoteBackendReachable() | |
{ | |
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(); | |
try | |
{ | |
var isConnected = CrossConnectivity.Current.IsConnected; | |
var isReachable = await CrossConnectivity.Current.IsRemoteReachable(Constants.AppServiceUrl, port: 443); | |
var isRemoteBackendReachable = isConnected && isReachable; | |
tcs.SetResult(isRemoteBackendReachable); | |
} | |
catch (Exception e) | |
{ | |
tcs.SetResult(false); | |
} | |
return await tcs.Task; | |
} | |
private TaskCompletionSource<object> _phoneConnectivityChangedSource; | |
private bool _isPhoneConnectivityChangedRunning; | |
public Task WaitForPhoneConnectivityChangedToComplete() | |
{ | |
return _phoneConnectivityChangedSource == null ? Task.FromResult<object>(null) : _phoneConnectivityChangedSource.Task; | |
} | |
private void SetPhoneConnectivityChangedRunning(bool running) | |
{ | |
_isPhoneConnectivityChangedRunning = running; | |
if (_isPhoneConnectivityChangedRunning) | |
{ | |
_phoneConnectivityChangedSource = new TaskCompletionSource<object>(); | |
} | |
else | |
{ | |
_phoneConnectivityChangedSource.SetResult(null); | |
} | |
} | |
private readonly SemaphoreSlim _phoneConnectivityChangedMutex = new SemaphoreSlim(1, 1); | |
public async void PhoneConnectivityChanged(object sender, ConnectivityChangedEventArgs e) | |
{ | |
if (_phoneConnectivityChangedMutex.CurrentCount == 0) | |
{ | |
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("Info-SyncLock-PhoneConnectivityChanged", "No further threads can enter critical section. Indicator for high frequency connection lost / connection estabished."); | |
} | |
//Asynchronously wait to enter the Semaphore. If no-one has been granted access to the Semaphore, | |
//code execution will proceed, otherwise this thread waits here until the semaphore is released | |
await _phoneConnectivityChangedMutex.WaitAsync(); | |
SetPhoneConnectivityChangedRunning(running: true); | |
try | |
{ | |
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("PhoneConnectivityChanged", $"Connected: {(CrossConnectivity.Current.IsConnected ? "TRUE" : "FALSE")}"); | |
if (!CrossConnectivity.Current.IsConnected) | |
{ | |
SetPhoneConnectivityChangedRunning(running: false); | |
_phoneConnectivityChangedMutex.Release(); | |
return; | |
} | |
if (!await IsRemoteBackendReachable()) | |
{ | |
SetPhoneConnectivityChangedRunning(running: false); | |
_phoneConnectivityChangedMutex.Release(); | |
return; | |
} | |
if (!App.TruckDriver.Authenticated) | |
{ | |
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("PhoneConnectivityChanged", "SignIn"); | |
App.TruckDriver = await DependencyService.Get<IAuthenticate>().SignIn(); | |
} | |
if (!App.TruckDriver.Authenticated) | |
{ | |
SetPhoneConnectivityChangedRunning(running: false); | |
_phoneConnectivityChangedMutex.Release(); | |
return; | |
} | |
if (!OfflineSyncStoreManager.IsSyncStoreInitialized) | |
{ | |
if (!await OfflineSyncStoreManager.Instance.WaitForSyncStoreToBeInitialized()) | |
{ | |
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("Error-SyncStore-PhoneConnectivityChanged", "SyncStore couldn't be initialized! App must be terminated."); | |
DependencyService.Get<IApplicationClose>().CloseApp(); | |
return; | |
} | |
} | |
// Wait for local Pending Tasks / Operations to complete | |
await WaitForPendingTasksToComplete(); | |
if (App.IsApplicationVisible) | |
{ | |
try | |
{ | |
UserDialogs.Instance.ShowLoading("Daten werden synchronisiert..."); | |
await OfflineSyncStoreManager.Instance.PushPurgePullOfAllTablesInLocalStoreAsync(); | |
UserDialogs.Instance.HideLoading(); | |
} | |
catch (Exception exception) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(exception); | |
} | |
finally | |
{ | |
SetPhoneConnectivityChangedRunning(running: false); | |
_phoneConnectivityChangedMutex.Release(); | |
await PopToRootView(); | |
} | |
} | |
else | |
{ | |
try | |
{ | |
await OfflineSyncStoreManager.Instance.PushPurgePullOfAllTablesInLocalStoreAsync(); | |
} | |
catch (Exception exception) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(exception); | |
} | |
finally | |
{ | |
SetPhoneConnectivityChangedRunning(running: false); | |
_phoneConnectivityChangedMutex.Release(); | |
await PopToRootView(); | |
} | |
} | |
} | |
catch (Exception ex) | |
{ | |
SetPhoneConnectivityChangedRunning(running: false); | |
_phoneConnectivityChangedMutex.Release(); | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(ex); | |
} | |
} | |
public static async Task PopToRootView() | |
{ | |
// Show TruckAuftragOverview. OnAppearing => Authenticate + RefreshItemsAsync triggered. | |
MainPageCS masterDetailPage = Application.Current.MainPage as MainPageCS; | |
if (masterDetailPage != null) | |
{ | |
if (masterDetailPage.IsPresented) | |
{ | |
masterDetailPage.TruckerAppMasterPage.ListView.SelectedItem = null; | |
masterDetailPage.IsPresented = false; | |
} | |
masterDetailPage.Detail = masterDetailPage.TruckAuftragOverviewNavigation; | |
// If not on the TruckAuftragOverview page | |
if (masterDetailPage.Detail.Navigation.NavigationStack.Count > 1) | |
{ | |
await masterDetailPage.Detail.Navigation.PopToRootAsync(animated: false); | |
// Note: Authenticate, Refresh triggered implicitly when popped OnAppearing event is triggered. | |
} | |
Page uncastedOverviewPage = masterDetailPage.TruckAuftragOverviewNavigation.Navigation.NavigationStack.FirstOrDefault(); | |
var overviewPage = uncastedOverviewPage as TruckAuftragOverviewPage; | |
if (overviewPage != null) | |
{ | |
await overviewPage.Authenticate(); | |
} | |
} | |
} | |
private static async Task WaitForPendingTasksToComplete() | |
{ | |
System.Diagnostics.Debug.WriteLine("PhoneConnectivityChanged: Waiting for local user active WorkflowTask to complete..."); | |
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("PhoneConnectivityChanged", "Waiting for local user active WorkflowTask to complete..."); | |
// Check if a Workflow Task is active | |
await WorkflowTaskCommandFactory.Instance.WaitForWorkflowTaskToComplete(); | |
System.Diagnostics.Debug.WriteLine("PhoneConnectivityChanged: Awaited local user active WorkflowTask."); | |
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("PhoneConnectivityChanged", "Awaited local user active WorkflowTask."); | |
} | |
private async Task HandleConflictResolutionOfLocalTablePushAttemptAsync(ReadOnlyCollection<MobileServiceTableOperationError> syncErrors) | |
{ | |
/* | |
* * Handling Conflict Resolution | |
* * see :https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/#handling-conflict-resolution | |
* * */ | |
// Simple error/conflict handling. A real application would handle the various errors like network conditions, | |
// server conflicts and others via the IMobileServiceSyncHandler. | |
if (syncErrors != null) | |
{ | |
foreach (var error in syncErrors) | |
{ | |
var localItem = error.Item; | |
var serverItem = error.Result; | |
if (error.OperationKind == MobileServiceTableOperationKind.Update && error.Result != null) | |
{ | |
var localVersion = localItem?[MobileServiceSystemColumns.Version]; | |
var serverVersion = serverItem?[MobileServiceSystemColumns.Version]; | |
if (localVersion == null) | |
{ | |
if (await IsRemoteBackendReachable()) | |
{ | |
//Update failed, reverting to server's copy. | |
await error.CancelAndUpdateItemAsync(serverItem); | |
await MetricsManagerHelper.Instance | |
.SendErrorToApplicationInsightsAsync($"Operation: {error.OperationKind} LocalVersion == NULL." + | |
"Action: Reverting to remote-db server's copy\r\n" + | |
$"Table: {error.TableName} Record Id: {localItem?[MobileServiceSystemColumns.Id]}\r\n" + | |
$"Local: {localItem}\r\n" + | |
$"Server: {serverItem}"); | |
} | |
continue; | |
} | |
if (serverVersion == null) | |
{ | |
if (await IsRemoteBackendReachable()) | |
{ | |
//Update failed, reverting to server's copy. | |
await error.CancelAndUpdateItemAsync(serverItem); | |
await MetricsManagerHelper.Instance | |
.SendErrorToApplicationInsightsAsync($"Operation: {error.OperationKind} ServerVersion == NULL.\r\n" + | |
"Action: Reverting to remote-db server's copy\r\n" + | |
$"Table: {error.TableName} Record Id: {localItem?[MobileServiceSystemColumns.Id]}\r\n" + | |
$"Local: {localItem}\r\n" + | |
$"Server: {serverItem}"); | |
} | |
continue; | |
} | |
// Set Local version to Server Version an Update again. PendingOpertation is set to new updated localItem and in outer loop a PushAsync is attempted again. | |
localItem[MobileServiceSystemColumns.Version] = serverItem[MobileServiceSystemColumns.Version]; | |
if (await IsRemoteBackendReachable()) | |
{ | |
await error.UpdateOperationAsync(JObject.FromObject(localItem)); | |
await MetricsManagerHelper.Instance.SendInfoToApplicationInsightsAsync($"Operation: {error.OperationKind} LocalVersion older than (<) ServerVersion. LocalItem updated in local.db."); | |
} | |
} | |
else | |
{ | |
if (await IsRemoteBackendReachable()) | |
{ | |
// Discard local change. | |
await error.CancelAndDiscardItemAsync(); | |
await MetricsManagerHelper.Instance | |
.SendGenericMessageToApplicationInsightsAsync("Storno", $"Operation: {error.OperationKind} PUSH failed.\r\n" + | |
"Action: Discard local-db change. TruckerFahrer propably removed from that TruckAuftrag in RCSmobile*\r\n" + | |
$"Table: {error.TableName} Record Id: {localItem?[MobileServiceSystemColumns.Id]}\r\n" + | |
$"HttpStatus: {error.Status}\r\n" + | |
$"Local: {localItem}\r\n" + | |
$"Server: {serverItem}"); | |
} | |
} | |
} | |
} | |
} | |
#region LocalStore methods | |
public async Task PullAsync() | |
{ | |
bool isConnected = await IsRemoteBackendReachable(); | |
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db | |
// When it is online then a sync from the local-db with the remote-db is possible. | |
if (!isConnected) | |
{ | |
return; | |
} | |
try | |
{ | |
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/ | |
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/dataconcepts/#offline-synchronization | |
// Implict PUSH local-db changes to remote-db. Then PULL the remote-db data into local-db tables | |
await SyncTagSpracheTableInLocalStoreAsync(); | |
await SyncTruckFahrerTableInLocalStoreAsync(); | |
await SyncTruckAuftragTableInLocalStoreAsync(); | |
await SyncTruckAuftragLaufTableInLocalStoreAsync(); | |
await SyncTruckAuftragWorkflowTableInLocalStoreAsync(); | |
await SyncTruckWorkflowStammTableInLocalStoreAsync(); | |
await SyncTruckProblemStammTableInLocalStoreAsync(); | |
} | |
catch (Exception e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
} | |
} | |
public async Task SyncTruckProblemStammTableInLocalStoreAsync() | |
{ | |
bool isConnected = await IsRemoteBackendReachable(); | |
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db | |
// When it is online then a sync from the local-db with the remote-db is possible. | |
if (!isConnected) | |
{ | |
return; | |
} | |
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null; | |
try | |
{ | |
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/ | |
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/dataconcepts/#offline-synchronization | |
// Implict PUSH local-db changes to remote-db. Then PULL the remote-db data into local-db tables | |
await OfflineSyncStoreManager.Instance.TruckProblemStammTable.PullAsync(TruckProblemStammQueryAll, _truckProblemStammTableQuery); | |
} | |
catch (MobileServicePushFailedException e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
if (e.PushResult != null) | |
{ | |
syncErrors = e.PushResult.Errors; | |
} | |
} | |
} | |
catch (Exception e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
} | |
if (await IsRemoteBackendReachable() && syncErrors != null) | |
{ | |
try | |
{ | |
await HandleConflictResolutionOfLocalTablePushAttemptAsync(syncErrors); | |
} | |
catch (Exception e) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
} | |
} | |
public async Task SyncTruckWorkflowStammTableInLocalStoreAsync() | |
{ | |
bool isConnected = await IsRemoteBackendReachable(); | |
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db | |
// When it is online then a sync from the local-db with the remote-db is possible. | |
if (!isConnected) | |
{ | |
return; | |
} | |
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null; | |
try | |
{ | |
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/ | |
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/dataconcepts/#offline-synchronization | |
// Implict PUSH local-db changes to remote-db. Then PULL the remote-db data into local-db tables | |
await OfflineSyncStoreManager.Instance.TruckWorkFlowStammTable.PullAsync(TruckWorkFlowStammQueryAll, _truckWorkflowStammTableQuery); | |
} | |
catch (MobileServicePushFailedException e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
if (e.PushResult != null) | |
{ | |
syncErrors = e.PushResult.Errors; | |
} | |
} | |
} | |
catch (Exception e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
} | |
if (await IsRemoteBackendReachable() && syncErrors != null) | |
{ | |
try | |
{ | |
await HandleConflictResolutionOfLocalTablePushAttemptAsync(syncErrors); | |
} | |
catch (Exception e) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
} | |
} | |
public async Task SyncTruckAuftragWorkflowTableInLocalStoreAsync() | |
{ | |
bool isConnected = await IsRemoteBackendReachable(); | |
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db | |
// When it is online then a sync from the local-db with the remote-db is possible. | |
if (!isConnected) | |
{ | |
return; | |
} | |
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null; | |
try | |
{ | |
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/ | |
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/dataconcepts/#offline-synchronization | |
// Implict PUSH local-db changes to remote-db. Then PULL the remote-db data into local-db tables | |
await OfflineSyncStoreManager.Instance.TruckAuftragWorkFlowTable.PullAsync(TruckAuftragWorkFlowQueryAll, _truckAuftragWorkflowTableQuery); | |
} | |
catch (MobileServicePushFailedException e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
if (e.PushResult != null) | |
{ | |
syncErrors = e.PushResult.Errors; | |
} | |
} | |
} | |
catch (Exception e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
} | |
if (await IsRemoteBackendReachable() && syncErrors != null) | |
{ | |
try | |
{ | |
await HandleConflictResolutionOfLocalTablePushAttemptAsync(syncErrors); | |
} | |
catch (Exception e) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e).ConfigureAwait(false); | |
} | |
} | |
} | |
public async Task SyncTruckAuftragTableInLocalStoreAsync() | |
{ | |
bool isConnected = await IsRemoteBackendReachable(); | |
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db | |
// When it is online then a sync from the local-db with the remote-db is possible. | |
if (!isConnected) | |
{ | |
return; | |
} | |
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null; | |
try | |
{ | |
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/ | |
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/dataconcepts/#offline-synchronization | |
// Implict PUSH local-db changes to remote-db. Then PULL the remote-db data into local-db tables | |
await OfflineSyncStoreManager.Instance.TruckAuftragTable.PullAsync(TruckAuftragQueryAll, _truckAuftragTableQuery); | |
} | |
catch (MobileServicePushFailedException e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
if (e.PushResult != null) | |
{ | |
syncErrors = e.PushResult.Errors; | |
} | |
} | |
} | |
catch (Exception e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
} | |
if (await IsRemoteBackendReachable() && syncErrors != null) | |
{ | |
try | |
{ | |
await HandleConflictResolutionOfLocalTablePushAttemptAsync(syncErrors); | |
} | |
catch (Exception e) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
} | |
} | |
public async Task SyncTruckAuftragLaufTableInLocalStoreAsync() | |
{ | |
bool isConnected = await IsRemoteBackendReachable(); | |
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db | |
// When it is online then a sync from the local-db with the remote-db is possible. | |
if (!isConnected) | |
{ | |
return; | |
} | |
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null; | |
try | |
{ | |
await OfflineSyncStoreManager.Instance.TruckAuftragLaufTable.PullAsync(TruckAuftragLaufQueryAll, _truckAuftragLaufTableQuery); | |
} | |
catch (MobileServicePushFailedException e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
if (e.PushResult != null) | |
{ | |
syncErrors = e.PushResult.Errors; | |
} | |
} | |
} | |
catch (Exception e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
} | |
if (await IsRemoteBackendReachable() && syncErrors != null) | |
{ | |
try | |
{ | |
await HandleConflictResolutionOfLocalTablePushAttemptAsync(syncErrors); | |
} | |
catch (Exception e) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
} | |
} | |
public async Task SyncTagSpracheTableInLocalStoreAsync() | |
{ | |
bool isConnected = await IsRemoteBackendReachable(); | |
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db | |
// When it is online then a sync from the local-db with the remote-db is possible. | |
if (!isConnected) | |
{ | |
return; | |
} | |
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null; | |
try | |
{ | |
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/ | |
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/dataconcepts/#offline-synchronization | |
// Implict PUSH local-db changes to remote-db. Then PULL the remote-db data into local-db tables | |
await OfflineSyncStoreManager.Instance.TagSpracheTable.PullAsync(TagSpracheQueryAll, _tagSpracheTableQuery); | |
await OfflineSyncStoreManager.Instance.TagSpracheStore.InitLocalStoreCacheAsync(); | |
} | |
catch (MobileServicePushFailedException e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
if (e.PushResult != null) | |
{ | |
syncErrors = e.PushResult.Errors; | |
} | |
} | |
} | |
catch (Exception e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
} | |
if (await IsRemoteBackendReachable() && syncErrors != null) | |
{ | |
try | |
{ | |
await HandleConflictResolutionOfLocalTablePushAttemptAsync(syncErrors); | |
} | |
catch (Exception e) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
} | |
} | |
public async Task SyncTruckFahrerTableInLocalStoreAsync() | |
{ | |
bool isConnected = await IsRemoteBackendReachable(); | |
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db | |
// When it is online then a sync from the local-db with the remote-db is possible. | |
if (!isConnected) | |
{ | |
return; | |
} | |
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null; | |
try | |
{ | |
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/client/ | |
// see: https://adrianhall.github.io/develop-mobile-apps-with-csharp-and-azure/chapter3/dataconcepts/#offline-synchronization | |
// Implict PUSH local-db changes to remote-db. Then PULL the remote-db data into local-db tables | |
await OfflineSyncStoreManager.Instance.TruckFahrerTable.PullAsync(TruckFahrerQueryAll, _truckfahrerTableQuery); | |
} | |
catch (MobileServicePushFailedException e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
if (e.PushResult != null) | |
{ | |
syncErrors = e.PushResult.Errors; | |
} | |
} | |
} | |
catch (Exception e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
} | |
if (await IsRemoteBackendReachable() && syncErrors != null) | |
{ | |
try | |
{ | |
await HandleConflictResolutionOfLocalTablePushAttemptAsync(syncErrors); | |
} | |
catch (Exception e) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
} | |
} | |
//Instantiate a Singleton of the Semaphore with a value of 1. This means that only 1 thread can be granted access at a time. | |
private readonly SemaphoreSlim _pushAsyncMutex = new SemaphoreSlim(1, 1); | |
public async Task PushAsync(string contextName) | |
{ | |
if (_pushAsyncMutex.CurrentCount == 0) | |
{ | |
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("Info-SyncLock-PushAsync", "No further threads can enter critical section."); | |
} | |
//Asynchronously wait to enter the Semaphore. If no-one has been granted access to the Semaphore, | |
//code execution will proceed, otherwise this thread waits here until the semaphore is released | |
await _pushAsyncMutex.WaitAsync(); | |
try | |
{ | |
bool isConnected = await IsRemoteBackendReachable(); | |
// Sync is only enabled in Connected Mode. What should I sync in offline mode. There is not connection to the remote-db | |
// When it is online then a sync from the local-db with the remote-db is possible. | |
if (!isConnected) | |
{ | |
return; | |
} | |
const int maxAttempts = 5; | |
int currentAttempts = 0; | |
MobileServicePushFailedException mobileServicePushFailedException = null; | |
while (OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.PendingOperations > 0 && (currentAttempts < maxAttempts)) | |
{ | |
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null; | |
try | |
{ | |
System.Diagnostics.Debug.WriteLine($"Pending Ops before PUSH: {OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.PendingOperations}"); | |
// PUSH all local.db changes to the remote.db (changes of all tables are pushed to achieve referential integrity) | |
await OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.PushAsync(); | |
System.Diagnostics.Debug.WriteLine($"Pending Ops after PUSH: {OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.PendingOperations}"); | |
} | |
catch (MobileServicePushFailedException e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
mobileServicePushFailedException = e; | |
if (e.PushResult != null) | |
{ | |
syncErrors = e.PushResult.Errors; | |
} | |
if (syncErrors != null) | |
{ | |
System.Diagnostics.Debug.WriteLine($"Pending Ops before CONFLICT HANDLER: {OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.PendingOperations}"); | |
await HandleConflictResolutionOfLocalTablePushAttemptAsync(syncErrors); | |
System.Diagnostics.Debug.WriteLine($"Pending Ops after CONFLICT HANDLER: {OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.PendingOperations}"); | |
} | |
currentAttempts++; | |
} | |
else | |
{ | |
await MetricsManagerHelper.Instance.SendInfoToApplicationInsightsAsync("Internet Connection lost while data sync. Data sync not done."); | |
break; | |
} | |
} | |
catch (Exception e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
else | |
{ | |
await MetricsManagerHelper.Instance.SendInfoToApplicationInsightsAsync("Internet Connection lost while data sync. Data sync not done."); | |
} | |
} | |
} | |
if (isConnected && OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.PendingOperations > 0) | |
{ | |
if (mobileServicePushFailedException != null) | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(mobileServicePushFailedException); | |
await MetricsManagerHelper.Instance.SendInfoToApplicationInsightsAsync("Internet Connection lost while data sync. Data sync not done."); | |
} | |
} | |
catch (Exception e) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
} | |
finally | |
{ | |
// When the task is ready, release the semaphore.It is vital to ALWAYS release the semaphore when we are ready, or else we will end up with a Semaphore that is forever locked. | |
//This is why it is important to do the Release within a try...finally clause; program execution may crash or take a different path, this way you are guaranteed execution | |
_pushAsyncMutex.Release(); | |
} | |
} | |
public async Task PurgeTagSpracheTableAsync() | |
{ | |
try | |
{ | |
// PURGE TagSprachTabel in local.db | |
await Instance.TagSpracheTable.PurgeAsync(TagSpracheQueryAll, null, true, CancellationToken.None); | |
} | |
catch (Exception e) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
if (e.InnerException != null) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e.InnerException); | |
} | |
} | |
} | |
public async Task PurgeLocalStoreAsync() | |
{ | |
try | |
{ | |
// PURGE All local-db tables and force to delete pending changes | |
await Instance.TruckFahrerTable.PurgeAsync(TruckFahrerQueryAll, null, true, CancellationToken.None); | |
await Instance.TagSpracheTable.PurgeAsync(TagSpracheQueryAll, null, true, CancellationToken.None); | |
await Instance.TruckAuftragTable.PurgeAsync(TruckAuftragQueryAll, null, true, CancellationToken.None); | |
await Instance.TruckAuftragLaufTable.PurgeAsync(TruckAuftragLaufQueryAll, null, true, CancellationToken.None); | |
await Instance.TruckAuftragWorkFlowTable.PurgeAsync(TruckAuftragWorkFlowQueryAll, null, true, CancellationToken.None); | |
await Instance.TruckWorkFlowStammTable.PurgeAsync(TruckWorkFlowStammQueryAll, null, true, CancellationToken.None); | |
await Instance.TruckProblemStammTable.PurgeAsync(TruckProblemStammQueryAll, null, true, CancellationToken.None); | |
} | |
catch (Exception e) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
if (e.InnerException != null) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e.InnerException); | |
} | |
} | |
} | |
private readonly SemaphoreSlim _pushPurgePullMutex = new SemaphoreSlim(1, 1); | |
public async Task PushPurgePullOfAllTablesInLocalStoreAsync() | |
{ | |
if (_pushPurgePullMutex.CurrentCount == 0) | |
{ | |
await MetricsManagerHelper.Instance.SendGenericMessageToApplicationInsightsAsync("Info-SyncLock-PushPurgePullOfAllTablesInLocalStoreAsync", "No further threads can enter critical section."); | |
} | |
//Asynchronously wait to enter the Semaphore. If no-one has been granted access to the Semaphore, | |
//code execution will proceed, otherwise this thread waits here until the semaphore is released | |
await _pushPurgePullMutex.WaitAsync(); | |
bool isConnected; | |
try | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
// Purge only makes sense when we have connectivity | |
if (!isConnected) | |
return; | |
// PUSH any pending changes to the remote.db backend | |
await PushAsync("ALLTABLES"); | |
isConnected = await IsRemoteBackendReachable(); | |
if (!isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendErrorToApplicationInsightsAsync("Connection lost while trying to Push, Purge, Pull data..."); | |
return; | |
} | |
/* | |
* IMPORTANT Design Notes: | |
* ======================================================================================================================================================================== | |
* The previous call to PushAsync tries 5 times to push pending ops to the remote.db. To prevent that any pending ops in local.db are | |
* purged (deleted), that if statement check is done. | |
* | |
* When a TA Storno happens it could happen, that the "cancelled" TA remains in local.db when the PushAsync fails for any reason. That cancelled TA | |
* is no longer be assigned to the current LKW TruckFahrer. So any subsequent PullAsync will not fetch it any more, but it is as "stale" data in local.db. | |
* | |
* Ok, not good. | |
* | |
* If a LKW User would dare to start the workflow of that "stale" TA it would be rejected fron remote backend (Http NotFound exception thrown, see TruckAuftragController) | |
* | |
* If that TA was already started it could happen that PATCH operation to TruckAuftragLauf and TruckAuftragWorkflow goes through if the LKW User e.g. clicks on | |
* "Bin angekommen" Task. | |
* | |
* Well that is possible, but on the other hand it gets logged in Application Insights! | |
* | |
* PATCH could be prevented in remote backend on the TAL and TAW tables , too. But then if we throw a Http NotFound exception , we MUST ensure that that exception is | |
* never thrown when a Task31 TA is completed, where PATCH ops are done on TA, TAL, TAW where the TA.FahrerId is set to -1. | |
* | |
* | |
* Added such code in TruckAuftragLaufController and TruckAuftragWorkflowController, to prevent that a LKW User could successfully update a TA,TAL,TAW in remote.db | |
* that is already "cancelled" to him ("STORNO")!!! | |
* | |
* So far so good. | |
* | |
* So we prevented that a "cancelled" TA can be updated in remote.db from a LKW User that is no longer that assigned User. | |
* | |
* I can't prevent that that "stale" TA is displayed in the TruckerApp list for a while until the "PushPurgePullOfAllTablesInLocalStoreAsync" is invoked again. | |
* PushPurgePullOfAllTablesInLocalStoreAsync is invoked with next Push Command and/or PhoneConnectivityChanged event or when LKW User manually clicks on "Sync" button. | |
* | |
* */ | |
if (OfflineSyncStoreManager.Instance.MobileAppClient.SyncContext.PendingOperations == 0) | |
{ | |
// PURGE All local-db tables, but KEEP Pending Changes!!! Only get a "Fresh" copy of the data from remote.db | |
await Instance.TruckFahrerTable.PurgeAsync(TruckFahrerQueryAll, _truckfahrerTableQuery, CancellationToken.None); | |
await Instance.TagSpracheTable.PurgeAsync(TagSpracheQueryAll, _tagSpracheTableQuery, CancellationToken.None); | |
await Instance.TruckAuftragTable.PurgeAsync(TruckAuftragQueryAll, _truckAuftragTableQuery, CancellationToken.None); | |
await Instance.TruckAuftragLaufTable.PurgeAsync(TruckAuftragLaufQueryAll, _truckAuftragLaufTableQuery, CancellationToken.None); | |
await Instance.TruckAuftragWorkFlowTable.PurgeAsync(TruckAuftragWorkFlowQueryAll, _truckAuftragWorkflowTableQuery, CancellationToken.None); | |
await Instance.TruckWorkFlowStammTable.PurgeAsync(TruckWorkFlowStammQueryAll, _truckWorkflowStammTableQuery, CancellationToken.None); | |
await Instance.TruckProblemStammTable.PurgeAsync(TruckProblemStammQueryAll, _truckProblemStammTableQuery, CancellationToken.None); | |
} | |
//PULL all data from the remote-db sql server into the local-db truckerapp store | |
await Instance.PullAsync(); | |
} | |
catch (Exception e) | |
{ | |
isConnected = await IsRemoteBackendReachable(); | |
if (isConnected) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e); | |
if (e.InnerException != null) | |
{ | |
await MetricsManagerHelper.Instance.SendExceptionToApplicationInsightsAsync(e.InnerException); | |
} | |
} | |
} | |
finally | |
{ | |
// When the task is ready, release the semaphore.It is vital to ALWAYS release the semaphore when we are ready, or else we will end up with a Semaphore that is forever locked. | |
//This is why it is important to do the Release within a try...finally clause; program execution may crash or take a different path, this way you are guaranteed execution | |
_pushPurgePullMutex.Release(); | |
} | |
} | |
#endregion | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment