diff --git a/src/ByteSync.Client/Business/Synchronizations/MultiUploadZip.cs b/src/ByteSync.Client/Business/Synchronizations/MultiUploadZip.cs index 49c207643..09af56f16 100644 --- a/src/ByteSync.Client/Business/Synchronizations/MultiUploadZip.cs +++ b/src/ByteSync.Client/Business/Synchronizations/MultiUploadZip.cs @@ -12,7 +12,6 @@ public MultiUploadZip(string key, SharedFileDefinition sharedFileDefinition) Key = key; SharedFileDefinition = sharedFileDefinition; - // ZipPath = zipPath; SharedFileDefinition.IsMultiFileZip = true; CreationDate = DateTime.Now; @@ -21,8 +20,6 @@ public MultiUploadZip(string key, SharedFileDefinition sharedFileDefinition) MemoryStream = new MemoryStream(); ZipArchive = new ZipArchive(MemoryStream, ZipArchiveMode.Create, true); - - // ZipArchive = ZipFile.Open(zipPath, ZipArchiveMode.Update); ActionGroupsIds = new List(); @@ -35,8 +32,6 @@ public MultiUploadZip(string key, SharedFileDefinition sharedFileDefinition) public SharedFileDefinition SharedFileDefinition { get; } - // public string ZipPath { get; } - public MemoryStream MemoryStream { get; } public ZipArchive ZipArchive { get; } @@ -54,7 +49,7 @@ public MultiUploadZip(string key, SharedFileDefinition sharedFileDefinition) public bool CanAdd(FileInfo fileInfo, string actionsGroupId) { return - ActionGroupsIds.Count < 1500 && + ActionGroupsIds.Count < 100 && ActionsGroupIdsConcatenationLength + actionsGroupId.Length + 5 < 25000 && Size + fileInfo.Length < 8 * SizeConstants.ONE_MEGA_BYTES; } diff --git a/src/ByteSync.Client/DependencyInjection/Modules/GenericTypesModule.cs b/src/ByteSync.Client/DependencyInjection/Modules/GenericTypesModule.cs index e9f0b236f..6968d2574 100644 --- a/src/ByteSync.Client/DependencyInjection/Modules/GenericTypesModule.cs +++ b/src/ByteSync.Client/DependencyInjection/Modules/GenericTypesModule.cs @@ -12,7 +12,11 @@ public class GenericTypesModule : Module protected override void Load(ContainerBuilder builder) { builder.RegisterGeneric(typeof(SessionInvalidationCachePolicy<,>)) - .As(typeof(ISessionInvalidationSourceCachePolicy<,>)) + .As(typeof(ISessionInvalidationCachePolicy<,>)) + .InstancePerDependency(); + + builder.RegisterGeneric(typeof(PropertyIndexer<,>)) + .As(typeof(IPropertyIndexer<,>)) .InstancePerDependency(); builder.RegisterType>().As>(); diff --git a/src/ByteSync.Client/Interfaces/Controls/Communications/Http/ISynchronizationApiClient.cs b/src/ByteSync.Client/Interfaces/Controls/Communications/Http/ISynchronizationApiClient.cs index 6b401a985..3f29ee22b 100644 --- a/src/ByteSync.Client/Interfaces/Controls/Communications/Http/ISynchronizationApiClient.cs +++ b/src/ByteSync.Client/Interfaces/Controls/Communications/Http/ISynchronizationApiClient.cs @@ -7,7 +7,7 @@ namespace ByteSync.Interfaces.Controls.Communications.Http; public interface ISynchronizationApiClient { - public Task StartSynchronization(SynchronizationStartRequest synchronizationStartRequest); + public Task StartSynchronization(SynchronizationStartRequest synchronizationStartRequest); public Task AssertLocalCopyIsDone(string sessionId, List actionsGroupIds); diff --git a/src/ByteSync.Client/Interfaces/Controls/Synchronizations/ISynchronizationLooper.cs b/src/ByteSync.Client/Interfaces/Controls/Synchronizations/ISynchronizationLooper.cs index fc3a23e95..ec9a3f26b 100644 --- a/src/ByteSync.Client/Interfaces/Controls/Synchronizations/ISynchronizationLooper.cs +++ b/src/ByteSync.Client/Interfaces/Controls/Synchronizations/ISynchronizationLooper.cs @@ -2,9 +2,7 @@ namespace ByteSync.Interfaces.Controls.Synchronizations; -public interface ISynchronizationLooper +public interface ISynchronizationLooper : IAsyncDisposable { - // Task InitializeData(); - Task CloudSessionSynchronizationLoop(); } \ No newline at end of file diff --git a/src/ByteSync.Client/Interfaces/Repositories/IPropertyIndexer.cs b/src/ByteSync.Client/Interfaces/Repositories/IPropertyIndexer.cs new file mode 100644 index 000000000..59bed78cd --- /dev/null +++ b/src/ByteSync.Client/Interfaces/Repositories/IPropertyIndexer.cs @@ -0,0 +1,12 @@ +using DynamicData; + +namespace ByteSync.Interfaces.Repositories; + +public interface IPropertyIndexer + where TObject : notnull + where TIndex : notnull +{ + void Initialize(SourceCache sourceCache, Func indexSelector); + + List GetByIndex(TIndex index); +} \ No newline at end of file diff --git a/src/ByteSync.Client/Interfaces/Repositories/ISessionInvalidationSourceCachePolicy.cs b/src/ByteSync.Client/Interfaces/Repositories/ISessionInvalidationCachePolicy.cs similarity index 52% rename from src/ByteSync.Client/Interfaces/Repositories/ISessionInvalidationSourceCachePolicy.cs rename to src/ByteSync.Client/Interfaces/Repositories/ISessionInvalidationCachePolicy.cs index 0433ee759..e18987def 100644 --- a/src/ByteSync.Client/Interfaces/Repositories/ISessionInvalidationSourceCachePolicy.cs +++ b/src/ByteSync.Client/Interfaces/Repositories/ISessionInvalidationCachePolicy.cs @@ -2,7 +2,9 @@ namespace ByteSync.Interfaces.Repositories; -public interface ISessionInvalidationSourceCachePolicy : IDisposable where TKey : notnull +public interface ISessionInvalidationCachePolicy : IDisposable + where TKey : notnull + where TObject : notnull { void Initialize(SourceCache sourceCache, bool b, bool b1); } \ No newline at end of file diff --git a/src/ByteSync.Client/Repositories/AtomicActionRepository.cs b/src/ByteSync.Client/Repositories/AtomicActionRepository.cs index 2f561e649..ab046fdc4 100644 --- a/src/ByteSync.Client/Repositories/AtomicActionRepository.cs +++ b/src/ByteSync.Client/Repositories/AtomicActionRepository.cs @@ -6,22 +6,23 @@ namespace ByteSync.Repositories; public class AtomicActionRepository : BaseSourceCacheRepository, IAtomicActionRepository { - private readonly ISessionInvalidationSourceCachePolicy _sessionInvalidationSourceCachePolicy; + private readonly ISessionInvalidationCachePolicy _sessionInvalidationCachePolicy; + private readonly IPropertyIndexer _propertyIndexer; - public AtomicActionRepository(ISessionInvalidationSourceCachePolicy sessionInvalidationSourceCachePolicy) + public AtomicActionRepository(ISessionInvalidationCachePolicy sessionInvalidationCachePolicy, + IPropertyIndexer propertyIndexer) { - _sessionInvalidationSourceCachePolicy = sessionInvalidationSourceCachePolicy; - _sessionInvalidationSourceCachePolicy.Initialize(SourceCache, true, true); + _sessionInvalidationCachePolicy = sessionInvalidationCachePolicy; + _sessionInvalidationCachePolicy.Initialize(SourceCache, true, true); + + _propertyIndexer = propertyIndexer; + _propertyIndexer.Initialize(SourceCache, atomicAction => atomicAction.ComparisonItem!); } - + protected override string KeySelector(AtomicAction atomicAction) => atomicAction.AtomicActionId; public List GetAtomicActions(ComparisonItem comparisonItem) { - var result = SourceCache.Items - .Where(atomicAction => Equals(atomicAction.ComparisonItem, comparisonItem)) - .ToList(); - - return result; + return _propertyIndexer.GetByIndex(comparisonItem); } -} \ No newline at end of file +} diff --git a/src/ByteSync.Client/Repositories/BaseSourceCacheRepository.cs b/src/ByteSync.Client/Repositories/BaseSourceCacheRepository.cs index b89cb81d3..38b1e1367 100644 --- a/src/ByteSync.Client/Repositories/BaseSourceCacheRepository.cs +++ b/src/ByteSync.Client/Repositories/BaseSourceCacheRepository.cs @@ -4,7 +4,7 @@ namespace ByteSync.Repositories; public abstract class BaseSourceCacheRepository : IBaseSourceCacheRepository - where TKey : notnull + where TKey : notnull where TObject : notnull { protected BaseSourceCacheRepository() { diff --git a/src/ByteSync.Client/Repositories/InventoryFileRepository.cs b/src/ByteSync.Client/Repositories/InventoryFileRepository.cs index f65f6c5b1..64ffdc463 100644 --- a/src/ByteSync.Client/Repositories/InventoryFileRepository.cs +++ b/src/ByteSync.Client/Repositories/InventoryFileRepository.cs @@ -6,12 +6,12 @@ namespace ByteSync.Repositories; public class InventoryFileRepository : BaseSourceCacheRepository, IInventoryFileRepository { - private readonly ISessionInvalidationSourceCachePolicy _sessionInvalidationSourceCachePolicy; + private readonly ISessionInvalidationCachePolicy _sessionInvalidationCachePolicy; - public InventoryFileRepository(ISessionInvalidationSourceCachePolicy sessionInvalidationSourceCachePolicy) + public InventoryFileRepository(ISessionInvalidationCachePolicy sessionInvalidationCachePolicy) { - _sessionInvalidationSourceCachePolicy = sessionInvalidationSourceCachePolicy; - _sessionInvalidationSourceCachePolicy.Initialize(SourceCache, true, true); + _sessionInvalidationCachePolicy = sessionInvalidationCachePolicy; + _sessionInvalidationCachePolicy.Initialize(SourceCache, true, true); } protected override string KeySelector(InventoryFile inventoryFile) => inventoryFile.FullName; diff --git a/src/ByteSync.Client/Repositories/PathItemRepository.cs b/src/ByteSync.Client/Repositories/PathItemRepository.cs index b96ad81e3..0ae2c118f 100644 --- a/src/ByteSync.Client/Repositories/PathItemRepository.cs +++ b/src/ByteSync.Client/Repositories/PathItemRepository.cs @@ -7,17 +7,17 @@ namespace ByteSync.Repositories; public class PathItemRepository : BaseSourceCacheRepository, IPathItemRepository { - private readonly ISessionInvalidationSourceCachePolicy _sessionInvalidationSourceCachePolicy; + private readonly ISessionInvalidationCachePolicy _sessionInvalidationCachePolicy; - public PathItemRepository(IEnvironmentService environmentService, ISessionInvalidationSourceCachePolicy sessionInvalidationSourceCachePolicy) + public PathItemRepository(IEnvironmentService environmentService, ISessionInvalidationCachePolicy sessionInvalidationCachePolicy) { CurrentMemberPathItems = SourceCache .Connect() .Filter(pathItem => Equals(pathItem.ClientInstanceId, environmentService.ClientInstanceId!)) .AsObservableCache(); - _sessionInvalidationSourceCachePolicy = sessionInvalidationSourceCachePolicy; - _sessionInvalidationSourceCachePolicy.Initialize(SourceCache, true, false); + _sessionInvalidationCachePolicy = sessionInvalidationCachePolicy; + _sessionInvalidationCachePolicy.Initialize(SourceCache, true, false); } protected override string KeySelector(PathItem pathItem) => pathItem.Key; diff --git a/src/ByteSync.Client/Repositories/PropertyIndexer.cs b/src/ByteSync.Client/Repositories/PropertyIndexer.cs new file mode 100644 index 000000000..16bf306a0 --- /dev/null +++ b/src/ByteSync.Client/Repositories/PropertyIndexer.cs @@ -0,0 +1,80 @@ +using ByteSync.Interfaces.Repositories; +using DynamicData; + +namespace ByteSync.Repositories; + +public class PropertyIndexer : IPropertyIndexer + where TObject : notnull + where TIndex : notnull +{ + private Func _indexSelector; + private readonly Dictionary> _cache = new(); + + public void Initialize(SourceCache sourceCache, Func indexSelector) + { + _indexSelector = indexSelector; + + // Initializing the cache with existing objects + foreach (var obj in sourceCache.Items) + { + Update(obj); + } + + // Synchronization with SourceCache + sourceCache.Connect() + .Subscribe(changes => + { + foreach (var change in changes) + { + switch (change.Reason) + { + case ChangeReason.Add: + case ChangeReason.Update: + Update(change.Current); + break; + case ChangeReason.Remove: + Remove(change.Current); + break; + } + } + }); + } + + public void Update(TObject obj) + { + var index = _indexSelector(obj); + + if (!_cache.TryGetValue(index, out var objects)) + { + objects = new List(); + _cache[index] = objects; + } + + var existingObject = objects.FirstOrDefault(o => o.Equals(obj)); + if (existingObject != null) + { + objects.Remove(existingObject); + } + + objects.Add(obj); + } + + public void Remove(TObject obj) + { + var index = _indexSelector(obj); + + if (_cache.TryGetValue(index, out var objects)) + { + objects.RemoveAll(o => o.Equals(obj)); + if (objects.Count == 0) + { + _cache.Remove(index); + } + } + } + + public List GetByIndex(TIndex index) + { + return _cache.TryGetValue(index, out var objects) ? objects : new List(); + } +} diff --git a/src/ByteSync.Client/Repositories/SessionInvalidationCachePolicy.cs b/src/ByteSync.Client/Repositories/SessionInvalidationCachePolicy.cs index a3314e616..d7a88ef97 100644 --- a/src/ByteSync.Client/Repositories/SessionInvalidationCachePolicy.cs +++ b/src/ByteSync.Client/Repositories/SessionInvalidationCachePolicy.cs @@ -6,7 +6,9 @@ namespace ByteSync.Repositories; -public class SessionInvalidationCachePolicy : ISessionInvalidationSourceCachePolicy where TKey : notnull +public class SessionInvalidationCachePolicy : ISessionInvalidationCachePolicy + where TKey : notnull + where TObject : notnull { private readonly ISessionService _sessionService; private IDisposable? _sessionSubscription; diff --git a/src/ByteSync.Client/Repositories/SessionMemberRepository.cs b/src/ByteSync.Client/Repositories/SessionMemberRepository.cs index a6b16e4eb..b59422c8a 100644 --- a/src/ByteSync.Client/Repositories/SessionMemberRepository.cs +++ b/src/ByteSync.Client/Repositories/SessionMemberRepository.cs @@ -14,10 +14,10 @@ public class SessionMemberRepository : BaseSourceCacheRepository _sortedSessionMembersList; private readonly ReadOnlyObservableCollection _sortedOtherSessionMembersList; - private readonly ISessionInvalidationSourceCachePolicy _sessionInvalidationSourceCachePolicy; + private readonly ISessionInvalidationCachePolicy _sessionInvalidationCachePolicy; public SessionMemberRepository(IConnectionService connectionService, - ISessionInvalidationSourceCachePolicy sessionInvalidationSourceCachePolicy) + ISessionInvalidationCachePolicy sessionInvalidationCachePolicy) { _connectionService = connectionService; @@ -34,8 +34,8 @@ public SessionMemberRepository(IConnectionService connectionService, IsCurrentUserFirstSessionMemberObservable .Subscribe(value => IsCurrentUserFirstSessionMemberCurrentValue = value); - _sessionInvalidationSourceCachePolicy = sessionInvalidationSourceCachePolicy; - _sessionInvalidationSourceCachePolicy.Initialize(SourceCache, true, false); + _sessionInvalidationCachePolicy = sessionInvalidationCachePolicy; + _sessionInvalidationCachePolicy.Initialize(SourceCache, true, false); } protected override string KeySelector(SessionMemberInfo sessionMemberInfo) => sessionMemberInfo.ClientInstanceId; diff --git a/src/ByteSync.Client/Repositories/SharedActionsGroupRepository.cs b/src/ByteSync.Client/Repositories/SharedActionsGroupRepository.cs index 356370959..8b59aee56 100644 --- a/src/ByteSync.Client/Repositories/SharedActionsGroupRepository.cs +++ b/src/ByteSync.Client/Repositories/SharedActionsGroupRepository.cs @@ -9,14 +9,14 @@ namespace ByteSync.Repositories; public class SharedActionsGroupRepository : BaseSourceCacheRepository, ISharedActionsGroupRepository { - private readonly ISessionInvalidationSourceCachePolicy _sessionInvalidationSourceCachePolicy; + private readonly ISessionInvalidationCachePolicy _sessionInvalidationCachePolicy; - public SharedActionsGroupRepository(ISessionInvalidationSourceCachePolicy sessionInvalidationSourceCachePolicy) + public SharedActionsGroupRepository(ISessionInvalidationCachePolicy sessionInvalidationCachePolicy) { OrganizedSharedActionsGroups = new List(); - _sessionInvalidationSourceCachePolicy = sessionInvalidationSourceCachePolicy; - _sessionInvalidationSourceCachePolicy.Initialize(SourceCache, true, true); + _sessionInvalidationCachePolicy = sessionInvalidationCachePolicy; + _sessionInvalidationCachePolicy.Initialize(SourceCache, true, true); } protected override string KeySelector(SharedActionsGroup sharedAtomicAction) => sharedAtomicAction.ActionsGroupId; diff --git a/src/ByteSync.Client/Repositories/SharedAtomicActionRepository.cs b/src/ByteSync.Client/Repositories/SharedAtomicActionRepository.cs index 255c5ccaf..5d00f3839 100644 --- a/src/ByteSync.Client/Repositories/SharedAtomicActionRepository.cs +++ b/src/ByteSync.Client/Repositories/SharedAtomicActionRepository.cs @@ -6,12 +6,12 @@ namespace ByteSync.Repositories; public class SharedAtomicActionRepository : BaseSourceCacheRepository, ISharedAtomicActionRepository { - private readonly ISessionInvalidationSourceCachePolicy _sessionInvalidationSourceCachePolicy; + private readonly ISessionInvalidationCachePolicy _sessionInvalidationCachePolicy; - public SharedAtomicActionRepository(ISessionInvalidationSourceCachePolicy sessionInvalidationSourceCachePolicy) + public SharedAtomicActionRepository(ISessionInvalidationCachePolicy sessionInvalidationCachePolicy) { - _sessionInvalidationSourceCachePolicy = sessionInvalidationSourceCachePolicy; - _sessionInvalidationSourceCachePolicy.Initialize(SourceCache, true, true); + _sessionInvalidationCachePolicy = sessionInvalidationCachePolicy; + _sessionInvalidationCachePolicy.Initialize(SourceCache, true, true); } protected override string KeySelector(SharedAtomicAction sharedAtomicAction) => sharedAtomicAction.AtomicActionId; diff --git a/src/ByteSync.Client/Repositories/SynchronizationRuleRepository.cs b/src/ByteSync.Client/Repositories/SynchronizationRuleRepository.cs index e0923e3ae..09317320d 100644 --- a/src/ByteSync.Client/Repositories/SynchronizationRuleRepository.cs +++ b/src/ByteSync.Client/Repositories/SynchronizationRuleRepository.cs @@ -5,12 +5,12 @@ namespace ByteSync.Repositories; public class SynchronizationRuleRepository : BaseSourceCacheRepository, ISynchronizationRuleRepository { - private readonly ISessionInvalidationSourceCachePolicy _sessionInvalidationSourceCachePolicy; + private readonly ISessionInvalidationCachePolicy _sessionInvalidationCachePolicy; - public SynchronizationRuleRepository(ISessionInvalidationSourceCachePolicy sessionInvalidationSourceCachePolicy) + public SynchronizationRuleRepository(ISessionInvalidationCachePolicy sessionInvalidationCachePolicy) { - _sessionInvalidationSourceCachePolicy = sessionInvalidationSourceCachePolicy; - _sessionInvalidationSourceCachePolicy.Initialize(SourceCache, true, true); + _sessionInvalidationCachePolicy = sessionInvalidationCachePolicy; + _sessionInvalidationCachePolicy.Initialize(SourceCache, true, true); } protected override string KeySelector(SynchronizationRule synchronizationRule) => synchronizationRule.SynchronizationRuleId; diff --git a/src/ByteSync.Client/Services/Actions/SharedActionsGroupComputer.cs b/src/ByteSync.Client/Services/Actions/SharedActionsGroupComputer.cs index 39c69c302..9c1af3c2a 100644 --- a/src/ByteSync.Client/Services/Actions/SharedActionsGroupComputer.cs +++ b/src/ByteSync.Client/Services/Actions/SharedActionsGroupComputer.cs @@ -13,40 +13,48 @@ public class SharedActionsGroupComputer : ISharedActionsGroupComputer { private readonly ISharedAtomicActionRepository _sharedAtomicActionRepository; private readonly ISharedActionsGroupRepository _sharedActionsGroupRepository; - - private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + + private List _buffer; + private int _counter; + private readonly object _lock = new(); public SharedActionsGroupComputer(ISharedAtomicActionRepository sharedAtomicActionRepository, ISharedActionsGroupRepository sharedActionsGroupRepository) { _sharedAtomicActionRepository = sharedAtomicActionRepository; _sharedActionsGroupRepository = sharedActionsGroupRepository; + _buffer = new List(); - Counter = 0; + _counter = 0; } - private int Counter { get; set; } - public async Task ComputeSharedActionsGroups() { var sharedAtomicActions = _sharedAtomicActionRepository.Elements; - - var tasks = new List(); + var dictionary = sharedAtomicActions.GroupBy(saa => saa.PathIdentity) .ToDictionary(g => g.Key, g => g.ToList()); - foreach (KeyValuePair> pair in dictionary) + await Parallel.ForEachAsync(dictionary, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount * 2 }, (pair, _) => { - tasks.Add(Task.Run(() => ComputeGroups_CopyContentAndDate(pair.Value))); - tasks.Add(Task.Run(() => ComputeGroups_CopyContent(pair.Value))); - tasks.Add(Task.Run(() => ComputeGroups_CopyDate(pair.Value))); - tasks.Add(Task.Run(() => ComputeGroups_Create(pair.Value))); - tasks.Add(Task.Run(() => ComputeGroups_Delete(pair.Value))); - } + ComputeGroups_CopyContentAndDate(pair.Value); + ComputeGroups_CopyContent(pair.Value); + ComputeGroups_CopyDate(pair.Value); + ComputeGroups_Create(pair.Value); + ComputeGroups_Delete(pair.Value); + return ValueTask.CompletedTask; + }); - await Task.WhenAll(tasks); + lock (_lock) + { + if (_buffer.Count > 0) + { + _sharedActionsGroupRepository.AddOrUpdate(_buffer); + _buffer.Clear(); + } + } } - private async Task ComputeGroups_CopyContentAndDate(List atomicActions) + private void ComputeGroups_CopyContentAndDate(List atomicActions) { var sharedAtomicActions = GetSharedAtomicActions(atomicActions, ActionOperatorTypes.SynchronizeContentAndDate); @@ -55,7 +63,7 @@ private async Task ComputeGroups_CopyContentAndDate(List ato var sharedActionsGroups = new List(); foreach (var group in groups) { - var sharedActionsGroup = await BuildSharedActionsGroup(ActionOperatorTypes.SynchronizeContentAndDate); + var sharedActionsGroup = BuildSharedActionsGroup(ActionOperatorTypes.SynchronizeContentAndDate); sharedActionsGroup.Source = group.First().Source; sharedActionsGroup.Targets.AddAll(group.Select(saa => saa.Target!)); @@ -78,7 +86,7 @@ private async Task ComputeGroups_CopyContentAndDate(List ato AddSharedActionsGroups(sharedActionsGroups); } - private async Task ComputeGroups_CopyContent(List atomicActions) + private void ComputeGroups_CopyContent(List atomicActions) { var sharedAtomicActions = GetSharedAtomicActions(atomicActions, ActionOperatorTypes.SynchronizeContentOnly); @@ -87,7 +95,7 @@ private async Task ComputeGroups_CopyContent(List atomicActi var sharedActionsGroups = new List(); foreach (var group in groups) { - var sharedActionsGroup = await BuildSharedActionsGroup(ActionOperatorTypes.SynchronizeContentOnly); + var sharedActionsGroup = BuildSharedActionsGroup(ActionOperatorTypes.SynchronizeContentOnly); sharedActionsGroup.Source = group.First().Source; sharedActionsGroup.Targets.AddAll(group.Select(saa => saa.Target!)); @@ -104,7 +112,7 @@ private async Task ComputeGroups_CopyContent(List atomicActi AddSharedActionsGroups(sharedActionsGroups); } - private async Task ComputeGroups_CopyDate(List atomicActions) + private void ComputeGroups_CopyDate(List atomicActions) { var sharedAtomicActions = GetSharedAtomicActions(atomicActions, ActionOperatorTypes.SynchronizeDate); @@ -114,7 +122,7 @@ private async Task ComputeGroups_CopyDate(List atomicActions sharedAtomicActions.GroupBy(aa => aa.Source!) .ToDictionary(g => g.Key, g => g.ToList())) { - var sharedActionsGroup = await BuildSharedActionsGroup(ActionOperatorTypes.SynchronizeDate); + var sharedActionsGroup = BuildSharedActionsGroup(ActionOperatorTypes.SynchronizeDate); sharedActionsGroup.Source = pair.Key; sharedActionsGroup.Targets.AddAll(pair.Value.Select(saa => saa.Target!)); @@ -131,17 +139,17 @@ private async Task ComputeGroups_CopyDate(List atomicActions AddSharedActionsGroups(sharedActionsGroups); } - private Task ComputeGroups_Create(List atomicActions) + private void ComputeGroups_Create(List atomicActions) { - return DoComputeGroups_CreateDelete(ActionOperatorTypes.Create, atomicActions); + DoComputeGroups_CreateDelete(ActionOperatorTypes.Create, atomicActions); } - private Task ComputeGroups_Delete(List atomicActions) + private void ComputeGroups_Delete(List atomicActions) { - return DoComputeGroups_CreateDelete(ActionOperatorTypes.Delete, atomicActions); + DoComputeGroups_CreateDelete(ActionOperatorTypes.Delete, atomicActions); } - private async Task DoComputeGroups_CreateDelete(ActionOperatorTypes operatorType, List atomicActions) + private void DoComputeGroups_CreateDelete(ActionOperatorTypes operatorType, List atomicActions) { if (!operatorType.In(ActionOperatorTypes.Create, ActionOperatorTypes.Delete)) { @@ -155,7 +163,7 @@ private async Task DoComputeGroups_CreateDelete(ActionOperatorTypes operatorType return; } - var sharedActionsGroup = await BuildSharedActionsGroup(operatorType); + var sharedActionsGroup = BuildSharedActionsGroup(operatorType); sharedActionsGroup.Source = null; sharedActionsGroup.Targets.AddAll(sharedAtomicActions.Select(saa => saa.Target!)); @@ -175,24 +183,20 @@ private IEnumerable GetSharedAtomicActions(List BuildSharedActionsGroup(ActionOperatorTypes operatorType) + private SharedActionsGroup BuildSharedActionsGroup(ActionOperatorTypes operatorType) { - await _semaphore.WaitAsync(); - try - { - var group = new SharedActionsGroup(); - - Counter += 1; + var group = new SharedActionsGroup(); - group.Operator = operatorType; - group.ActionsGroupId = $"AGID_{Counter}"; + group.Operator = operatorType; + group.ActionsGroupId = GenerateUniqueId(); - return group; - } - finally - { - _semaphore.Release(); - } + return group; + } + + private string GenerateUniqueId() + { + var newCounter = Interlocked.Increment(ref _counter); + return $"AGID_{newCounter}"; } private void AffectSharedActionsGroupId(SharedActionsGroup sharedActionsGroup, List sharedAtomicActions) @@ -205,21 +209,38 @@ private void AffectSharedActionsGroupId(SharedActionsGroup sharedActionsGroup, L private void AddSharedActionsGroups(List sharedActionsGroups) { - if (sharedActionsGroups.Count > 0) + lock (_lock) { - _sharedActionsGroupRepository.AddOrUpdate(sharedActionsGroups); + _buffer.AddAll(sharedActionsGroups); + + CheckBuffer(); } + } - + private void AddSharedActionsGroup(SharedActionsGroup sharedActionsGroup) { - _sharedActionsGroupRepository.AddOrUpdate(sharedActionsGroup); + lock (_lock) + { + _buffer.Add(sharedActionsGroup); + + CheckBuffer(); + } + } + + private void CheckBuffer() + { + if (_buffer.Count > 500) + { + _sharedActionsGroupRepository.AddOrUpdate(_buffer); + _buffer.Clear(); + } } - + private static List> GetCopyGroups(IEnumerable sharedAtomicActions, bool isContentAndDate) { var root = sharedAtomicActions - .Where(saa => saa.Target != null) // La target peut être null dans certains cas avec les règles de synchronisation + .Where(saa => saa.Target != null) // The target may be null in some cases with synchronization rules .GroupBy(saa => saa.Source!) .Select(x => new { diff --git a/src/ByteSync.Client/Services/Communications/Api/SynchronizationApiClient.cs b/src/ByteSync.Client/Services/Communications/Api/SynchronizationApiClient.cs index e8eeee0fa..05f63531a 100644 --- a/src/ByteSync.Client/Services/Communications/Api/SynchronizationApiClient.cs +++ b/src/ByteSync.Client/Services/Communications/Api/SynchronizationApiClient.cs @@ -17,14 +17,12 @@ public SynchronizationApiClient(IApiInvoker apiInvoker, ILogger StartSynchronization(SynchronizationStartRequest synchronizationStartRequest) + public async Task StartSynchronization(SynchronizationStartRequest synchronizationStartRequest) { try { - var result = await _apiInvoker.PostAsync($"session/{synchronizationStartRequest.SessionId}/synchronization/start", + await _apiInvoker.PostAsync($"session/{synchronizationStartRequest.SessionId}/synchronization/start", synchronizationStartRequest); - - return result; } catch (Exception ex) { diff --git a/src/ByteSync.Client/Services/Communications/PushReceivers/FileTransferPushReceiver.cs b/src/ByteSync.Client/Services/Communications/PushReceivers/FileTransferPushReceiver.cs index d19f52d25..664369be2 100644 --- a/src/ByteSync.Client/Services/Communications/PushReceivers/FileTransferPushReceiver.cs +++ b/src/ByteSync.Client/Services/Communications/PushReceivers/FileTransferPushReceiver.cs @@ -81,7 +81,9 @@ private async void OnUploadFinished(FileTransferPush fileTransferPush) try { - _logger.LogInformation("OnUploadFinished: {SharedFileType}", fileTransferPush.SharedFileDefinition.SharedFileType); + _logger.LogInformation("OnUploadFinished: {SharedFileId} ({SharedFileType})", + fileTransferPush.SharedFileDefinition.Id, + fileTransferPush.SharedFileDefinition.SharedFileType); if (_sessionService.CurrentSession?.SessionId == fileTransferPush.SessionId) { @@ -100,7 +102,15 @@ private async void OnUploadFinished(FileTransferPush fileTransferPush) if (afterTransferSharedFile != null) { - await afterTransferSharedFile.OnUploadFinishedError(sharedFileDefinition, ex); + try + { + await afterTransferSharedFile.OnUploadFinishedError(sharedFileDefinition, ex); + } + catch (Exception ex2) + { + _logger.LogError(ex2, "CloudSessionManager.OnUploadFinishedError sharedFileDefinition.Id :{id}, uploadedBy:{UploaderClientInstanceId} ", + fileTransferPush.SharedFileDefinition.Id, fileTransferPush.SharedFileDefinition.ClientInstanceId); + } } } } diff --git a/src/ByteSync.Client/Services/Communications/PushReceivers/SynchronizationPushReceiver.cs b/src/ByteSync.Client/Services/Communications/PushReceivers/SynchronizationPushReceiver.cs index 0ca083e14..0bf67f820 100644 --- a/src/ByteSync.Client/Services/Communications/PushReceivers/SynchronizationPushReceiver.cs +++ b/src/ByteSync.Client/Services/Communications/PushReceivers/SynchronizationPushReceiver.cs @@ -13,7 +13,6 @@ public class SynchronizationPushReceiver : IPushReceiver private readonly ISynchronizationService _synchronizationService; private readonly ILogger _logger; - public SynchronizationPushReceiver(IHubPushHandler2 hubPushHandler2, ISessionService sessionService, ISynchronizationService synchronizationService, ILogger logger) { @@ -30,7 +29,7 @@ public SynchronizationPushReceiver(IHubPushHandler2 hubPushHandler2, ISessionSer { if (latestSession != null && latestSession.SessionId.Equals(synchronization.SessionId)) { - _logger.LogInformation("The Data Synchronization has been started by another client ({@StartedBy}). " + + _logger.LogInformation("The Data Synchronization has been started by client ({@StartedBy}). " + "Retrieving data...", synchronization.StartedBy); _synchronizationService.OnSynchronizationStarted(synchronization); diff --git a/src/ByteSync.Client/Services/Synchronizations/SynchronizationActionRemoteUploader.cs b/src/ByteSync.Client/Services/Synchronizations/SynchronizationActionRemoteUploader.cs index 70fdaf13c..791613964 100644 --- a/src/ByteSync.Client/Services/Synchronizations/SynchronizationActionRemoteUploader.cs +++ b/src/ByteSync.Client/Services/Synchronizations/SynchronizationActionRemoteUploader.cs @@ -23,8 +23,7 @@ public class SynchronizationActionRemoteUploader : ISynchronizationActionRemoteU private readonly ILogger _logger; private MultiUploadZip? _currentMultiUploadZip; - - + public SynchronizationActionRemoteUploader(ICloudProxy connectionManager, ISessionService sessionService, IDeltaManager deltaManager, ISynchronizationActionServerInformer synchronizationActionServerInformer, IFileUploaderFactory fileUploaderFactory, ILogger logger) @@ -52,8 +51,6 @@ public SynchronizationActionRemoteUploader(ICloudProxy connectionManager, ISessi private ByteSyncEndpoint CurrentEndPoint => _connectionManager.CurrentEndPoint; - // public AbstractSession Session => _sessionService.SessionObservable.Value!; - public async Task UploadForRemote(SharedActionsGroup sharedActionsGroup) { string? localFullName = null; @@ -113,7 +110,7 @@ public async Task UploadForRemote(SharedActionsGroup sharedActionsGroup) { await MultiZipPrepareSemaphore.WaitAsync(); - // on crée le currentMultiUploadZip + // create the currentMultiUploadZip var sharedFileDefinition = BuildSharedFileDefinition(sharedFileType); _logger.LogInformation("Creating MultiUploadZip with id:{MultiZipId} for grouped upload (delta:{isDelta})", @@ -184,7 +181,7 @@ public Task Abort() { return Task.Run(() => { - // L'abandon de la synchro a été demandé, on doit libérer les ressources + // The synchronization has been requested to be abandoned; we must free up resources if (_currentMultiUploadZip != null) { try @@ -206,7 +203,7 @@ public Task Abort() private bool IsFileUploadableWithMultiUpload(FileInfo fileInfo) { - return fileInfo.Length <= 200 * SizeConstants.ONE_KILO_BYTES; + return fileInfo.Length <= 250 * SizeConstants.ONE_KILO_BYTES; } private async Task CloseAndUploadCurrentMultiZip() diff --git a/src/ByteSync.Client/Services/Synchronizations/SynchronizationLooper.cs b/src/ByteSync.Client/Services/Synchronizations/SynchronizationLooper.cs index 7899d6a65..da9bf10dd 100644 --- a/src/ByteSync.Client/Services/Synchronizations/SynchronizationLooper.cs +++ b/src/ByteSync.Client/Services/Synchronizations/SynchronizationLooper.cs @@ -1,4 +1,6 @@ -using ByteSync.Business.Arguments; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using ByteSync.Business.Arguments; using ByteSync.Common.Business.Sessions; using ByteSync.Common.Business.Sessions.Cloud; using ByteSync.Common.Business.Sessions.Local; @@ -16,22 +18,33 @@ public class SynchronizationLooper : ISynchronizationLooper private readonly ISynchronizationActionHandler _synchronizationActionHandler; private readonly ISynchronizationApiClient _synchronizationApiClient; private readonly ISharedActionsGroupRepository _sharedActionsGroupRepository; + private readonly ISynchronizationService _synchronizationService; private readonly ILogger _logger; + + private readonly CompositeDisposable _disposables = new(); public SynchronizationLooper(ISessionService sessionService, ISessionMemberService sessionMemberService, ISynchronizationActionHandler synchronizationActionHandler, ISynchronizationApiClient synchronizationApiClient, ISharedActionsGroupRepository sharedActionsGroupRepository, - ILogger logger) + ISynchronizationService synchronizationService, ILogger logger) { _sessionService = sessionService; _sessionMemberService = sessionMemberService; _synchronizationActionHandler = synchronizationActionHandler; _synchronizationApiClient = synchronizationApiClient; _sharedActionsGroupRepository = sharedActionsGroupRepository; + _synchronizationService = synchronizationService; _logger = logger; Session = null; - _sessionService.SessionObservable.Subscribe(value => Session = value); + + var subscription = _sessionService.SessionObservable + .Subscribe(value => Session = value); + _disposables.Add(subscription); + + subscription = _synchronizationService.SynchronizationProcessData.SynchronizationAbortRequest.DistinctUntilChanged() + .Subscribe(synchronizationAbortRequest => IsSynchronizationAbortRequested = synchronizationAbortRequest != null); + _disposables.Add(subscription); } private AbstractSession? Session { get; set; } @@ -109,4 +122,11 @@ public async Task CloudSessionSynchronizationLoop() _logger.LogError(ex, "Error while informing server"); } } + + public ValueTask DisposeAsync() + { + _disposables.Dispose(); + + return ValueTask.CompletedTask; + } } \ No newline at end of file diff --git a/src/ByteSync.Client/Services/Synchronizations/SynchronizationService.cs b/src/ByteSync.Client/Services/Synchronizations/SynchronizationService.cs index 4d310085b..6c8a8df5b 100644 --- a/src/ByteSync.Client/Services/Synchronizations/SynchronizationService.cs +++ b/src/ByteSync.Client/Services/Synchronizations/SynchronizationService.cs @@ -32,7 +32,6 @@ public SynchronizationService(ISessionService sessionService, ISessionMemberServ _synchronizationLooperFactory = synchronizationLooperFactory; _timeTrackingCache = timeTrackingCache; _logger = logger; - SynchronizationProcessData = new SynchronizationProcessData(); @@ -40,8 +39,12 @@ public SynchronizationService(ISessionService sessionService, ISessionMemberServ .Where(tuple => tuple is { First: not null, Second: true }) .Subscribe(_ => { - var synchronizationLooper = _synchronizationLooperFactory.CreateSynchronizationLooper(); - synchronizationLooper.CloudSessionSynchronizationLoop(); + Task.Run(async () => + { + var synchronizationLooper = _synchronizationLooperFactory.CreateSynchronizationLooper(); + await synchronizationLooper.CloudSessionSynchronizationLoop(); + await synchronizationLooper.DisposeAsync(); + }); }); _sessionService.SessionStatusObservable diff --git a/src/ByteSync.Client/Services/Synchronizations/SynchronizationStarter.cs b/src/ByteSync.Client/Services/Synchronizations/SynchronizationStarter.cs index d2e8d3236..6d848a3e7 100644 --- a/src/ByteSync.Client/Services/Synchronizations/SynchronizationStarter.cs +++ b/src/ByteSync.Client/Services/Synchronizations/SynchronizationStarter.cs @@ -92,16 +92,8 @@ private async Task StartCloudSessionSynchronization(CloudSession cloudSession) SessionId = cloudSession.SessionId, ActionsGroupDefinitions = actionsGroupDefinitions, }; - var synchronization = await _synchronizationApiClient.StartSynchronization(synchronizationStartRequest); - - try - { - await _synchronizationService.OnSynchronizationStarted(synchronization); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during synchronization loop"); - } + + await _synchronizationApiClient.StartSynchronization(synchronizationStartRequest); } private async Task UploadSynchronizationStartData(CloudSession cloudSession, SharedSynchronizationStartData sharedSynchronizationStartData) diff --git a/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationMainViewModel.cs b/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationMainViewModel.cs index cda7090d9..e17a893eb 100644 --- a/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationMainViewModel.cs +++ b/src/ByteSync.Client/ViewModels/Sessions/Synchronizations/SynchronizationMainViewModel.cs @@ -1,7 +1,6 @@ using System.Collections.ObjectModel; using System.Reactive; using System.Reactive.Linq; -using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Mixins; using ByteSync.Assets.Resources; @@ -12,12 +11,10 @@ using ByteSync.Business.Synchronizations; using ByteSync.Common.Business.Sessions.Cloud; using ByteSync.Common.Business.Synchronizations; -using ByteSync.Common.Helpers; using ByteSync.Interfaces; using ByteSync.Interfaces.Controls.Synchronizations; using ByteSync.Interfaces.Controls.TimeTracking; using ByteSync.Interfaces.Dialogs; -using ByteSync.Interfaces.EventsHubs; using ByteSync.Interfaces.Repositories; using ByteSync.Interfaces.Services.Sessions; using ByteSync.ViewModels.Misc; @@ -120,6 +117,14 @@ public SynchronizationMainViewModel(ISessionService sessionService, ILocalizatio .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(tuple => OnSynchronizationStarted(tuple.First!)) .DisposeWith(disposables); + + _synchronizationService.SynchronizationProcessData.SynchronizationDataTransmitted + .CombineLatest(_synchronizationService.SynchronizationProcessData.SynchronizationAbortRequest, + _synchronizationService.SynchronizationProcessData.SynchronizationEnd) + .Where(tuple => tuple.Second == null && tuple.Third == null) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(tuple => OnSynchronizationDataTransmitted(tuple.First)) + .DisposeWith(disposables); _synchronizationService.SynchronizationProcessData.SynchronizationAbortRequest.DistinctUntilChanged() .Where(synchronizationAbortRequest => synchronizationAbortRequest != null) @@ -214,14 +219,6 @@ public SynchronizationMainViewModel(ISessionService sessionService, ILocalizatio IsMainCheckVisible = false; } - private bool ComputeCanStartSynchronization(bool isSyncStarted, bool isCloudSession, bool isCloudSessionCreatedByMe, - ObservableCollection? actions, int actionsCount, bool isInventoryError) - { - var result = !isSyncStarted && (!isCloudSession || isCloudSessionCreatedByMe) && actions != null && actionsCount > 0 && !isInventoryError; - - return result; - } - private bool ComputeShowStartSynchronizationObservable(bool isSynchronizationRunning, bool isCloudSession, bool isSessionCreatedByMe, bool hasSynchronizationStarted, bool isProfileSessionSynchronization, bool hasSessionBeenRestarted) { @@ -366,7 +363,6 @@ private async Task AbortSynchronization() private void OnSynchronizationStarted(Synchronization synchronizationStart) { StartDateTime = synchronizationStart.Started.LocalDateTime; - TreatableActions = _synchronizationService.SynchronizationProcessData.TotalActionsToProcess; MainStatus = Resources.SynchronizationMain_SynchronizationRunning; @@ -381,6 +377,10 @@ private void OnSynchronizationStarted(Synchronization synchronizationStart) IsMainProgressRingVisible = true; } + private void OnSynchronizationDataTransmitted(bool tupleFirst) + { + TreatableActions = _synchronizationService.SynchronizationProcessData.TotalActionsToProcess; + } private void OnSynchronizationAbortRequested(SynchronizationAbortRequest synchronizationAbortRequest) { diff --git a/src/ByteSync.Functions/Helpers/Middlewares/JwtMiddleware.cs b/src/ByteSync.Functions/Helpers/Middlewares/JwtMiddleware.cs index b66d62eea..e5ddafdaa 100644 --- a/src/ByteSync.Functions/Helpers/Middlewares/JwtMiddleware.cs +++ b/src/ByteSync.Functions/Helpers/Middlewares/JwtMiddleware.cs @@ -5,6 +5,7 @@ using ByteSync.Functions.Http; using ByteSync.ServerCommon.Business.Auth; using ByteSync.ServerCommon.Business.Settings; +using ByteSync.ServerCommon.Exceptions; using ByteSync.ServerCommon.Interfaces.Repositories; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; @@ -48,14 +49,16 @@ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next { var tokenHandler = new JwtSecurityTokenHandler(); + bool isOK = false; + Client? client = null; try { var claims = ValidateToken(tokenHandler, token); - var client = await GetClient(claims); + client = await GetClient(claims); context.Items.Add(AuthConstants.FUNCTION_CONTEXT_CLIENT, client!); - - await BeginScopeAndGoNext(context, next, client); + + isOK = true; } catch (SecurityTokenExpiredException ex) { @@ -67,6 +70,11 @@ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next _logger.LogError(ex, "Error validating token"); await HandleTokenError(context, "Invalid token"); } + + if (isOK) + { + await BeginScopeAndGoNext(context, next, client!); + } } else { @@ -166,12 +174,13 @@ private TokenValidationParameters BuildTokenValidationParameters() return client; } - private static async Task HandleTokenError(FunctionContext context, string message) + private static async Task HandleTokenError(FunctionContext context, string message, + HttpStatusCode httpStatusCode = HttpStatusCode.Unauthorized) { var httpReqData = await context.GetHttpRequestDataAsync(); if (httpReqData != null) { - var newHttpResponse = httpReqData.CreateResponse(HttpStatusCode.Unauthorized); + var newHttpResponse = httpReqData.CreateResponse(httpStatusCode); await newHttpResponse.WriteAsJsonAsync(new { ResponseStatus = message }, newHttpResponse.StatusCode); context.GetInvocationResult().Value = newHttpResponse; } diff --git a/src/ByteSync.Functions/Http/SynchronizationFunction.cs b/src/ByteSync.Functions/Http/SynchronizationFunction.cs index 2dbc205ff..e157034d9 100644 --- a/src/ByteSync.Functions/Http/SynchronizationFunction.cs +++ b/src/ByteSync.Functions/Http/SynchronizationFunction.cs @@ -27,10 +27,10 @@ public async Task StartSynchronization( var client = FunctionHelper.GetClientFromContext(executionContext); var synchronizationStartRequest = await FunctionHelper.DeserializeRequestBody(req); - var result = await _synchronizationService.StartSynchronization(sessionId, client, synchronizationStartRequest.ActionsGroupDefinitions); + await _synchronizationService.StartSynchronization(sessionId, client, synchronizationStartRequest.ActionsGroupDefinitions); var response = req.CreateResponse(); - await response.WriteAsJsonAsync(result, HttpStatusCode.OK); + response.StatusCode = HttpStatusCode.OK; return response; } diff --git a/src/ByteSync.Functions/Program.cs b/src/ByteSync.Functions/Program.cs index 03da0a65f..108c294fd 100644 --- a/src/ByteSync.Functions/Program.cs +++ b/src/ByteSync.Functions/Program.cs @@ -8,7 +8,6 @@ using ByteSync.ServerCommon.Business.Settings; using ByteSync.ServerCommon.Commands.Inventories; using ByteSync.ServerCommon.Helpers; -using ByteSync.ServerCommon.Misc; using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -88,24 +87,14 @@ services.Configure(configuration.GetSection("Redis")); services.Configure(configuration.GetSection("BlobStorage")); services.Configure(configuration.GetSection("SignalR")); - services.Configure(configuration.GetSection("CosmosDb")); services.Configure(appSettingsSection); var appSettings = appSettingsSection.Get(); services.AddClaimAuthorization(); services.AddJwtAuthentication(appSettings!.Secret); - - services.AddDbContext(); services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(Assembly.GetExecutingAssembly(), typeof(AddPathItemRequest).Assembly)); }) .Build(); -using (var scope = host.Services.CreateScope()) -{ - var services = scope.ServiceProvider; - var dbContext = services.GetRequiredService(); - await dbContext.InitializeCosmosDb(); -} - host.Run(); diff --git a/src/ByteSync.ServerCommon/Business/Settings/CosmosDbSettings.cs b/src/ByteSync.ServerCommon/Business/Settings/CosmosDbSettings.cs deleted file mode 100644 index 96a64f1b0..000000000 --- a/src/ByteSync.ServerCommon/Business/Settings/CosmosDbSettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ByteSync.ServerCommon.Business.Settings; - -public class CosmosDbSettings -{ - public string ConnectionString { get; set; } = ""; - - public string DatabaseName { get; set; } = ""; -} \ No newline at end of file diff --git a/src/ByteSync.ServerCommon/ByteSync.ServerCommon.csproj b/src/ByteSync.ServerCommon/ByteSync.ServerCommon.csproj index af56b40c5..8a813b1cd 100644 --- a/src/ByteSync.ServerCommon/ByteSync.ServerCommon.csproj +++ b/src/ByteSync.ServerCommon/ByteSync.ServerCommon.csproj @@ -14,7 +14,6 @@ - diff --git a/src/ByteSync.ServerCommon/Commands/Inventories/SetLocalInventoryStatusCommandHandler.cs b/src/ByteSync.ServerCommon/Commands/Inventories/SetLocalInventoryStatusCommandHandler.cs index 0e2e1b403..98bfac2ff 100644 --- a/src/ByteSync.ServerCommon/Commands/Inventories/SetLocalInventoryStatusCommandHandler.cs +++ b/src/ByteSync.ServerCommon/Commands/Inventories/SetLocalInventoryStatusCommandHandler.cs @@ -33,30 +33,22 @@ public async Task Handle(SetLocalInventoryStatusRequest request, Cancellat { inventoryData ??= new InventoryData(sessionId); - if (!inventoryData.IsInventoryStarted) - { - var inventoryMember = _inventoryMemberService.GetOrCreateInventoryMember(inventoryData, sessionId, client); + var inventoryMember = _inventoryMemberService.GetOrCreateInventoryMember(inventoryData, sessionId, client); - if (inventoryMember.LastLocalInventoryStatusUpdate == null || - parameters.UtcChangeDate > inventoryMember.LastLocalInventoryStatusUpdate) - { - inventoryMember.SessionMemberGeneralStatus = parameters.SessionMemberGeneralStatus; - inventoryMember.LastLocalInventoryStatusUpdate = parameters.UtcChangeDate; + if (inventoryMember.LastLocalInventoryStatusUpdate == null || + parameters.UtcChangeDate > inventoryMember.LastLocalInventoryStatusUpdate) + { + inventoryMember.SessionMemberGeneralStatus = parameters.SessionMemberGeneralStatus; + inventoryMember.LastLocalInventoryStatusUpdate = parameters.UtcChangeDate; - _invokeClientsService.SessionGroupExcept(sessionId, client).SessionMemberGeneralStatusUpdated(parameters); + _invokeClientsService.SessionGroupExcept(sessionId, client).SessionMemberGeneralStatusUpdated(parameters); - return inventoryData; - } - else - { - _logger.LogWarning("SetLocalInventoryStatus: session {sessionId}, client {clientInstanceId} has a more recent status update", sessionId, - client.ClientInstanceId); - return null; - } + return inventoryData; } else { - _logger.LogWarning("RemovePathItem: session {sessionId} is already activated", sessionId); + _logger.LogWarning("SetLocalInventoryStatus: session {sessionId}, client {clientInstanceId} has a more recent status update", sessionId, + client.ClientInstanceId); return null; } }); diff --git a/src/ByteSync.ServerCommon/Entities/ActionsGroupDefinitionEntity.cs b/src/ByteSync.ServerCommon/Entities/ActionsGroupDefinitionEntity.cs deleted file mode 100644 index 480ebb34d..000000000 --- a/src/ByteSync.ServerCommon/Entities/ActionsGroupDefinitionEntity.cs +++ /dev/null @@ -1,27 +0,0 @@ -using ByteSync.Common.Business.Actions; -using ByteSync.Common.Business.Inventories; - -namespace ByteSync.ServerCommon.Entities; - -public class ActionsGroupDefinitionEntity -{ - public string ActionsGroupDefinitionEntityId { get; set; } = null!; - - public string SessionId { get; set; } = null!; - - public string? Source { get; set; } - - public List Targets { get; set; } - - public FileSystemTypes FileSystemType { get; set; } - - public ActionOperatorTypes Operator { get; set; } - - public long? Size { get; set; } - - public DateTime? CreationTimeUtc { get; set; } - - public bool AppliesOnlySynchronizeDate { get; set; } - - public DateTime? LastWriteTimeUtc { get; set; } -} \ No newline at end of file diff --git a/src/ByteSync.ServerCommon/Exceptions/ElementNotFoundException.cs b/src/ByteSync.ServerCommon/Exceptions/ElementNotFoundException.cs new file mode 100644 index 000000000..3965d375d --- /dev/null +++ b/src/ByteSync.ServerCommon/Exceptions/ElementNotFoundException.cs @@ -0,0 +1,20 @@ +using ByteSync.ServerCommon.Business.Repositories; + +namespace ByteSync.ServerCommon.Exceptions; + +public class ElementNotFoundException : Exception +{ + public ElementNotFoundException(string message) : base(message) + { + } + + public ElementNotFoundException(string message, Exception innerException) : base(message, innerException) + { + } + + public ElementNotFoundException(CacheKey cacheKey) : + base("Element not found in cache, key: " + cacheKey.Value + ", type: " + cacheKey.EntityType) + { + + } +} \ No newline at end of file diff --git a/src/ByteSync.ServerCommon/Factories/TrackingActionEntityFactory.cs b/src/ByteSync.ServerCommon/Factories/TrackingActionEntityFactory.cs deleted file mode 100644 index a55fa63e7..000000000 --- a/src/ByteSync.ServerCommon/Factories/TrackingActionEntityFactory.cs +++ /dev/null @@ -1,38 +0,0 @@ -using ByteSync.Common.Business.Actions; -using ByteSync.ServerCommon.Entities; -using ByteSync.ServerCommon.Interfaces.Factories; -using ByteSync.ServerCommon.Interfaces.Repositories; - -namespace ByteSync.ServerCommon.Factories; - -public class TrackingActionEntityFactory : ITrackingActionEntityFactory -{ - private readonly IActionsGroupDefinitionsRepository _actionsGroupDefinitionsRepository; - - public TrackingActionEntityFactory(IActionsGroupDefinitionsRepository actionsGroupDefinitionsRepository) - { - _actionsGroupDefinitionsRepository = actionsGroupDefinitionsRepository; - } - - public async Task Create(string sessionId, string actionsGroupId) - { - var actionGroupDefinition = await _actionsGroupDefinitionsRepository.GetActionGroupDefinition(actionsGroupId, sessionId); - - string? source = actionGroupDefinition.Source; - if (actionGroupDefinition.Operator == ActionOperatorTypes.SynchronizeDate - || (actionGroupDefinition.Operator == ActionOperatorTypes.SynchronizeContentAndDate && actionGroupDefinition.AppliesOnlySynchronizeDate)) - { - source = null; - } - - var trackingActionEntity = new TrackingActionEntity - { - ActionsGroupId = actionsGroupId, - SourceClientInstanceId = source, - TargetClientInstanceIds = [..actionGroupDefinition.Targets], - Size = actionGroupDefinition.Size, - }; - - return trackingActionEntity; - } -} \ No newline at end of file diff --git a/src/ByteSync.ServerCommon/Interfaces/Factories/ITrackingActionEntityFactory.cs b/src/ByteSync.ServerCommon/Interfaces/Factories/ITrackingActionEntityFactory.cs deleted file mode 100644 index b677dd58c..000000000 --- a/src/ByteSync.ServerCommon/Interfaces/Factories/ITrackingActionEntityFactory.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ByteSync.ServerCommon.Entities; - -namespace ByteSync.ServerCommon.Interfaces.Factories; - -public interface ITrackingActionEntityFactory -{ - Task Create(string sessionId, string actionsGroupId); -} \ No newline at end of file diff --git a/src/ByteSync.ServerCommon/Interfaces/Repositories/IActionsGroupDefinitionsRepository.cs b/src/ByteSync.ServerCommon/Interfaces/Repositories/IActionsGroupDefinitionsRepository.cs deleted file mode 100644 index 9618b4e36..000000000 --- a/src/ByteSync.ServerCommon/Interfaces/Repositories/IActionsGroupDefinitionsRepository.cs +++ /dev/null @@ -1,13 +0,0 @@ -using ByteSync.Common.Business.Actions; -using ByteSync.ServerCommon.Entities; - -namespace ByteSync.ServerCommon.Interfaces.Repositories; - -public interface IActionsGroupDefinitionsRepository -{ - Task AddOrUpdateActionsGroupDefinitions(string sessionId, List synchronizationActionsDefinitions); - - Task GetActionGroupDefinition(string actionsGroupId, string sessionId); - - Task DeleteActionsGroupDefinitions(string sessionId); -} \ No newline at end of file diff --git a/src/ByteSync.ServerCommon/Interfaces/Repositories/ICacheRepository.cs b/src/ByteSync.ServerCommon/Interfaces/Repositories/ICacheRepository.cs index 63599b263..b5d3d32d9 100644 --- a/src/ByteSync.ServerCommon/Interfaces/Repositories/ICacheRepository.cs +++ b/src/ByteSync.ServerCommon/Interfaces/Repositories/ICacheRepository.cs @@ -13,9 +13,7 @@ public interface ICacheRepository where T : class Task> Update(CacheKey cacheKey, Func updateHandler, bool throwIfNotExists, ITransaction? transaction = null, IRedLock? redisLock = null); - Task> AddOrUpdate(CacheKey cacheKey, Func handler, ITransaction? transaction = null); + Task> AddOrUpdate(CacheKey cacheKey, Func handler, ITransaction? transaction = null, IRedLock? redisLock = null); Task Delete(CacheKey cacheKey, ITransaction? transaction = null); - - Task AcquireLockAsync(CacheKey cacheKey); } \ No newline at end of file diff --git a/src/ByteSync.ServerCommon/Interfaces/Repositories/ITrackingActionRepository.cs b/src/ByteSync.ServerCommon/Interfaces/Repositories/ITrackingActionRepository.cs index 992b0bd7c..8bbe6a598 100644 --- a/src/ByteSync.ServerCommon/Interfaces/Repositories/ITrackingActionRepository.cs +++ b/src/ByteSync.ServerCommon/Interfaces/Repositories/ITrackingActionRepository.cs @@ -5,8 +5,8 @@ namespace ByteSync.ServerCommon.Interfaces.Repositories; public interface ITrackingActionRepository : IRepository { - Task GetOrBuild(string sessionId, string key); + Task GetOrThrow(string sessionId, string key); - Task AddOrUpdate(string sessionId, List actionsGroupIds, + Task AddOrUpdate(string sessionId, List actionsGroupIds, Func updateHandler); } \ No newline at end of file diff --git a/src/ByteSync.ServerCommon/Interfaces/Services/ISynchronizationProgressService.cs b/src/ByteSync.ServerCommon/Interfaces/Services/ISynchronizationProgressService.cs index 88d90eaaf..118d209b7 100644 --- a/src/ByteSync.ServerCommon/Interfaces/Services/ISynchronizationProgressService.cs +++ b/src/ByteSync.ServerCommon/Interfaces/Services/ISynchronizationProgressService.cs @@ -12,7 +12,7 @@ public interface ISynchronizationProgressService Task UpdateSynchronizationProgress(SynchronizationEntity synchronizationEntity, bool needSendSynchronizationUpdated); - Task InformSynchronizationStarted(SynchronizationEntity synchronizationEntity, Client client); + Task InformSynchronizationStarted(SynchronizationEntity synchronizationEntity, Client client); Task MapToSynchronization(SynchronizationEntity synchronizationEntity); diff --git a/src/ByteSync.ServerCommon/Interfaces/Services/ISynchronizationService.cs b/src/ByteSync.ServerCommon/Interfaces/Services/ISynchronizationService.cs index 2b9e55f68..1cebd5d48 100644 --- a/src/ByteSync.ServerCommon/Interfaces/Services/ISynchronizationService.cs +++ b/src/ByteSync.ServerCommon/Interfaces/Services/ISynchronizationService.cs @@ -7,7 +7,7 @@ namespace ByteSync.ServerCommon.Interfaces.Services; public interface ISynchronizationService { - Task StartSynchronization(string sessionId, Client client, List actionsGroupDefinitions); + Task StartSynchronization(string sessionId, Client client, List actionsGroupDefinitions); Task OnUploadIsFinishedAsync(SharedFileDefinition sharedFileDefinition, int totalParts, Client client); diff --git a/src/ByteSync.ServerCommon/Misc/ByteSyncDbContext.cs b/src/ByteSync.ServerCommon/Misc/ByteSyncDbContext.cs deleted file mode 100644 index 0cf8fc5ad..000000000 --- a/src/ByteSync.ServerCommon/Misc/ByteSyncDbContext.cs +++ /dev/null @@ -1,47 +0,0 @@ -using ByteSync.ServerCommon.Business.Settings; -using ByteSync.ServerCommon.Entities; -using Microsoft.Azure.Cosmos; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; - -namespace ByteSync.ServerCommon.Misc; - -public class ByteSyncDbContext : DbContext -{ - private readonly CosmosDbSettings _cosmosDbSettings; - - public ByteSyncDbContext(IOptions cosmosDbSettings) - { - _cosmosDbSettings = cosmosDbSettings.Value; - } - - public CosmosDbSettings CosmosDbSettings => _cosmosDbSettings; - - public DbSet ActionsGroupDefinitions { get; set; } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseCosmos(_cosmosDbSettings.ConnectionString, _cosmosDbSettings.DatabaseName); - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity() - .ToContainer("ActionsGroupDefinitions") - .HasPartitionKey(e => e.SessionId) - .HasNoDiscriminator(); - } - - public async Task InitializeCosmosDb() - { - var client = new CosmosClient(_cosmosDbSettings.ConnectionString); - - var database = await client.CreateDatabaseIfNotExistsAsync(_cosmosDbSettings.DatabaseName); - await database.Database.CreateContainerIfNotExistsAsync( - new ContainerProperties - { - Id = "ActionsGroupDefinitions", - PartitionKeyPath = "/SessionId" - }); - } -} \ No newline at end of file diff --git a/src/ByteSync.ServerCommon/Repositories/ActionsGroupDefinitionsRepository.cs b/src/ByteSync.ServerCommon/Repositories/ActionsGroupDefinitionsRepository.cs deleted file mode 100644 index fb6580ce5..000000000 --- a/src/ByteSync.ServerCommon/Repositories/ActionsGroupDefinitionsRepository.cs +++ /dev/null @@ -1,56 +0,0 @@ -using ByteSync.Common.Business.Actions; -using ByteSync.ServerCommon.Entities; -using ByteSync.ServerCommon.Interfaces.Repositories; -using ByteSync.ServerCommon.Misc; -using Microsoft.EntityFrameworkCore; - -namespace ByteSync.ServerCommon.Repositories; - -public class ActionsGroupDefinitionsRepository : IActionsGroupDefinitionsRepository -{ - private readonly ByteSyncDbContext _dbContext; - - public ActionsGroupDefinitionsRepository(ByteSyncDbContext dbContext) - { - _dbContext = dbContext; - } - - public async Task AddOrUpdateActionsGroupDefinitions(string sessionId, List synchronizationActionsDefinitions) - { - List actionsGroupDefinitionEntities = synchronizationActionsDefinitions - .Select(definition => new ActionsGroupDefinitionEntity - { - ActionsGroupDefinitionEntityId = definition.ActionsGroupId, - Operator = definition.Operator, - Size = definition.Size, - CreationTimeUtc = definition.CreationTimeUtc, - AppliesOnlySynchronizeDate = definition.AppliesOnlySynchronizeDate, - LastWriteTimeUtc = definition.LastWriteTimeUtc, - SessionId = sessionId, - Source = definition.Source, - Targets = definition.Targets, - FileSystemType = definition.FileSystemType, - }).ToList(); - - await _dbContext.ActionsGroupDefinitions.AddRangeAsync(actionsGroupDefinitionEntities); - await _dbContext.SaveChangesAsync(); - } - - public async Task GetActionGroupDefinition(string actionsGroupId, string sessionId) - { - return await _dbContext.ActionsGroupDefinitions - .FirstAsync(e => e.ActionsGroupDefinitionEntityId == actionsGroupId && - e.SessionId == sessionId); - } - - public async Task DeleteActionsGroupDefinitions(string sessionId) - { - var entitiesToDelete = await _dbContext.ActionsGroupDefinitions - .Where(e => e.SessionId == sessionId) - .ToListAsync(); - - _dbContext.ActionsGroupDefinitions.RemoveRange(entitiesToDelete); - - await _dbContext.SaveChangesAsync(); - } -} \ No newline at end of file diff --git a/src/ByteSync.ServerCommon/Repositories/BaseRepository.cs b/src/ByteSync.ServerCommon/Repositories/BaseRepository.cs index b7351e173..0c9ced064 100644 --- a/src/ByteSync.ServerCommon/Repositories/BaseRepository.cs +++ b/src/ByteSync.ServerCommon/Repositories/BaseRepository.cs @@ -26,7 +26,7 @@ protected BaseRepository(IRedisInfrastructureService cacheService, ICacheReposit return await Get(cacheKey); } - public async Task Get(CacheKey cacheKey) + protected async Task Get(CacheKey cacheKey) { return await _cacheRepository.Get(cacheKey); } diff --git a/src/ByteSync.ServerCommon/Repositories/CacheRepository.cs b/src/ByteSync.ServerCommon/Repositories/CacheRepository.cs index 0332a949d..3322ebf30 100644 --- a/src/ByteSync.ServerCommon/Repositories/CacheRepository.cs +++ b/src/ByteSync.ServerCommon/Repositories/CacheRepository.cs @@ -87,21 +87,31 @@ public async Task> Update(CacheKey cacheKey, Func } } - public async Task> AddOrUpdate(CacheKey cacheKey, Func handler, ITransaction? transaction = null) + public async Task> AddOrUpdate(CacheKey cacheKey, Func handler, ITransaction? transaction = null, IRedLock? redisLock = null) { IDatabaseAsync database = _redisInfrastructureService.GetDatabase(transaction); + bool shouldDispose = redisLock == null; + redisLock ??= await _redisInfrastructureService.AcquireLockAsync(cacheKey); - await using var redisLock = await _redisInfrastructureService.AcquireLockAsync(cacheKey); - - var cachedElement = await Get(cacheKey); - var createdOrUpdatedElement = handler.Invoke(cachedElement); + try + { + var cachedElement = await Get(cacheKey); + var createdOrUpdatedElement = handler.Invoke(cachedElement); - if (createdOrUpdatedElement == null) + if (createdOrUpdatedElement == null) + { + return new UpdateEntityResult(cachedElement, UpdateEntityStatus.NoOperation); + } + + return await SaveInternal(cacheKey, createdOrUpdatedElement, database); + } + finally { - return new UpdateEntityResult(cachedElement, UpdateEntityStatus.NoOperation); + if (shouldDispose) + { + await redisLock.DisposeAsync(); + } } - - return await SaveInternal(cacheKey, createdOrUpdatedElement, database); } public async Task Delete(CacheKey cacheKey, ITransaction? transaction = null) @@ -109,11 +119,6 @@ public async Task Delete(CacheKey cacheKey, ITransaction? transaction = null) IDatabaseAsync database = _redisInfrastructureService.GetDatabase(transaction); await database.KeyDeleteAsync(cacheKey.Value); } - - public async Task AcquireLockAsync(CacheKey cacheKey) - { - return await _redisInfrastructureService.AcquireLockAsync(cacheKey); - } private async Task> SaveInternal(CacheKey cacheKey, T element, IDatabaseAsync database) { diff --git a/src/ByteSync.ServerCommon/Repositories/SynchronizationRepository.cs b/src/ByteSync.ServerCommon/Repositories/SynchronizationRepository.cs index 1126fe4cb..890ae98bf 100644 --- a/src/ByteSync.ServerCommon/Repositories/SynchronizationRepository.cs +++ b/src/ByteSync.ServerCommon/Repositories/SynchronizationRepository.cs @@ -7,27 +7,53 @@ namespace ByteSync.ServerCommon.Repositories; public class SynchronizationRepository : BaseRepository, ISynchronizationRepository { - private readonly IActionsGroupDefinitionsRepository _actionsGroupDefinitionsRepository; - + private readonly ICacheRepository _cacheTrackingAction; + public SynchronizationRepository(IRedisInfrastructureService redisInfrastructureService, ICacheRepository cacheRepository, - IActionsGroupDefinitionsRepository actionsGroupDefinitionsRepository) : base(redisInfrastructureService, cacheRepository) + ICacheRepository cacheTrackingAction) : base(redisInfrastructureService, cacheRepository) { - _actionsGroupDefinitionsRepository = actionsGroupDefinitionsRepository; + _cacheTrackingAction = cacheTrackingAction; } public override EntityType EntityType => EntityType.Synchronization; public async Task AddSynchronization(SynchronizationEntity synchronizationEntity, List actionsGroupDefinitions) { - await Save(synchronizationEntity.SessionId, synchronizationEntity); + var synchronizationCacheKey = _cacheService.ComputeCacheKey(EntityType.Synchronization, synchronizationEntity.SessionId); + await using var synchronizationLock = await _cacheService.AcquireLockAsync(synchronizationCacheKey); + + await Save(synchronizationEntity.SessionId, synchronizationEntity, null, synchronizationLock); + + var semaphore = new SemaphoreSlim(20); // Limit to 20 tasks in parallel + var tasks = actionsGroupDefinitions.Select(async groupDefinition => + { + await semaphore.WaitAsync(); + try + { + var trackingActionEntity = new TrackingActionEntity + { + ActionsGroupId = groupDefinition.ActionsGroupId, + SourceClientInstanceId = groupDefinition.Source, + TargetClientInstanceIds = [..groupDefinition.Targets], + Size = groupDefinition.Size, + }; - await _actionsGroupDefinitionsRepository.AddOrUpdateActionsGroupDefinitions(synchronizationEntity.SessionId, actionsGroupDefinitions); + var cacheKey = _cacheService.ComputeCacheKey(EntityType.TrackingAction, $"{synchronizationEntity.SessionId}_{groupDefinition.ActionsGroupId}"); + + // ReSharper disable once AccessToDisposedClosure + await _cacheTrackingAction.Save(cacheKey, trackingActionEntity, null, synchronizationLock); + } + finally + { + semaphore.Release(); + } + }); + + await Task.WhenAll(tasks); } public async Task ResetSession(string sessionId) { await Delete(sessionId); - - await _actionsGroupDefinitionsRepository.DeleteActionsGroupDefinitions(sessionId); } } \ No newline at end of file diff --git a/src/ByteSync.ServerCommon/Repositories/TrackingActionRepository.cs b/src/ByteSync.ServerCommon/Repositories/TrackingActionRepository.cs index e1059ecba..d1d55f50d 100644 --- a/src/ByteSync.ServerCommon/Repositories/TrackingActionRepository.cs +++ b/src/ByteSync.ServerCommon/Repositories/TrackingActionRepository.cs @@ -1,10 +1,10 @@ -using ByteSync.ServerCommon.Business.Repositories; +using System.Collections.Concurrent; +using ByteSync.ServerCommon.Business.Repositories; using ByteSync.ServerCommon.Entities; -using ByteSync.ServerCommon.Interfaces.Factories; +using ByteSync.ServerCommon.Exceptions; using ByteSync.ServerCommon.Interfaces.Repositories; using ByteSync.ServerCommon.Interfaces.Services; using Microsoft.Extensions.Logging; -using RedLockNet; namespace ByteSync.ServerCommon.Repositories; @@ -12,92 +12,100 @@ public class TrackingActionRepository : BaseRepository, IT { private readonly IRedisInfrastructureService _redisInfrastructureService; private readonly ISynchronizationRepository _synchronizationRepository; - private readonly ITrackingActionEntityFactory _trackingActionEntityFactory; private readonly ICacheRepository _synchronizationCacheRepository; private readonly ILogger _logger; - - public TrackingActionRepository(IRedisInfrastructureService redisInfrastructureService, ISynchronizationRepository synchronizationRepository, - ITrackingActionEntityFactory trackingActionEntityFactory, ICacheRepository cacheRepository, - ICacheRepository synchronizationCacheRepository, ILogger logger) + + public TrackingActionRepository(IRedisInfrastructureService redisInfrastructureService, ISynchronizationRepository synchronizationRepository, + ICacheRepository cacheRepository, + ICacheRepository synchronizationCacheRepository, ILogger logger) : base(redisInfrastructureService, cacheRepository) { _redisInfrastructureService = redisInfrastructureService; _synchronizationRepository = synchronizationRepository; - _trackingActionEntityFactory = trackingActionEntityFactory; _synchronizationCacheRepository = synchronizationCacheRepository; _logger = logger; } public override EntityType EntityType => EntityType.TrackingAction; - - public async Task GetOrBuild(string sessionId, string actionsGroupId) + + public async Task GetOrThrow(string sessionId, string actionsGroupId) { var cacheKey = _redisInfrastructureService.ComputeCacheKey(EntityType, $"{sessionId}_{actionsGroupId}"); - - await using var actionsGroupIdLock = await _redisInfrastructureService.AcquireLockAsync(cacheKey); - return await DoGetOrBuild(sessionId, actionsGroupId, cacheKey, actionsGroupIdLock); + return await GetOrThrow(cacheKey); } - - private async Task DoGetOrBuild(string sessionId, string actionsGroupId, CacheKey cacheKey, IRedLock actionsGroupIdLock) + + private async Task GetOrThrow(CacheKey cacheKey) { - var trackingActionEntity = await Get($"{sessionId}_{actionsGroupId}"); - + var trackingActionEntity = await Get(cacheKey); + if (trackingActionEntity == null) { - trackingActionEntity = await _trackingActionEntityFactory.Create(sessionId, actionsGroupId); - - await Save(cacheKey, trackingActionEntity, null, actionsGroupIdLock); + throw new ElementNotFoundException(cacheKey); } - + return trackingActionEntity; } - public async Task AddOrUpdate(string sessionId, List actionsGroupIds, + public async Task AddOrUpdate(string sessionId, List actionsGroupIds, Func updateHandler) { + var trackingActionEntities = new ConcurrentBag(); + var synchronizationCacheKey = _redisInfrastructureService.ComputeCacheKey(EntityType.Synchronization, sessionId); await using var synchronizationLock = await _redisInfrastructureService.AcquireLockAsync(synchronizationCacheKey); - - var synchronizationEntity = (await _synchronizationRepository.Get(sessionId)); + + var synchronizationEntity = await _synchronizationRepository.Get(sessionId); if (synchronizationEntity == null) { throw new InvalidOperationException($"SynchronizationEntity for session {sessionId} not found"); } - + var transaction = _redisInfrastructureService.OpenTransaction(); - var locks = new List(); - - List trackingActionEntities = new List(); - bool areAllUpdated = true; - foreach (var actionsGroupId in actionsGroupIds) + var semaphore = new SemaphoreSlim(20); + var updateFailures = new ConcurrentBag(); + + var invokeLock = new object(); + + var tasks = actionsGroupIds.Select(async actionsGroupId => { - if (!areAllUpdated) + await semaphore.WaitAsync(); + try { - break; - } - - var cacheKey = _redisInfrastructureService.ComputeCacheKey(EntityType, $"{sessionId}_{actionsGroupId}"); - var actionsGroupIdLock = await _redisInfrastructureService.AcquireLockAsync(cacheKey); - locks.Add(actionsGroupIdLock); + var cacheKey = _redisInfrastructureService.ComputeCacheKey(EntityType, $"{sessionId}_{actionsGroupId}"); - var trackingActionEntity = await DoGetOrBuild(sessionId, actionsGroupId, cacheKey, actionsGroupIdLock); - bool isUpdated = updateHandler.Invoke(trackingActionEntity, synchronizationEntity); + var trackingActionEntity = await GetOrThrow(cacheKey); - if (isUpdated) - { - await Save(cacheKey, trackingActionEntity, transaction, actionsGroupIdLock); - trackingActionEntities.Add(trackingActionEntity); + bool isUpdated; + lock (invokeLock) + { + isUpdated = updateHandler.Invoke(trackingActionEntity, synchronizationEntity); + } + + if (isUpdated) + { + // ReSharper disable once AccessToDisposedClosure + await Save(cacheKey, trackingActionEntity, transaction, synchronizationLock); + trackingActionEntities.Add(trackingActionEntity); + } + else + { + _logger.LogWarning("AddOrUpdate: can not update element {TrackingActionEntity} for session {SessionId}. No element will be updated", + trackingActionEntity.ActionsGroupId, sessionId); + + updateFailures.Add(true); + } } - else + finally { - _logger.LogWarning("AddOrUpdate: can not update element {TrackingActionEntity} for session {SessionId}. No element will be updated", - trackingActionEntity.ActionsGroupId, sessionId); + semaphore.Release(); } + }); - areAllUpdated &= isUpdated; - } + await Task.WhenAll(tasks); + + var areAllUpdated = updateFailures.IsEmpty; if (areAllUpdated) { @@ -106,13 +114,6 @@ public async Task AddOrUpdate(string sessionId, List redisSettings, ICacheK _connectionMultiplexer, }; - RedLockRetryConfiguration redLockRetryConfiguration = new RedLockRetryConfiguration(5, 500); + RedLockRetryConfiguration redLockRetryConfiguration = new RedLockRetryConfiguration(10, 1000); _redLockFactory = RedLockFactory.Create(multiplexers, redLockRetryConfiguration, loggerFactory); } @@ -80,8 +80,8 @@ public async Task AcquireLockAsync(EntityType entityType, string entit public async Task AcquireLockAsync(CacheKey cacheKey) { - var redisLock = await _redLockFactory.CreateLockAsync(cacheKey.Value, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(1)); - + var redisLock = await _redLockFactory.CreateLockAsync(cacheKey.Value, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(2)); + if (redisLock.IsAcquired) { return redisLock; diff --git a/src/ByteSync.ServerCommon/Services/SynchronizationProgressService.cs b/src/ByteSync.ServerCommon/Services/SynchronizationProgressService.cs index 8ae519e17..ca1336f35 100644 --- a/src/ByteSync.ServerCommon/Services/SynchronizationProgressService.cs +++ b/src/ByteSync.ServerCommon/Services/SynchronizationProgressService.cs @@ -45,12 +45,10 @@ public async Task UpdateSynchronizationProgress(SynchronizationEntity synchroniz } } - public async Task InformSynchronizationStarted(SynchronizationEntity synchronizationEntity, Client client) + public async Task InformSynchronizationStarted(SynchronizationEntity synchronizationEntity, Client client) { var synchronization = await MapToSynchronization(synchronizationEntity); - await _invokeClientsService.SessionGroupExcept(synchronization.SessionId, client).SynchronizationStarted(synchronization); - - return synchronization; + await _invokeClientsService.SessionGroup(synchronization.SessionId).SynchronizationStarted(synchronization); } public async Task UploadIsFinished(SharedFileDefinition sharedFileDefinition, int totalParts, HashSet targetInstanceIds) @@ -100,7 +98,7 @@ private async Task SendSynchronizationUpdated(SynchronizationEntity synchronizat private async Task SendSynchronizationProgressUpdated(SynchronizationEntity synchronizationEntity) { var synchronizationProgressPush = CreateSynchronizationProgressPush(synchronizationEntity, null); - + await _invokeClientsService.Clients(synchronizationEntity.Progress.Members) .SynchronizationProgressUpdated(synchronizationProgressPush); } @@ -116,7 +114,7 @@ private async Task SendSynchronizationProgressUpdated(TrackingActionResult track } var synchronizationProgressPush = CreateSynchronizationProgressPush(trackingActionResult.SynchronizationEntity, trackingActionSummaries); - + await _invokeClientsService.Clients(trackingActionResult.SynchronizationEntity.Progress.Members) .SynchronizationProgressUpdated(synchronizationProgressPush); } diff --git a/src/ByteSync.ServerCommon/Services/SynchronizationService.cs b/src/ByteSync.ServerCommon/Services/SynchronizationService.cs index 375559b14..e130e6458 100644 --- a/src/ByteSync.ServerCommon/Services/SynchronizationService.cs +++ b/src/ByteSync.ServerCommon/Services/SynchronizationService.cs @@ -1,8 +1,10 @@ -using ByteSync.Common.Business.Actions; +using System.Collections.Concurrent; +using ByteSync.Common.Business.Actions; using ByteSync.Common.Business.SharedFiles; using ByteSync.Common.Business.Synchronizations; using ByteSync.Common.Helpers; using ByteSync.ServerCommon.Business.Auth; +using ByteSync.ServerCommon.Business.Repositories; using ByteSync.ServerCommon.Entities; using ByteSync.ServerCommon.Exceptions; using ByteSync.ServerCommon.Interfaces.Repositories; @@ -46,7 +48,7 @@ public SynchronizationService(ICloudSessionsRepository cloudSessionsRepository, } } - public async Task StartSynchronization(string sessionId, Client client, List actionsGroupDefinitions) + public async Task StartSynchronization(string sessionId, Client client, List actionsGroupDefinitions) { var synchronizationEntity = await _synchronizationRepository.Get(sessionId); @@ -67,12 +69,8 @@ public async Task StartSynchronization(string sessionId, Client }; await _synchronizationRepository.AddSynchronization(synchronizationEntity, actionsGroupDefinitions); - - return await _synchronizationProgressService.InformSynchronizationStarted(synchronizationEntity, client); - } - else - { - return await _synchronizationProgressService.MapToSynchronization(synchronizationEntity); + + await _synchronizationProgressService.InformSynchronizationStarted(synchronizationEntity, client); } } @@ -90,8 +88,11 @@ public async Task OnUploadIsFinishedAsync(SharedFileDefinition sharedFileDefinit } trackingAction.IsSourceSuccess = true; - - targetInstanceIds.AddAll(trackingAction.TargetClientInstanceIds); + + foreach (var targetClientInstanceId in trackingAction.TargetClientInstanceIds) + { + targetInstanceIds.Add(targetClientInstanceId); + } return true; }); @@ -117,7 +118,7 @@ public async Task OnFilePartIsUploadedAsync(SharedFileDefinition sharedFileDefin } var actionsGroupsId = sharedFileDefinition.ActionsGroupIds!.First(); - var trackingAction = await _trackingActionRepository.GetOrBuild(sharedFileDefinition.SessionId, actionsGroupsId); + var trackingAction = await _trackingActionRepository.GetOrThrow(sharedFileDefinition.SessionId, actionsGroupsId); await _synchronizationProgressService.FilePartIsUploaded(sharedFileDefinition, partNumber, trackingAction.TargetClientInstanceIds); } @@ -144,8 +145,15 @@ public async Task OnDownloadIsFinishedAsync(SharedFileDefinition sharedFileDefin synchronization.Progress.FinishedActionsCount += 1; synchronization.Progress.ProcessedVolume += trackingAction.Size ?? 0; } - - synchronization.Progress.ExchangedVolume += sharedFileDefinition.UploadedFileLength; + + if (sharedFileDefinition.IsMultiFileZip) + { + synchronization.Progress.ExchangedVolume += trackingAction.Size ?? 0; + } + else + { + synchronization.Progress.ExchangedVolume += sharedFileDefinition.UploadedFileLength; + } needSendSynchronizationUpdated = CheckSynchronizationIsFinished(synchronization); diff --git a/tests/ByteSync.Client.IntegrationTests/Models/Comparisons/TestInventoryComparer.cs b/tests/ByteSync.Client.IntegrationTests/Models/Comparisons/TestInventoryComparer.cs index 2f2149932..192a9e72c 100644 --- a/tests/ByteSync.Client.IntegrationTests/Models/Comparisons/TestInventoryComparer.cs +++ b/tests/ByteSync.Client.IntegrationTests/Models/Comparisons/TestInventoryComparer.cs @@ -11,6 +11,7 @@ using ByteSync.Models.FileSystems; using ByteSync.Services.Sessions; using ByteSync.TestsCommon; +using FluentAssertions; using Moq; using NUnit.Framework.Legacy; @@ -61,38 +62,37 @@ public async Task Test_2_Inventories_Empty() sessionSettings.ExcludeHiddenFiles = true; sessionSettings.ExcludeSystemFiles = true; comparisonResult = await comparisonResultPreparer.BuildAndCompare(sessionSettings, inventoryDataA, inventoryDataB); - ClassicAssert.AreEqual(0, comparisonResult.ComparisonItems.Count); + comparisonResult.ComparisonItems.Should().HaveCount(0); // Test sessionSettings.DataType = DataTypes.Directories; comparisonResult = await comparisonResultPreparer.BuildAndCompare(sessionSettings, inventoryDataA, inventoryDataB); - ClassicAssert.AreEqual(0, comparisonResult.ComparisonItems.Count); + comparisonResult.ComparisonItems.Should().HaveCount(0); // Test sessionSettings.DataType = DataTypes.Files; comparisonResult = await comparisonResultPreparer.BuildAndCompare(sessionSettings, inventoryDataA, inventoryDataB); - ClassicAssert.AreEqual(0, comparisonResult.ComparisonItems.Count); + comparisonResult.ComparisonItems.Should().HaveCount(0); // Test sessionSettings.AnalysisMode = AnalysisModes.Smart; sessionSettings.DataType = DataTypes.FilesDirectories; comparisonResult = await comparisonResultPreparer.BuildAndCompare(sessionSettings, inventoryDataA, inventoryDataB); - ClassicAssert.AreEqual(0, comparisonResult.ComparisonItems.Count); + comparisonResult.ComparisonItems.Should().HaveCount(0); // Test sessionSettings.AnalysisMode = AnalysisModes.Smart; sessionSettings.DataType = DataTypes.Directories; comparisonResult = await comparisonResultPreparer.BuildAndCompare(sessionSettings, inventoryDataA, inventoryDataB); - ClassicAssert.AreEqual(0, comparisonResult.ComparisonItems.Count); + comparisonResult.ComparisonItems.Should().HaveCount(0); // Test sessionSettings.AnalysisMode = AnalysisModes.Smart; sessionSettings.DataType = DataTypes.Files; comparisonResult = await comparisonResultPreparer.BuildAndCompare(sessionSettings, inventoryDataA, inventoryDataB); - ClassicAssert.AreEqual(0, comparisonResult.ComparisonItems.Count); + comparisonResult.ComparisonItems.Should().HaveCount(0); } - [Test] public async Task Test_2_Inventories_1_Same_File() { @@ -148,39 +148,35 @@ public async Task Test_2_Inventories_1_Same_File() void DoChecksEmpty() { - ClassicAssert.AreEqual(0, comparisonResult.ComparisonItems.Count); + comparisonResult.ComparisonItems.Should().HaveCount(0); } void DoChecks() { - ClassicAssert.AreEqual(1, comparisonResult.ComparisonItems.Count); + comparisonResult.ComparisonItems.Should().HaveCount(1); var comparisonItem = comparisonResult.ComparisonItems.Single(); - ClassicAssert.AreEqual(1, comparisonItem.ContentIdentities.Count); + comparisonItem.ContentIdentities.Should().HaveCount(1); var contentIdentity = comparisonItem.ContentIdentities.Single(); - ClassicAssert.AreEqual(null, contentIdentity.Core.SignatureHash); - ClassicAssert.AreEqual(8, contentIdentity.Core.Size); + contentIdentity.Core.SignatureHash.Should().BeNull(); + contentIdentity.Core.Size.Should().Be(8); - ClassicAssert.AreEqual(2, contentIdentity.FileSystemDescriptions.Count); - ClassicAssert.AreEqual(1, contentIdentity.InventoryPartsByLastWriteTimes.Count); - ClassicAssert.AreEqual(false, contentIdentity.HasAnalysisError); + contentIdentity.FileSystemDescriptions.Should().HaveCount(2); + contentIdentity.InventoryPartsByLastWriteTimes.Should().HaveCount(1); + contentIdentity.HasAnalysisError.Should().BeFalse(); - ClassicAssert.AreEqual("file1.txt", comparisonItem.PathIdentity.FileName); - ClassicAssert.AreEqual(FileSystemTypes.File, comparisonItem.PathIdentity.FileSystemType); - ClassicAssert.AreEqual("/file1.txt", comparisonItem.PathIdentity.LinkingData); - ClassicAssert.AreEqual("/file1.txt", comparisonItem.PathIdentity.LinkingKeyValue); + comparisonItem.PathIdentity.FileName.Should().Be("file1.txt"); + comparisonItem.PathIdentity.FileSystemType.Should().Be(FileSystemTypes.File); + comparisonItem.PathIdentity.LinkingData.Should().Be("/file1.txt"); + comparisonItem.PathIdentity.LinkingKeyValue.Should().Be("/file1.txt"); - // ClassicAssert.AreEqual(true, comparisonItem.ContentRepartition.IsOK); - // ClassicAssert.AreEqual(false, comparisonItem.ContentRepartition.IsSuccessStatus); - // ClassicAssert.AreEqual(false, comparisonItem.ContentRepartition.IsErrorStatus); - ClassicAssert.AreEqual(1, comparisonItem.ContentRepartition.FingerPrintGroups.Count); - ClassicAssert.AreEqual(1, comparisonItem.ContentRepartition.LastWriteTimeGroups.Count); - ClassicAssert.AreEqual(0, comparisonItem.ContentRepartition.MissingInventories.Count); - ClassicAssert.AreEqual(0, comparisonItem.ContentRepartition.MissingInventoryParts.Count); + comparisonItem.ContentRepartition.FingerPrintGroups.Should().HaveCount(1); + comparisonItem.ContentRepartition.LastWriteTimeGroups.Should().HaveCount(1); + comparisonItem.ContentRepartition.MissingInventories.Should().BeEmpty(); + comparisonItem.ContentRepartition.MissingInventoryParts.Should().BeEmpty(); } } - [Test] public async Task Test_2_Inventories_1_File_Different() { @@ -209,7 +205,7 @@ public async Task Test_2_Inventories_1_File_Different() // Test sessionSettings.DataType = DataTypes.Directories; comparisonResult = await comparisonResultPreparer.BuildAndCompare(sessionSettings, inventoryDataA, inventoryDataB); - ClassicAssert.AreEqual(0, comparisonResult.ComparisonItems.Count); + comparisonResult.ComparisonItems.Should().HaveCount(0); // Test sessionSettings.DataType = DataTypes.Files; @@ -226,7 +222,7 @@ public async Task Test_2_Inventories_1_File_Different() sessionSettings.AnalysisMode = AnalysisModes.Smart; sessionSettings.DataType = DataTypes.Directories; comparisonResult = await comparisonResultPreparer.BuildAndCompare(sessionSettings, inventoryDataA, inventoryDataB); - ClassicAssert.AreEqual(0, comparisonResult.ComparisonItems.Count); + comparisonResult.ComparisonItems.Should().HaveCount(0); // Test sessionSettings.AnalysisMode = AnalysisModes.Smart; @@ -236,43 +232,38 @@ public async Task Test_2_Inventories_1_File_Different() void DoChecks() { - ClassicAssert.AreEqual(1, comparisonResult.ComparisonItems.Count); + comparisonResult.ComparisonItems.Should().HaveCount(1); var comparisonItem = comparisonResult.ComparisonItems.Single(); - ClassicAssert.AreEqual(2, comparisonItem.ContentIdentities.Count); + comparisonItem.ContentIdentities.Should().HaveCount(2); var contentIdentityA = comparisonItem.ContentIdentities.Single(ci => ci.IsPresentIn(inventoryDataA.Inventory)); var contentIdentityB = comparisonItem.ContentIdentities.Single(ci => ci.IsPresentIn(inventoryDataB.Inventory)); - ClassicAssert.IsNotNull(contentIdentityA.Core.SignatureHash); - ClassicAssert.IsNotEmpty(contentIdentityA.Core.SignatureHash); - ClassicAssert.AreEqual(8, contentIdentityA.Core.Size); - ClassicAssert.AreEqual(1, contentIdentityA.FileSystemDescriptions.Count); - ClassicAssert.AreEqual(1, contentIdentityA.InventoryPartsByLastWriteTimes.Count); - ClassicAssert.AreEqual(false, contentIdentityA.HasAnalysisError); - - ClassicAssert.IsNotNull(contentIdentityB.Core.SignatureHash); - ClassicAssert.IsNotEmpty(contentIdentityB.Core.SignatureHash); - ClassicAssert.AreEqual(9, contentIdentityB.Core.Size); - ClassicAssert.AreEqual(1, contentIdentityB.FileSystemDescriptions.Count); - ClassicAssert.AreEqual(1, contentIdentityB.InventoryPartsByLastWriteTimes.Count); - ClassicAssert.AreEqual(false, contentIdentityB.HasAnalysisError); - - ClassicAssert.AreEqual("file1.txt", comparisonItem.PathIdentity.FileName); - ClassicAssert.AreEqual(FileSystemTypes.File, comparisonItem.PathIdentity.FileSystemType); - ClassicAssert.AreEqual("/file1.txt", comparisonItem.PathIdentity.LinkingData); - ClassicAssert.AreEqual("/file1.txt", comparisonItem.PathIdentity.LinkingKeyValue); - - // ClassicAssert.AreEqual(false, comparisonItem.ContentRepartition.IsOK); - // ClassicAssert.AreEqual(false, comparisonItem.ContentRepartition.IsSuccessStatus); - // ClassicAssert.AreEqual(false, comparisonItem.ContentRepartition.IsErrorStatus); - ClassicAssert.AreEqual(2, comparisonItem.ContentRepartition.FingerPrintGroups.Count); - ClassicAssert.IsTrue(comparisonItem.ContentRepartition.LastWriteTimeGroups.Count.In(1, 2)); - ClassicAssert.AreEqual(0, comparisonItem.ContentRepartition.MissingInventories.Count); - ClassicAssert.AreEqual(0, comparisonItem.ContentRepartition.MissingInventoryParts.Count); + contentIdentityA.Core.SignatureHash.Should().NotBeNull(); + contentIdentityA.Core.SignatureHash.Should().NotBeEmpty(); + contentIdentityA.Core.Size.Should().Be(8); + contentIdentityA.FileSystemDescriptions.Should().HaveCount(1); + contentIdentityA.InventoryPartsByLastWriteTimes.Should().HaveCount(1); + contentIdentityA.HasAnalysisError.Should().BeFalse(); + + contentIdentityB.Core.SignatureHash.Should().NotBeNull(); + contentIdentityB.Core.SignatureHash.Should().NotBeEmpty(); + contentIdentityB.Core.Size.Should().Be(9); + contentIdentityB.FileSystemDescriptions.Should().HaveCount(1); + contentIdentityB.InventoryPartsByLastWriteTimes.Should().HaveCount(1); + contentIdentityB.HasAnalysisError.Should().BeFalse(); + + comparisonItem.PathIdentity.FileName.Should().Be("file1.txt"); + comparisonItem.PathIdentity.FileSystemType.Should().Be(FileSystemTypes.File); + comparisonItem.PathIdentity.LinkingData.Should().Be("/file1.txt"); + comparisonItem.PathIdentity.LinkingKeyValue.Should().Be("/file1.txt"); + + comparisonItem.ContentRepartition.FingerPrintGroups.Should().HaveCount(2); + comparisonItem.ContentRepartition.LastWriteTimeGroups.Count.Should().BeOneOf(1, 2); + comparisonItem.ContentRepartition.MissingInventories.Should().BeEmpty(); + comparisonItem.ContentRepartition.MissingInventoryParts.Should().BeEmpty(); } } - - [Test] public async Task Test_2_Inventories_1_Directory() { @@ -306,7 +297,7 @@ public async Task Test_2_Inventories_1_Directory() // Test sessionSettings.DataType = DataTypes.Files; comparisonResult = await comparisonResultPreparer.BuildAndCompare(sessionSettings, inventoryDataA, inventoryDataB); - ClassicAssert.AreEqual(0, comparisonResult.ComparisonItems.Count); + comparisonResult.ComparisonItems.Should().HaveCount(0); // Test sessionSettings.AnalysisMode = AnalysisModes.Smart; @@ -324,32 +315,29 @@ public async Task Test_2_Inventories_1_Directory() sessionSettings.AnalysisMode = AnalysisModes.Smart; sessionSettings.DataType = DataTypes.Files; comparisonResult = await comparisonResultPreparer.BuildAndCompare(sessionSettings, inventoryDataA, inventoryDataB); - ClassicAssert.AreEqual(0, comparisonResult.ComparisonItems.Count); + comparisonResult.ComparisonItems.Should().HaveCount(0); void DoChecks() { - ClassicAssert.AreEqual(1, comparisonResult.ComparisonItems.Count); + comparisonResult.ComparisonItems.Should().HaveCount(1); var comparisonItem = comparisonResult.ComparisonItems.Single(); - ClassicAssert.AreEqual(1, comparisonItem.ContentIdentities.Count); + comparisonItem.ContentIdentities.Should().HaveCount(1); var contentIdentity = comparisonItem.ContentIdentities.Single(); - ClassicAssert.IsNull(contentIdentity.Core); - ClassicAssert.AreEqual(2, contentIdentity.FileSystemDescriptions.Count); - ClassicAssert.AreEqual(0, contentIdentity.InventoryPartsByLastWriteTimes.Count); - ClassicAssert.AreEqual(false, contentIdentity.HasAnalysisError); + contentIdentity.Core.Should().BeNull(); + contentIdentity.FileSystemDescriptions.Should().HaveCount(2); + contentIdentity.InventoryPartsByLastWriteTimes.Should().HaveCount(0); + contentIdentity.HasAnalysisError.Should().BeFalse(); - ClassicAssert.AreEqual("Dir1", comparisonItem.PathIdentity.FileName); - ClassicAssert.AreEqual(FileSystemTypes.Directory, comparisonItem.PathIdentity.FileSystemType); - ClassicAssert.AreEqual("/dir1", comparisonItem.PathIdentity.LinkingData); - ClassicAssert.AreEqual("/Dir1", comparisonItem.PathIdentity.LinkingKeyValue); + comparisonItem.PathIdentity.FileName.Should().Be("Dir1"); + comparisonItem.PathIdentity.FileSystemType.Should().Be(FileSystemTypes.Directory); + comparisonItem.PathIdentity.LinkingData.Should().Be("/dir1"); + comparisonItem.PathIdentity.LinkingKeyValue.Should().Be("/Dir1"); - // ClassicAssert.AreEqual(true, comparisonItem.ContentRepartition.IsOK); - // ClassicAssert.AreEqual(false, comparisonItem.ContentRepartition.IsSuccessStatus); - // ClassicAssert.AreEqual(false, comparisonItem.ContentRepartition.IsErrorStatus); - ClassicAssert.AreEqual(0, comparisonItem.ContentRepartition.FingerPrintGroups.Count); - ClassicAssert.AreEqual(0, comparisonItem.ContentRepartition.LastWriteTimeGroups.Count); - ClassicAssert.AreEqual(0, comparisonItem.ContentRepartition.MissingInventories.Count); - ClassicAssert.AreEqual(0, comparisonItem.ContentRepartition.MissingInventoryParts.Count); + comparisonItem.ContentRepartition.FingerPrintGroups.Should().BeEmpty(); + comparisonItem.ContentRepartition.LastWriteTimeGroups.Should().BeEmpty(); + comparisonItem.ContentRepartition.MissingInventories.Should().BeEmpty(); + comparisonItem.ContentRepartition.MissingInventoryParts.Should().BeEmpty(); } } @@ -389,33 +377,29 @@ public async Task Test_2_AnalysisModes(AnalysisModes analysisMode) sessionSettings.ExcludeSystemFiles = true; comparisonResult = await comparisonResultPreparer.BuildAndCompare(sessionSettings, inventoryDataA, inventoryDataB); - ClassicAssert.AreEqual(10, comparisonResult.ComparisonItems.Count); + comparisonResult.ComparisonItems.Should().HaveCount(10); foreach (var comparisonItem in comparisonResult.ComparisonItems) { - ClassicAssert.AreEqual(1, comparisonItem.ContentIdentities.Count); + comparisonItem.ContentIdentities.Should().HaveCount(1); var contentIdentity = comparisonItem.ContentIdentities.Single(); - ClassicAssert.AreEqual(2, contentIdentity.FileSystemDescriptions.Count); + contentIdentity.FileSystemDescriptions.Should().HaveCount(2); foreach (var fileSystemDescription in contentIdentity.FileSystemDescriptions) { var fileDescription = (FileDescription)fileSystemDescription; if (analysisMode == AnalysisModes.Smart) { - ClassicAssert.IsNull(fileDescription.SignatureGuid); + fileDescription.SignatureGuid.Should().BeNull(); } else { - ClassicAssert.IsTrue(fileDescription.SignatureGuid.IsNotEmpty(true)); + fileDescription.SignatureGuid.IsNotEmpty(true).Should().BeTrue(); } } - - // ClassicAssert.AreEqual(true, comparisonItem.ContentRepartition.IsOK); - // ClassicAssert.AreEqual(false, comparisonItem.ContentRepartition.IsSuccessStatus); - // ClassicAssert.AreEqual(false, comparisonItem.ContentRepartition.IsErrorStatus); - - ClassicAssert.IsTrue(comparisonItem.PathIdentity.FileName.StartsWith("file_")); - ClassicAssert.IsTrue(comparisonItem.PathIdentity.FileName.EndsWith(".txt")); + + comparisonItem.PathIdentity.FileName.Should().StartWith("file_"); + comparisonItem.PathIdentity.FileName.Should().EndWith(".txt"); } } diff --git a/tests/ByteSync.Client.IntegrationTests/Repositories/TestAtomicActionRepository.cs b/tests/ByteSync.Client.IntegrationTests/Repositories/TestAtomicActionRepository.cs new file mode 100644 index 000000000..33b342652 --- /dev/null +++ b/tests/ByteSync.Client.IntegrationTests/Repositories/TestAtomicActionRepository.cs @@ -0,0 +1,124 @@ +using Autofac; +using ByteSync.Business.Actions.Local; +using ByteSync.Business.Inventories; +using ByteSync.Common.Business.Inventories; +using ByteSync.Interfaces.Controls.Communications; +using ByteSync.Interfaces.Controls.Communications.SignalR; +using ByteSync.Interfaces.Repositories; +using ByteSync.Interfaces.Services.Communications; +using ByteSync.Interfaces.Services.Sessions; +using ByteSync.Models.Comparisons.Result; +using ByteSync.Repositories; +using ByteSync.Services.Communications; +using ByteSync.Services.Communications.SignalR; +using ByteSync.Services.Sessions; +using ByteSync.TestsCommon; + +namespace ByteSync.Client.IntegrationTests.Repositories; + +public class TestAtomicActionRepository : IntegrationTest +{ + private AtomicActionRepository _atomicActionRepository; + + [SetUp] + public void Setup() + { + RegisterType(); + RegisterType(); + RegisterType(); + RegisterType(); + RegisterType, ISessionInvalidationCachePolicy>(); + RegisterType, IPropertyIndexer>(); + RegisterType(); + BuildMoqContainer(); + + _testDirectoryService.CreateTestDirectory(); + _atomicActionRepository = Container.Resolve(); + } + + [Test] + public void AddOrUpdate_ShouldAddAtomicAction() + { + // Arrange + var pathIdentity = new PathIdentity(FileSystemTypes.File, "/fileToCopy1.txt", "fileToCopy1.txt", "/fileToCopy1.txt"); + var comparisonItem = new ComparisonItem(pathIdentity); + var atomicAction = new AtomicAction { AtomicActionId = "Action1", ComparisonItem = comparisonItem }; + + // Act + _atomicActionRepository.AddOrUpdate(atomicAction); + var result = _atomicActionRepository.GetAtomicActions(comparisonItem); + + // Assert + Assert.That(result, Has.Count.EqualTo(1)); + Assert.That(result.First().AtomicActionId, Is.EqualTo("Action1")); + } + + [Test] + public void Remove_ShouldRemoveAtomicAction() + { + // Arrange + var pathIdentity = new PathIdentity(FileSystemTypes.File, "/fileToCopy2.txt", "fileToCopy2.txt", "/fileToCopy2.txt"); + var comparisonItem = new ComparisonItem(pathIdentity); + var atomicAction = new AtomicAction { AtomicActionId = "Action2", ComparisonItem = comparisonItem }; + _atomicActionRepository.AddOrUpdate(atomicAction); + + // Act + _atomicActionRepository.Remove(atomicAction); + var result = _atomicActionRepository.GetAtomicActions(comparisonItem); + + // Assert + Assert.That(result, Is.Empty); + } + + [Test] + public void GetAtomicActions_ShouldReturnCorrectActionsForComparisonItem() + { + // Arrange + var pathIdentity1 = new PathIdentity(FileSystemTypes.File, "/fileToCopy1.txt", "fileToCopy1.txt", "/fileToCopy1.txt"); + var comparisonItem1 = new ComparisonItem(pathIdentity1); + var pathIdentity2 = new PathIdentity(FileSystemTypes.File, "/fileToCopy2.txt", "fileToCopy2.txt", "/fileToCopy2.txt"); + var comparisonItem2 = new ComparisonItem(pathIdentity2); + + var atomicAction1 = new AtomicAction { AtomicActionId = "Action3", ComparisonItem = comparisonItem1 }; + var atomicAction2 = new AtomicAction { AtomicActionId = "Action4", ComparisonItem = comparisonItem2 }; + + _atomicActionRepository.AddOrUpdate([atomicAction1, atomicAction2]); + + // Act + var result1 = _atomicActionRepository.GetAtomicActions(comparisonItem1); + var result2 = _atomicActionRepository.GetAtomicActions(comparisonItem2); + + // Assert + Assert.That(result1, Has.Count.EqualTo(1)); + Assert.That(result1.First().AtomicActionId, Is.EqualTo("Action3")); + + Assert.That(result2, Has.Count.EqualTo(1)); + Assert.That(result2.First().AtomicActionId, Is.EqualTo("Action4")); + } + + [Test] + public async Task GetAtomicActions_ShouldReturnEmptyAfterSessionReset() + { + // Arrange + var pathIdentity1 = new PathIdentity(FileSystemTypes.File, "/fileToCopy1.txt", "fileToCopy1.txt", "/fileToCopy1.txt"); + var comparisonItem1 = new ComparisonItem(pathIdentity1); + var pathIdentity2 = new PathIdentity(FileSystemTypes.File, "/fileToCopy2.txt", "fileToCopy2.txt", "/fileToCopy2.txt"); + var comparisonItem2 = new ComparisonItem(pathIdentity2); + + var atomicAction1 = new AtomicAction { AtomicActionId = "Action3", ComparisonItem = comparisonItem1 }; + var atomicAction2 = new AtomicAction { AtomicActionId = "Action4", ComparisonItem = comparisonItem2 }; + + _atomicActionRepository.AddOrUpdate([atomicAction1, atomicAction2]); + + // Act + var sessionService = Container.Resolve(); + await sessionService.ResetSession(); + + var result1 = _atomicActionRepository.GetAtomicActions(comparisonItem1); + var result2 = _atomicActionRepository.GetAtomicActions(comparisonItem2); + + // Assert + Assert.That(result1, Is.Empty); + Assert.That(result2, Is.Empty); + } +} diff --git a/tests/ByteSync.Client.Tests/Services/Actions/TestSharedActionsGroupComputer.cs b/tests/ByteSync.Client.Tests/Services/Actions/TestSharedActionsGroupComputer.cs index 727072ff1..3d80fcc97 100644 --- a/tests/ByteSync.Client.Tests/Services/Actions/TestSharedActionsGroupComputer.cs +++ b/tests/ByteSync.Client.Tests/Services/Actions/TestSharedActionsGroupComputer.cs @@ -56,7 +56,7 @@ public async Task Test_1Action_1Source_1Target() List> capturedGroups = new List>(); _sharedActionsGroupRepository.Setup(s => s.AddOrUpdate(It.IsAny>())) - .Callback>(groups => capturedGroups.Add(groups)); + .Callback>(groups => capturedGroups.Add(groups.ToList())); // sharedActionsGroupComputer = new SharedActionsGroupComputer(); // sharedActionsGroups = sharedActionsGroupComputer.ComputeSharedActionsGroups(sharedAtomicActions); @@ -164,7 +164,7 @@ public async Task Test_2Actions_1Source_2Targets_1() List> capturedGroups = new List>(); _sharedActionsGroupRepository.Setup(s => s.AddOrUpdate(It.IsAny>())) - .Callback>(groups => capturedGroups.Add(groups)); + .Callback>(groups => capturedGroups.Add(groups.ToList())); await _sharedActionsGroupComputer.ComputeSharedActionsGroups(); @@ -239,7 +239,7 @@ public async Task Test_2Actions_1Source_2Targets_2() List> capturedGroups = new List>(); _sharedActionsGroupRepository.Setup(s => s.AddOrUpdate(It.IsAny>())) - .Callback>(groups => capturedGroups.Add(groups)); + .Callback>(groups => capturedGroups.Add(groups.ToList())); await _sharedActionsGroupComputer.ComputeSharedActionsGroups(); Assert.That(capturedGroups.Count, Is.EqualTo(1)); @@ -312,7 +312,7 @@ public async Task Test_2Actions_1Source_2Targets_3() List> capturedGroups = new List>(); _sharedActionsGroupRepository.Setup(s => s.AddOrUpdate(It.IsAny>())) - .Callback>(groups => capturedGroups.Add(groups)); + .Callback>(groups => capturedGroups.Add(groups.ToList())); await _sharedActionsGroupComputer.ComputeSharedActionsGroups(); @@ -385,7 +385,7 @@ public async Task Test_2Actions_1Source_2Targets_4() List> capturedGroups = new List>(); _sharedActionsGroupRepository.Setup(s => s.AddOrUpdate(It.IsAny>())) - .Callback>(groups => capturedGroups.Add(groups)); + .Callback>(groups => capturedGroups.Add(groups.ToList())); await _sharedActionsGroupComputer.ComputeSharedActionsGroups(); @@ -458,7 +458,7 @@ public async Task Test_2Actions_1Source_2Targets_5() List> capturedGroups = new List>(); _sharedActionsGroupRepository.Setup(s => s.AddOrUpdate(It.IsAny>())) - .Callback>(groups => capturedGroups.Add(groups)); + .Callback>(groups => capturedGroups.Add(groups.ToList())); await _sharedActionsGroupComputer.ComputeSharedActionsGroups(); diff --git a/tests/ByteSync.Client.Tests/Services/Repositories/SessionMembersRepositoryTests.cs b/tests/ByteSync.Client.Tests/Services/Repositories/SessionMembersRepositoryTests.cs index 5adb0d7f8..f47351586 100644 --- a/tests/ByteSync.Client.Tests/Services/Repositories/SessionMembersRepositoryTests.cs +++ b/tests/ByteSync.Client.Tests/Services/Repositories/SessionMembersRepositoryTests.cs @@ -16,7 +16,7 @@ namespace ByteSync.Tests.Services.Repositories; public class SessionMembersRepositoryTests { private Mock _mockConnectionService; - private Mock> _mockSessionInvalidationCachePolicy; + private Mock> _mockSessionInvalidationCachePolicy; private SessionMemberRepository _sessionMemberRepository; @@ -27,7 +27,7 @@ public class SessionMembersRepositoryTests public void SetUp() { _mockConnectionService = new Mock(); - _mockSessionInvalidationCachePolicy = new Mock>(); + _mockSessionInvalidationCachePolicy = new Mock>(); _sessionMemberRepository = new SessionMemberRepository( _mockConnectionService.Object, diff --git a/tests/ByteSync.Client.Tests/Services/Sessions/SynchronizationServiceTests.cs b/tests/ByteSync.Client.Tests/Services/Sessions/SynchronizationServiceTests.cs index 6102d0e8b..70db5d84c 100644 --- a/tests/ByteSync.Client.Tests/Services/Sessions/SynchronizationServiceTests.cs +++ b/tests/ByteSync.Client.Tests/Services/Sessions/SynchronizationServiceTests.cs @@ -2,11 +2,14 @@ using ByteSync.Business.Sessions; using ByteSync.Common.Business.Sessions; using ByteSync.Common.Business.Sessions.Cloud; +using ByteSync.Common.Business.Synchronizations; using ByteSync.Interfaces.Controls.Communications.Http; +using ByteSync.Interfaces.Controls.Synchronizations; using ByteSync.Interfaces.Controls.TimeTracking; using ByteSync.Interfaces.Factories; using ByteSync.Interfaces.Services.Sessions; using ByteSync.Services.Synchronizations; +using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; @@ -22,8 +25,6 @@ public class SynchronizationServiceTests private Mock _synchronizationLooperFactoryMock; private Mock _timeTrackingCacheMock; private Mock> _loggerMock; - - // private SynchronizationService _synchronizationService; [SetUp] public void Setup() @@ -50,19 +51,115 @@ public async Task AbortSynchronization_CloudSession_AbortsSynchronization() _sessionServiceMock .SetupGet(x => x.SessionStatusObservable) .Returns(sessionStatusObservable); - // _connectionManagerMock - // .Setup(x => x.HubWrapper.RequestAbortSynchronization(cloudSession.SessionId)) - // .ReturnsAsync(new SynchronizationAbortRequest()); - // _connectionManagerMock.SetupPushHandler(); var synchronizationService = new SynchronizationService(_sessionServiceMock.Object, _sessionMemberServiceMock.Object, _synchronizationApiClientMock.Object, _synchronizationLooperFactoryMock.Object, _timeTrackingCacheMock.Object, _loggerMock.Object); // Act await synchronizationService.AbortSynchronization(); + } + + [Test] + public void SynchronizationStart_WhenDataTransmitted_ShouldStartSynchronizationLoop() + { + // Arrange + var cloudSession = new CloudSession(); + var sessionObservable = new BehaviorSubject(cloudSession); + _sessionServiceMock + .SetupGet(x => x.SessionObservable) + .Returns(sessionObservable); + + var sessionStatusObservable = new BehaviorSubject(SessionStatus.Preparation); + _sessionServiceMock + .SetupGet(x => x.SessionStatusObservable) + .Returns(sessionStatusObservable); + + var mockSynchronizationLooper = new Mock(); + _synchronizationLooperFactoryMock + .Setup(x => x.CreateSynchronizationLooper()) + .Returns(mockSynchronizationLooper.Object); + + var synchronizationService = new SynchronizationService( + _sessionServiceMock.Object, + _sessionMemberServiceMock.Object, + _synchronizationApiClientMock.Object, + _synchronizationLooperFactoryMock.Object, + _timeTrackingCacheMock.Object, + _loggerMock.Object + ); + + var synchronization = new Synchronization { SessionId = "test-session" }; + + // Act + synchronizationService.SynchronizationProcessData.SynchronizationStart.OnNext(synchronization); + synchronizationService.SynchronizationProcessData.SynchronizationDataTransmitted.OnNext(true); + + // Wait until the asynchronous task can start + Thread.Sleep(100); + + // Assert + _synchronizationLooperFactoryMock.Verify(x => x.CreateSynchronizationLooper(), Times.Once); + mockSynchronizationLooper.Verify(x => x.CloudSessionSynchronizationLoop(), Times.Once); + } + + [Test] + public async Task SynchronizationStart_WhenDataTransmitted_ShouldRunInSeparateTask() + { + // Arrange + var cloudSession = new CloudSession(); + var sessionObservable = new BehaviorSubject(cloudSession); + _sessionServiceMock + .SetupGet(x => x.SessionObservable) + .Returns(sessionObservable); + + var sessionStatusObservable = new BehaviorSubject(SessionStatus.Preparation); + _sessionServiceMock + .SetupGet(x => x.SessionStatusObservable) + .Returns(sessionStatusObservable); + + // Setup un ManualResetEvent pour capturer l'exécution de la méthode + var executionCaptured = new ManualResetEventSlim(false); + var executionThreadId = 0; + + var mockSynchronizationLooper = new Mock(); + mockSynchronizationLooper + .Setup(x => x.CloudSessionSynchronizationLoop()) + .Callback(() => { + executionThreadId = Environment.CurrentManagedThreadId; + executionCaptured.Set(); + }); + + _synchronizationLooperFactoryMock + .Setup(x => x.CreateSynchronizationLooper()) + .Returns(mockSynchronizationLooper.Object); + + var synchronizationService = new SynchronizationService( + _sessionServiceMock.Object, + _sessionMemberServiceMock.Object, + _synchronizationApiClientMock.Object, + _synchronizationLooperFactoryMock.Object, + _timeTrackingCacheMock.Object, + _loggerMock.Object + ); + + var synchronization = new Synchronization { SessionId = "test-session" }; + var currentThreadId = Environment.CurrentManagedThreadId; + + // Act + synchronizationService.SynchronizationProcessData.SynchronizationStart.OnNext(synchronization); + synchronizationService.SynchronizationProcessData.SynchronizationDataTransmitted.OnNext(true); + + // Attendre l'exécution avec timeout + var executed = executionCaptured.Wait(TimeSpan.FromSeconds(1)); // Assert - // _connectionManagerMock.Verify(x => x.HubWrapper.RequestAbortSynchronization(cloudSession.SessionId), Times.Once); - // _connectionManagerMock.Verify(); + executed.Should().BeTrue(); + currentThreadId.Should().NotBe(executionThreadId); + // Assert.IsTrue(executed, "La méthode CloudSessionSynchronizationLoop n'a pas été exécutée"); + // Assert.AreNotEqual(currentThreadId, executionThreadId, + // "La méthode CloudSessionSynchronizationLoop a été exécutée sur le même thread, elle devrait être dans un Task séparé"); + + _synchronizationLooperFactoryMock.Verify(x => x.CreateSynchronizationLooper(), Times.Once); + mockSynchronizationLooper.Verify(x => x.CloudSessionSynchronizationLoop(), Times.Once); } } \ No newline at end of file diff --git a/tests/ByteSync.Functions.IntegrationTests/GlobalTestSetup.cs b/tests/ByteSync.Functions.IntegrationTests/GlobalTestSetup.cs index c5bbcf285..2434c6456 100644 --- a/tests/ByteSync.Functions.IntegrationTests/GlobalTestSetup.cs +++ b/tests/ByteSync.Functions.IntegrationTests/GlobalTestSetup.cs @@ -51,7 +51,6 @@ private void ConfigureServices(IServiceCollection services) services.Configure(Configuration.GetSection("Redis")); services.Configure(Configuration.GetSection("BlobStorage")); services.Configure(Configuration.GetSection("SignalR")); - services.Configure(Configuration.GetSection("CosmosDb")); services.Configure(Configuration.GetSection("AppSettings")); } diff --git a/tests/ByteSync.ServerCommon.Tests/Commands/Inventories/SetLocalInventoryStatusCommandHandlerTests.cs b/tests/ByteSync.ServerCommon.Tests/Commands/Inventories/SetLocalInventoryStatusCommandHandlerTests.cs index 769a986cc..2d2beb4d3 100644 --- a/tests/ByteSync.ServerCommon.Tests/Commands/Inventories/SetLocalInventoryStatusCommandHandlerTests.cs +++ b/tests/ByteSync.ServerCommon.Tests/Commands/Inventories/SetLocalInventoryStatusCommandHandlerTests.cs @@ -179,4 +179,53 @@ public async Task SetLocalInventoryStatus_CreatesNewInventoryData_WhenInventoryD A.CallTo(() => _mockInventoryRepository.AddOrUpdate(sessionId, A>.Ignored)) .MustHaveHappenedOnceExactly(); } -} \ No newline at end of file + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task SetLocalInventoryStatus_HandlesRegardlessOfIsInventoryStarted(bool isInventoryStarted) + { + // Arrange + var sessionId = "testSession"; + var client = new Client { ClientId = "client1", ClientInstanceId = "clientInstanceId1" }; + var currentDate = DateTime.UtcNow; + var parameters = new UpdateSessionMemberGeneralStatusParameters + { + SessionId = sessionId, + UtcChangeDate = currentDate, + SessionMemberGeneralStatus = SessionMemberGeneralStatus.InventoryRunningAnalysis + }; + + var inventoryData = new InventoryData(sessionId) { IsInventoryStarted = isInventoryStarted }; + var inventoryMemberData = new InventoryMemberData + { + ClientInstanceId = client.ClientInstanceId, + SessionMemberGeneralStatus = SessionMemberGeneralStatus.InventoryFinished, + LastLocalInventoryStatusUpdate = null + }; + + A.CallTo(() => _mockInventoryMemberService.GetOrCreateInventoryMember(A.Ignored, sessionId, client)) + .Invokes(() => inventoryData.InventoryMembers.Add(inventoryMemberData)) + .Returns(inventoryMemberData); + + InventoryData? funcResult = null; + A.CallTo(() => _mockInventoryRepository.AddOrUpdate(A.Ignored, A>.Ignored)) + .Invokes((string _, Func func) => + { + funcResult = func(inventoryData); + }) + .ReturnsLazily(() => UpdateResultBuilder.BuildAddOrUpdateResult(funcResult, false)); + + var request = new SetLocalInventoryStatusRequest(client, parameters); + + // Act + var result = await _setLocalInventoryStatusCommandHandler.Handle(request, CancellationToken.None); + + // Assert + result.Should().Be(true); + inventoryData.InventoryMembers.Single().SessionMemberGeneralStatus.Should().Be(parameters.SessionMemberGeneralStatus); + inventoryData.InventoryMembers.Single().LastLocalInventoryStatusUpdate.Should().Be(parameters.UtcChangeDate); + A.CallTo(() => _mockInventoryRepository.AddOrUpdate(sessionId, A>.Ignored)) + .MustHaveHappenedOnceExactly(); + } +} diff --git a/tests/ByteSync.ServerCommon.Tests/Helpers/TestSettingsInitializer.cs b/tests/ByteSync.ServerCommon.Tests/Helpers/TestSettingsInitializer.cs index 8c136b5ed..f6e3582aa 100644 --- a/tests/ByteSync.ServerCommon.Tests/Helpers/TestSettingsInitializer.cs +++ b/tests/ByteSync.ServerCommon.Tests/Helpers/TestSettingsInitializer.cs @@ -8,7 +8,6 @@ public class TestSettingsInitializer private static IConfiguration? _config; private static RedisSettings? _redisSettings; - private static CosmosDbSettings? _cosmosDbSettings; public IConfiguration InitConfiguration() { @@ -33,18 +32,4 @@ public static RedisSettings GetRedisSettings() return _redisSettings; } - - public static CosmosDbSettings GetCosmosDbSettings() - { - if (_cosmosDbSettings == null) - { - _config ??= new TestSettingsInitializer().InitConfiguration(); - - var cosmosDbSettings = _config.GetSection("CosmosDb").Get(); - - _cosmosDbSettings = cosmosDbSettings; - } - - return _cosmosDbSettings; - } } \ No newline at end of file diff --git a/tests/ByteSync.ServerCommon.Tests/Repositories/ActionsGroupDefinitionsRepositoryTests.cs b/tests/ByteSync.ServerCommon.Tests/Repositories/ActionsGroupDefinitionsRepositoryTests.cs deleted file mode 100644 index 2eff5d3ff..000000000 --- a/tests/ByteSync.ServerCommon.Tests/Repositories/ActionsGroupDefinitionsRepositoryTests.cs +++ /dev/null @@ -1,126 +0,0 @@ -using ByteSync.Common.Business.Actions; -using ByteSync.Common.Business.Inventories; -using ByteSync.ServerCommon.Misc; -using ByteSync.ServerCommon.Repositories; -using ByteSync.ServerCommon.Tests.Helpers; -using FluentAssertions; -using Microsoft.Extensions.Options; - -namespace ByteSync.ServerCommon.Tests.Repositories; - -public class ActionsGroupDefinitionsRepositoryTests -{ - public ActionsGroupDefinitionsRepositoryTests() - { - - } - - [Test] - public async Task AddOrUpdateActionsGroupDefinitions_ShouldSaveActionsGroupDefinitions_IntegrationTest() - { - // Arrange - var cosmosDbSettings = TestSettingsInitializer.GetCosmosDbSettings(); - - ByteSyncDbContext byteSyncDbContext = new ByteSyncDbContext(Options.Create(cosmosDbSettings)); - await byteSyncDbContext.InitializeCosmosDb(); - - var repository = new ActionsGroupDefinitionsRepository(byteSyncDbContext); - - string sessionId = "sessionId_" + DateTime.Now.Ticks; - var actionsGroupId1 = "ActionsGroupId_1_" + DateTime.Now.Ticks; - var actionsGroupId2 = "ActionsGroupId_2_" + DateTime.Now.Ticks; - var actionsGroupDefinitions = new List - { - new () - { - Operator = ActionOperatorTypes.SynchronizeContentAndDate, - Size = 100, - Source = "SourceTest", - Targets = ["TargetTest"], - FileSystemType = FileSystemTypes.File, - CreationTimeUtc = DateTime.UtcNow.AddMinutes(-10), - LastWriteTimeUtc = DateTime.UtcNow.AddMinutes(-5), - AppliesOnlySynchronizeDate = false, - ActionsGroupId = actionsGroupId1, - }, - new () - { - Operator = ActionOperatorTypes.SynchronizeContentAndDate, - Size = 200, - Source = "SourceTest", - Targets = ["TargetTest"], - FileSystemType = FileSystemTypes.File, - CreationTimeUtc = DateTime.UtcNow.AddMinutes(-20), - LastWriteTimeUtc = DateTime.UtcNow.AddMinutes(-10), - AppliesOnlySynchronizeDate = false, - ActionsGroupId = actionsGroupId2, - }, - }; - - // Act - await repository.AddOrUpdateActionsGroupDefinitions(sessionId, actionsGroupDefinitions); - - // Assert - var countBefore = byteSyncDbContext.ActionsGroupDefinitions - .Count(e => e.SessionId == sessionId); - countBefore.Should().Be(2); - } - - [Test] - public async Task ResetSession_ShouldDeleteActionsGroupDefinitions_IntegrationTest() - { - // Arrange - var cosmosDbSettings = TestSettingsInitializer.GetCosmosDbSettings(); - - ByteSyncDbContext byteSyncDbContext = new ByteSyncDbContext(Options.Create(cosmosDbSettings)); - await byteSyncDbContext.InitializeCosmosDb(); - - var repository = new ActionsGroupDefinitionsRepository(byteSyncDbContext); - - string sessionId = "sessionId_" + DateTime.Now.Ticks; - var actionsGroupId1 = "ActionsGroupId_1_" + DateTime.Now.Ticks; - var actionsGroupId2 = "ActionsGroupId_2_" + DateTime.Now.Ticks; - var actionsGroupDefinitions = new List - { - new () - { - Operator = ActionOperatorTypes.SynchronizeContentAndDate, - Size = 100, - Source = "SourceTest", - Targets = ["TargetTest"], - FileSystemType = FileSystemTypes.File, - CreationTimeUtc = DateTime.UtcNow.AddMinutes(-10), - LastWriteTimeUtc = DateTime.UtcNow.AddMinutes(-5), - AppliesOnlySynchronizeDate = false, - ActionsGroupId = actionsGroupId1, - }, - new () - { - Operator = ActionOperatorTypes.SynchronizeContentAndDate, - Size = 200, - Source = "SourceTest", - Targets = ["TargetTest"], - FileSystemType = FileSystemTypes.File, - CreationTimeUtc = DateTime.UtcNow.AddMinutes(-20), - LastWriteTimeUtc = DateTime.UtcNow.AddMinutes(-10), - AppliesOnlySynchronizeDate = false, - ActionsGroupId = actionsGroupId2, - }, - }; - - await repository.AddOrUpdateActionsGroupDefinitions(sessionId, actionsGroupDefinitions); - - var countBefore = byteSyncDbContext.ActionsGroupDefinitions - .Count(e => e.SessionId == sessionId); - countBefore.Should().Be(2); - - // Act - await repository.DeleteActionsGroupDefinitions(sessionId); - - // Assert - var countAfter = byteSyncDbContext.ActionsGroupDefinitions - .Count(e => e.SessionId == sessionId); - - countAfter.Should().Be(0); - } -} \ No newline at end of file diff --git a/tests/ByteSync.ServerCommon.Tests/Repositories/SynchronizationRepositoryTests.cs b/tests/ByteSync.ServerCommon.Tests/Repositories/SynchronizationRepositoryTests.cs index d3904aba4..1b7c3bed1 100644 --- a/tests/ByteSync.ServerCommon.Tests/Repositories/SynchronizationRepositoryTests.cs +++ b/tests/ByteSync.ServerCommon.Tests/Repositories/SynchronizationRepositoryTests.cs @@ -16,9 +16,9 @@ namespace ByteSync.ServerCommon.Tests.Repositories; public class SynchronizationRepositoryTests { private SynchronizationRepository _repository; - private CacheRepository _cacheRepository; + private CacheRepository _synchronizationEntityCacheRepository; private RedisInfrastructureService _redisInfrastructureService; - private IActionsGroupDefinitionsRepository _actionsGroupDefRepo; + private CacheRepository _trackingActionEntityCacheRepository; [SetUp] public void SetUp() @@ -31,16 +31,17 @@ public void SetUp() Options.Create(redisSettings), cacheKeyFactory, loggerFactoryMock); - - _cacheRepository = new CacheRepository(_redisInfrastructureService); - _actionsGroupDefRepo = A.Fake(); + + _synchronizationEntityCacheRepository = new CacheRepository(_redisInfrastructureService); + _trackingActionEntityCacheRepository = new CacheRepository(_redisInfrastructureService); _repository = new SynchronizationRepository( _redisInfrastructureService, - _cacheRepository, - _actionsGroupDefRepo); + _synchronizationEntityCacheRepository, + _trackingActionEntityCacheRepository); } + [Test] public async Task AddSynchronization_IntegrationTest() { @@ -59,15 +60,10 @@ public async Task AddSynchronization_IntegrationTest() // Assert var cacheKey = _redisInfrastructureService.ComputeCacheKey(EntityType.Synchronization, sessionId); - var savedEntity = await _cacheRepository.Get(cacheKey); + var savedEntity = await _synchronizationEntityCacheRepository.Get(cacheKey); savedEntity.Should().NotBeNull(); savedEntity.Should().BeEquivalentTo(synchronizationEntity); - - A.CallTo(() => _actionsGroupDefRepo.AddOrUpdateActionsGroupDefinitions( - sessionId, - A>.That.IsSameSequenceAs(actionsGroupDefinitions))) - .MustHaveHappenedOnceExactly(); } [Test] @@ -78,20 +74,17 @@ public async Task ResetSession_IntegrationTest() var synchronizationEntity = new SynchronizationEntity { SessionId = sessionId }; var cacheKey = _redisInfrastructureService.ComputeCacheKey(EntityType.Synchronization, sessionId); - await _cacheRepository.Save(cacheKey, synchronizationEntity); + await _synchronizationEntityCacheRepository.Save(cacheKey, synchronizationEntity); // Verify entity exists before resetting - var entityBeforeReset = await _cacheRepository.Get(cacheKey); + var entityBeforeReset = await _synchronizationEntityCacheRepository.Get(cacheKey); entityBeforeReset.Should().NotBeNull(); // Act await _repository.ResetSession(sessionId); // Assert - var entityAfterReset = await _cacheRepository.Get(cacheKey); + var entityAfterReset = await _synchronizationEntityCacheRepository.Get(cacheKey); entityAfterReset.Should().BeNull(); - - A.CallTo(() => _actionsGroupDefRepo.DeleteActionsGroupDefinitions(sessionId)) - .MustHaveHappenedOnceExactly(); } } diff --git a/tests/ByteSync.ServerCommon.Tests/Repositories/TrackingActionRepositoryTests.cs b/tests/ByteSync.ServerCommon.Tests/Repositories/TrackingActionRepositoryTests.cs index 7fdcbe266..0b0afd341 100644 --- a/tests/ByteSync.ServerCommon.Tests/Repositories/TrackingActionRepositoryTests.cs +++ b/tests/ByteSync.ServerCommon.Tests/Repositories/TrackingActionRepositoryTests.cs @@ -1,10 +1,10 @@ using ByteSync.Common.Business.Actions; +using ByteSync.ServerCommon.Business.Repositories; using ByteSync.ServerCommon.Entities; using ByteSync.ServerCommon.Factories; using ByteSync.ServerCommon.Interfaces.Factories; using ByteSync.ServerCommon.Interfaces.Repositories; using ByteSync.ServerCommon.Interfaces.Services; -using ByteSync.ServerCommon.Misc; using ByteSync.ServerCommon.Repositories; using ByteSync.ServerCommon.Services; using ByteSync.ServerCommon.Tests.Helpers; @@ -21,10 +21,8 @@ public class TrackingActionRepositoryTests private TrackingActionRepository _repository; private IRedisInfrastructureService _redisInfrastructureService; private ISynchronizationRepository _synchronizationRepository; - private ITrackingActionEntityFactory _trackingActionEntityFactory; private ICacheRepository _cacheRepository; private ICacheRepository _synchronizationCacheRepository; - private ActionsGroupDefinitionsRepository _actionsGroupDefinitionsRepository; [SetUp] public void SetUp() @@ -33,47 +31,17 @@ public void SetUp() var cacheKeyFactory = new CacheKeyFactory(Options.Create(redisSettings)); var loggerFactoryMock = A.Fake(); var loggerMock = A.Fake>(); - var cosmosDbSettings = TestSettingsInitializer.GetCosmosDbSettings(); - ByteSyncDbContext byteSyncDbContext = new ByteSyncDbContext(Options.Create(cosmosDbSettings)); - byteSyncDbContext.InitializeCosmosDb().Wait(); - _actionsGroupDefinitionsRepository = new ActionsGroupDefinitionsRepository(byteSyncDbContext); - _trackingActionEntityFactory = new TrackingActionEntityFactory(_actionsGroupDefinitionsRepository); _synchronizationRepository = new SynchronizationRepository( new RedisInfrastructureService(Options.Create(redisSettings), cacheKeyFactory, loggerFactoryMock), new CacheRepository(new RedisInfrastructureService(Options.Create(redisSettings), cacheKeyFactory, loggerFactoryMock)), - _actionsGroupDefinitionsRepository); + new CacheRepository(new RedisInfrastructureService(Options.Create(redisSettings), cacheKeyFactory, loggerFactoryMock))); _redisInfrastructureService = new RedisInfrastructureService(Options.Create(redisSettings), cacheKeyFactory, loggerFactoryMock); _cacheRepository = new CacheRepository(_redisInfrastructureService); _synchronizationCacheRepository = new CacheRepository(_redisInfrastructureService); - _repository = new TrackingActionRepository(_redisInfrastructureService, _synchronizationRepository, _trackingActionEntityFactory, _cacheRepository, + _repository = new TrackingActionRepository(_redisInfrastructureService, _synchronizationRepository, _cacheRepository, _synchronizationCacheRepository, loggerMock); } - [Test] - public async Task GetOrBuild_WhenEntityExists_ShouldReturnExistingEntity() - { - // Arrange - var nowTicks = DateTime.Now.Ticks; - var sessionId = "session_" + nowTicks; - var actionsGroupId = "group_" + nowTicks; - var existingEntity = new TrackingActionEntity { ActionsGroupId = actionsGroupId }; - - List actionsGroupDefinitions = - [ - new() - { - ActionsGroupId = actionsGroupId, - } - ]; - await _actionsGroupDefinitionsRepository.AddOrUpdateActionsGroupDefinitions(sessionId, actionsGroupDefinitions); - - // Act - var result = await _repository.GetOrBuild(sessionId, actionsGroupId); - - // Assert - result.Should().BeEquivalentTo(existingEntity); - } - [Test] public async Task AddOrUpdate_WhenAllUpdatesSucceed_ShouldReturnUpdatedEntities() { diff --git a/tests/ByteSync.ServerCommon.Tests/Services/SynchronizationServiceTests.cs b/tests/ByteSync.ServerCommon.Tests/Services/SynchronizationServiceTests.cs index 9491afaf8..76d952672 100644 --- a/tests/ByteSync.ServerCommon.Tests/Services/SynchronizationServiceTests.cs +++ b/tests/ByteSync.ServerCommon.Tests/Services/SynchronizationServiceTests.cs @@ -125,13 +125,12 @@ public async Task StartSynchronization_WhenNoExistingSynchronization_CreatesNewS A.CallTo(() => _synchronizationRepository.AddSynchronization(A._, actionsGroupDefinitions)) .Returns(Task.CompletedTask); A.CallTo(() => _synchronizationProgressService.InformSynchronizationStarted(A._, client)) - .Returns(synchronization); + .Returns(Task.CompletedTask); // Act - var result = await _synchronizationService.StartSynchronization(sessionId, client, actionsGroupDefinitions); + await _synchronizationService.StartSynchronization(sessionId, client, actionsGroupDefinitions); // Assert - result.Should().BeSameAs(synchronization); A.CallTo(() => _synchronizationRepository.AddSynchronization(A._, actionsGroupDefinitions)).MustHaveHappenedOnceExactly(); A.CallTo(() => _synchronizationProgressService.InformSynchronizationStarted(A._, client)).MustHaveHappenedOnceExactly(); } @@ -151,12 +150,10 @@ public async Task StartSynchronization_WhenSynchronizationExists_ReturnsExisting .Returns(synchronization); // Act - var result = await _synchronizationService.StartSynchronization(sessionId, client, actionsGroupDefinitions); + await _synchronizationService.StartSynchronization(sessionId, client, actionsGroupDefinitions); // Assert - result.Should().BeSameAs(synchronization); A.CallTo(() => _synchronizationRepository.AddSynchronization(A._, actionsGroupDefinitions)).MustNotHaveHappened(); - A.CallTo(() => _synchronizationProgressService.MapToSynchronization(synchronizationEntity)).MustHaveHappened(); } [Test] @@ -239,7 +236,7 @@ public async Task OnFilePartIsUploadedAsync_WhenCheckSynchronizationSuccess_Runs trackingActionEntity.TargetClientInstanceIds.Add("targetClientInstanceId"); SynchronizationEntity synchronizationEntity = new SynchronizationEntity(); - A.CallTo(() => _trackingActionRepository.GetOrBuild(sessionId, "ActionGroupId")) + A.CallTo(() => _trackingActionRepository.GetOrThrow(sessionId, "ActionGroupId")) .Returns(trackingActionEntity); A.CallTo(() => _synchronizationStatusCheckerService.CheckSynchronizationCanBeUpdated(synchronizationEntity)) @@ -254,7 +251,7 @@ public async Task OnFilePartIsUploadedAsync_WhenCheckSynchronizationSuccess_Runs // Act await _synchronizationService.OnFilePartIsUploadedAsync(sharedFileDefinition, 1); - A.CallTo(() => _trackingActionRepository.GetOrBuild(sessionId, "ActionGroupId")) + A.CallTo(() => _trackingActionRepository.GetOrThrow(sessionId, "ActionGroupId")) .MustHaveHappenedOnceExactly(); A.CallTo(() => _synchronizationStatusCheckerService.CheckSynchronizationCanBeUpdated(synchronizationEntity)) .MustHaveHappenedOnceExactly();