/// <summary> /// Gets a batch of changes to synchronize when given batch size, /// destination knowledge, and change data retriever parameters. /// </summary> /// <returns>A DbSyncContext object that will be used to retrieve the modified data.</returns> internal virtual async Task <(SyncContext, BatchInfo, DatabaseChangesSelected)> InternalGetChangesAsync( IScopeInfo scopeInfo, SyncContext context, bool isNew, long?fromLastTimestamp, long?toNewTimestamp, Guid?excludingScopeId, bool supportsMultiActiveResultSets, string batchRootDirectory, string batchDirectoryName, DbConnection connection, DbTransaction transaction, CancellationToken cancellationToken, IProgress <ProgressArgs> progress) { // batch info containing changes BatchInfo batchInfo; // Statistics about changes that are selected DatabaseChangesSelected changesSelected; context.SyncStage = SyncStage.ChangesSelecting; if (context.SyncWay == SyncWay.Upload && context.SyncType == SyncType.Reinitialize) { (batchInfo, changesSelected) = await this.InternalGetEmptyChangesAsync(scopeInfo, batchRootDirectory).ConfigureAwait(false); return(context, batchInfo, changesSelected); } // create local directory if (!string.IsNullOrEmpty(batchRootDirectory) && !Directory.Exists(batchRootDirectory)) { Directory.CreateDirectory(batchRootDirectory); } changesSelected = new DatabaseChangesSelected(); // Create a batch // batchinfo generate a schema clone with scope columns if needed batchInfo = new BatchInfo(scopeInfo.Schema, batchRootDirectory, batchDirectoryName); batchInfo.TryRemoveDirectory(); batchInfo.CreateDirectory(); // Call interceptor var databaseChangesSelectingArgs = new DatabaseChangesSelectingArgs(context, batchInfo.GetDirectoryFullPath(), this.Options.BatchSize, isNew, fromLastTimestamp, toNewTimestamp, connection, transaction); await this.InterceptAsync(databaseChangesSelectingArgs, progress, cancellationToken).ConfigureAwait(false); var cptSyncTable = 0; var currentProgress = context.ProgressPercentage; var schemaTables = scopeInfo.Schema.Tables.SortByDependencies(tab => tab.GetRelations().Select(r => r.GetParentTable())); var lstAllBatchPartInfos = new ConcurrentBag <BatchPartInfo>(); var lstTableChangesSelected = new ConcurrentBag <TableChangesSelected>(); var threadNumberLimits = supportsMultiActiveResultSets ? 16 : 1; if (supportsMultiActiveResultSets) { await schemaTables.ForEachAsync(async syncTable => { if (cancellationToken.IsCancellationRequested) { return; } // tmp count of table for report progress pct cptSyncTable++; List <BatchPartInfo> syncTableBatchPartInfos; TableChangesSelected tableChangesSelected; (context, syncTableBatchPartInfos, tableChangesSelected) = await InternalReadSyncTableChangesAsync( scopeInfo, context, excludingScopeId, syncTable, batchInfo, isNew, fromLastTimestamp, connection, transaction, cancellationToken, progress).ConfigureAwait(false); if (syncTableBatchPartInfos == null) { return; } // We don't report progress if no table changes is empty, to limit verbosity if (tableChangesSelected != null && (tableChangesSelected.Deletes > 0 || tableChangesSelected.Upserts > 0)) { lstTableChangesSelected.Add(tableChangesSelected); } // Add sync table bpi to all bpi syncTableBatchPartInfos.ForEach(bpi => lstAllBatchPartInfos.Add(bpi)); context.ProgressPercentage = currentProgress + (cptSyncTable * 0.2d / scopeInfo.Schema.Tables.Count); }, threadNumberLimits); } else { foreach (var syncTable in schemaTables) { if (cancellationToken.IsCancellationRequested) { continue; } // tmp count of table for report progress pct cptSyncTable++; List <BatchPartInfo> syncTableBatchPartInfos; TableChangesSelected tableChangesSelected; (context, syncTableBatchPartInfos, tableChangesSelected) = await InternalReadSyncTableChangesAsync( scopeInfo, context, excludingScopeId, syncTable, batchInfo, isNew, fromLastTimestamp, connection, transaction, cancellationToken, progress).ConfigureAwait(false); if (syncTableBatchPartInfos == null) { continue; } // We don't report progress if no table changes is empty, to limit verbosity if (tableChangesSelected != null && (tableChangesSelected.Deletes > 0 || tableChangesSelected.Upserts > 0)) { lstTableChangesSelected.Add(tableChangesSelected); } // Add sync table bpi to all bpi syncTableBatchPartInfos.ForEach(bpi => lstAllBatchPartInfos.Add(bpi)); context.ProgressPercentage = currentProgress + (cptSyncTable * 0.2d / scopeInfo.Schema.Tables.Count); } } while (!lstTableChangesSelected.IsEmpty) { if (lstTableChangesSelected.TryTake(out var tableChangesSelected)) { changesSelected.TableChangesSelected.Add(tableChangesSelected); } } // delete all empty batchparts (empty tables) foreach (var bpi in lstAllBatchPartInfos.Where(bpi => bpi.RowsCount <= 0)) { File.Delete(Path.Combine(batchInfo.GetDirectoryFullPath(), bpi.FileName)); } // Generate a good index order to be compliant with previous versions var tmpLstBatchPartInfos = new List <BatchPartInfo>(); foreach (var table in schemaTables) { // get all bpi where count > 0 and ordered by index foreach (var bpi in lstAllBatchPartInfos.Where(bpi => bpi.RowsCount > 0 && bpi.Tables[0].EqualsByName(new BatchPartTableInfo(table.TableName, table.SchemaName))).OrderBy(bpi => bpi.Index).ToArray()) { batchInfo.BatchPartsInfo.Add(bpi); batchInfo.RowsCount += bpi.RowsCount; tmpLstBatchPartInfos.Add(bpi); } } var newBatchIndex = 0; foreach (var bpi in tmpLstBatchPartInfos) { bpi.Index = newBatchIndex; newBatchIndex++; bpi.IsLastBatch = newBatchIndex == tmpLstBatchPartInfos.Count; } //Set the total rows count contained in the batch info batchInfo.EnsureLastBatch(); if (batchInfo.RowsCount <= 0) { var cleanFolder = await this.InternalCanCleanFolderAsync(scopeInfo.Name, context.Parameters, batchInfo, cancellationToken).ConfigureAwait(false); batchInfo.Clear(cleanFolder); } var databaseChangesSelectedArgs = new DatabaseChangesSelectedArgs(context, fromLastTimestamp, toNewTimestamp, batchInfo, changesSelected, connection); await this.InterceptAsync(databaseChangesSelectedArgs, progress, cancellationToken).ConfigureAwait(false); return(context, batchInfo, changesSelected); }
/// <summary> /// Apply changes : Insert / Updates Delete /// the fromScope is local client scope when this method is called from server /// the fromScope is server scope when this method is called from client /// </summary> public virtual async Task <(SyncContext, ChangesApplied)> ApplyChangesAsync(SyncContext context, ScopeInfo fromScope, BatchInfo changes) { ChangeApplicationAction changeApplicationAction; DbTransaction applyTransaction = null; ChangesApplied changesApplied = new ChangesApplied(); DbConnection connection = null; try { using (connection = this.CreateConnection()) { await connection.OpenAsync(); // Create a transaction applyTransaction = connection.BeginTransaction(); // ----------------------------------------------------- // 0) Check if we are in a reinit mode // ----------------------------------------------------- if (context.SyncWay == SyncWay.Download && context.SyncType != SyncType.Normal) { changeApplicationAction = this.ResetInternal(context, connection, applyTransaction, fromScope); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw new SyncException("Rollback during reset tables", context.SyncStage, this.ProviderTypeName, SyncExceptionType.Rollback); } } // ----------------------------------------------------- // 1) Applying deletes. Do not apply deletes if we are in a new database // ----------------------------------------------------- if (!fromScope.IsNewScope) { changeApplicationAction = this.ApplyChangesInternal(context, connection, applyTransaction, fromScope, changes, DmRowState.Deleted, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw new SyncException("Rollback during applying deletes", context.SyncStage, this.ProviderTypeName, SyncExceptionType.Rollback); } } // ----------------------------------------------------- // 1) Applying Inserts // ----------------------------------------------------- changeApplicationAction = this.ApplyChangesInternal(context, connection, applyTransaction, fromScope, changes, DmRowState.Added, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw new SyncException("Rollback during applying inserts", context.SyncStage, this.ProviderTypeName, SyncExceptionType.Rollback); } // ----------------------------------------------------- // 1) Applying updates // ----------------------------------------------------- changeApplicationAction = this.ApplyChangesInternal(context, connection, applyTransaction, fromScope, changes, DmRowState.Modified, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw new SyncException("Rollback during applying updates", context.SyncStage, this.ProviderTypeName, SyncExceptionType.Rollback); } applyTransaction?.Commit(); return(context, changesApplied); } } catch (SyncException se) { throw; } catch (Exception ex) { throw new SyncException(ex, SyncStage.TableChangesApplying, this.ProviderTypeName); } finally { if (applyTransaction != null) { applyTransaction.Dispose(); applyTransaction = null; } if (connection != null && connection.State == ConnectionState.Open) { connection.Close(); } if (changes != null) { changes.Clear(); } } }
/// <summary> /// Apply changes : Insert / Updates Delete /// the fromScope is local client scope when this method is called from server /// the fromScope is server scope when this method is called from client /// </summary> public virtual async Task <(SyncContext, ChangesApplied)> ApplyChangesAsync(SyncContext context, ScopeInfo fromScope, BatchInfo changes) { try { ChangeApplicationAction changeApplicationAction; DbTransaction applyTransaction = null; ChangesApplied changesApplied = new ChangesApplied(); using (var connection = this.CreateConnection()) { try { await connection.OpenAsync(); // Create a transaction applyTransaction = connection.BeginTransaction(); Debug.WriteLine($"----- Applying Changes for Scope \"{fromScope.Name}\" -----"); Debug.WriteLine(""); // ----------------------------------------------------- // 0) Check if we are in a reinit mode // ----------------------------------------------------- if (context.SyncWay == SyncWay.Download && context.SyncType != SyncType.Normal) { changeApplicationAction = this.ResetInternal(context, connection, applyTransaction, fromScope); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw SyncException.CreateRollbackException(context.SyncStage); } } // ----------------------------------------------------- // 1) Applying deletes. Do not apply deletes if we are in a new database // ----------------------------------------------------- if (!fromScope.IsNewScope) { changeApplicationAction = this.ApplyChangesInternal(context, connection, applyTransaction, fromScope, changes, DmRowState.Deleted, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw SyncException.CreateRollbackException(context.SyncStage); } } // ----------------------------------------------------- // 1) Applying Inserts // ----------------------------------------------------- changeApplicationAction = this.ApplyChangesInternal(context, connection, applyTransaction, fromScope, changes, DmRowState.Added, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw SyncException.CreateRollbackException(context.SyncStage); } // ----------------------------------------------------- // 1) Applying updates // ----------------------------------------------------- changeApplicationAction = this.ApplyChangesInternal(context, connection, applyTransaction, fromScope, changes, DmRowState.Modified, changesApplied); // Rollback if (changeApplicationAction == ChangeApplicationAction.Rollback) { throw SyncException.CreateRollbackException(context.SyncStage); } applyTransaction.Commit(); Debug.WriteLine($"--- End Applying Changes for Scope \"{fromScope.Name}\" ---"); Debug.WriteLine(""); } catch (Exception exception) { Debug.WriteLine($"Caught exception while applying changes: {exception}"); throw; } finally { if (applyTransaction != null) { applyTransaction.Dispose(); applyTransaction = null; } if (connection != null && connection.State == ConnectionState.Open) { connection.Close(); } if (changes != null) { changes.Clear(); } } return(context, changesApplied); } } catch (Exception) { throw; } }