public void LeastShardAllocationStrategy_should_rebalance_from_region_with_most_number_of_shards()
        {
            var allocations = new Dictionary <IActorRef, IImmutableList <string> >
            {
                { _regionA, new [] { "shard1" }.ToImmutableList() },
                { _regionB, new [] { "shard2", "shard3" }.ToImmutableList() },
                { _regionC, ImmutableList <string> .Empty }
            }.ToImmutableDictionary();

            // so far regionB has 2 shards and regionC has 0 shards, but the diff is less than rebalanceThreshold
            var r1 = _allocationStrategy.Rebalance(allocations, ImmutableHashSet <string> .Empty).Result;

            r1.Count.Should().Be(0);

            allocations = allocations.SetItem(_regionB, new[] { "shard2", "shard3", "shard4" }.ToImmutableList());
            var r2 = _allocationStrategy.Rebalance(allocations, ImmutableHashSet <string> .Empty).Result;

            r2.Should().BeEquivalentTo(new[] { "shard2", "shard3" });

            var r3 = _allocationStrategy.Rebalance(allocations, ImmutableHashSet.Create("shard4")).Result;

            r3.Count.Should().Be(0);

            allocations = allocations.SetItem(_regionA, new[] { "shard1", "shard5", "shard6" }.ToImmutableList());
            var r4 = _allocationStrategy.Rebalance(allocations, ImmutableHashSet.Create("shard1")).Result;

            r4.Should().BeEquivalentTo(new[] { "shard2" });
        }
        private void TestRebalance(
            IShardAllocationStrategy allocationStrategy,
            IImmutableDictionary <IActorRef, IImmutableList <string> > allocations,
            ImmutableList <IImmutableDictionary <IActorRef, IImmutableList <string> > > steps,
            int maxSteps)
        {
            var round           = steps.Count;
            var rebalanceResult = allocationStrategy.Rebalance(allocations, ImmutableHashSet <string> .Empty).Result;
            var newAllocations  = LeastShardAllocationStrategySpec.AfterRebalance(allocationStrategy, allocations, rebalanceResult);

            LeastShardAllocationStrategySpec.CountShards(newAllocations).Should().Be(LeastShardAllocationStrategySpec.CountShards(allocations), $"test {allocationStrategy}[{ string.Join(", ", LeastShardAllocationStrategySpec.CountShardsPerRegion(allocations))}]: ");
            var min      = LeastShardAllocationStrategySpec.CountShardsPerRegion(newAllocations).Min();
            var max      = LeastShardAllocationStrategySpec.CountShardsPerRegion(newAllocations).Max();
            var diff     = max - min;
            var newSteps = steps.Add(newAllocations);

            if (diff <= 1)
            {
                if (round >= 3 && maxSteps <= 10)
                {
                    // Should be very rare (I have not seen it)
                    Sys.Log.Info($"rebalance solved in round {round}, [{string.Join(" => ", newSteps.Select(step => string.Join(", ", LeastShardAllocationStrategySpec.CountShardsPerRegion(step))))}]");
                }
            }
            else if (round == maxSteps)
            {
                throw new AssertionFailedException($"Couldn't solve rebalance in $round rounds, [{string.Join(" => ", newSteps.Select(step => string.Join(", ", LeastShardAllocationStrategySpec.CountShardsPerRegion(step))))}]");
            }
            else
            {
                TestRebalance(allocationStrategy, newAllocations, newSteps, maxSteps);
            }
        }
        public void LeastShardAllocationStrategy_should_reallocate_from_region_with_most_number_of_shards()
        {
            var allocations = new Dictionary <IActorRef, IImmutableList <string> >
            {
                { _regionA, new [] { "shard1" }.ToImmutableList() },
                { _regionB, new [] { "shard2", "shard3" }.ToImmutableList() },
                { _regionC, ImmutableList <string> .Empty }
            }.ToImmutableDictionary();

            // so far regionB has 2 shards and regionC has 0 shards, but the diff is less than rebalanceThreshold
            var r1 = _allocationStrategy.Rebalance(allocations, ImmutableHashSet <string> .Empty).Result;

            Assert.Equal(r1.Count, 0);

            allocations = allocations.SetItem(_regionB, new[] { "shard2", "shard3", "shard4" }.ToImmutableList());
            var r2 = _allocationStrategy.Rebalance(allocations, ImmutableHashSet <string> .Empty).Result;

            Assert.Equal(r2.Count, 1);
            Assert.Equal(r2.First(), "shard2");

            var r3 = _allocationStrategy.Rebalance(allocations, ImmutableHashSet <string> .Empty.Add("shard4")).Result;

            Assert.Equal(r3.Count, 0);

            allocations = allocations.SetItem(_regionA, new[] { "shard1", "shard5", "shard6" }.ToImmutableList());
            var r4 = _allocationStrategy.Rebalance(allocations, ImmutableHashSet <string> .Empty.Add("shard1")).Result;

            Assert.Equal(r4.Count, 1);
            Assert.Equal(r2.First(), "shard2");
        }
 private void HandleRebalanceTick()
 {
     if (_currentState.Regions.Count != 0)
     {
         var shardsTask = AllocationStrategy.Rebalance(_currentState.Regions, _rebalanceInProgress);
         if (shardsTask.IsCompleted && !shardsTask.IsFaulted)
         {
             ContinueRebalance(shardsTask.Result);
         }
         else
         {
             shardsTask.ContinueWith(t => !(t.IsFaulted || t.IsCanceled)
                 ? new RebalanceResult(t.Result)
                 : new RebalanceResult(Enumerable.Empty <ShardId>()))
             .PipeTo(Self);
         }
     }
 }
 private void HandleRebalanceTick()
 {
     if (_currentState.Regions.Count != 0)
     {
         var shardsTask = AllocationStrategy.Rebalance(_currentState.Regions, _rebalanceInProgress);
         if (shardsTask.IsCompleted && !shardsTask.IsFaulted)
         {
             ContinueRebalance(shardsTask.Result);
         }
         else
         {
             shardsTask.ContinueWith(t => !(t.IsFaulted || t.IsCanceled)
                 ? new RebalanceResult(t.Result)
                 : new RebalanceResult(ImmutableHashSet <ShardId> .Empty), TaskContinuationOptions.ExecuteSynchronously)
             .PipeTo(Self);
         }
     }
 }