private static Result <ModelOutput> Predict(RedisClusterSize currentClusterSize, ModelContext modelContext) { // TODO: autoscaler should consider the server load percentage as well. If a shard had a very high load // percentage, it means that it is for some reason receiving an uneven load. Hence, adding shards helps in // this situation. There is no easy way to add that to the current model. Ideas: // - If any server reached a load >70% at any time in the period analyzed, we need to guarantee that // there's at least as many shards as there were before (i.e. no downscales are allowed). var shortestPaths = ComputeAllowedPaths(currentClusterSize, modelContext); var eligibleClusterSizes = shortestPaths .Select(kvp => (Size: kvp.Key, Node: kvp.Value)) // Find all plans that we can reach from the current one via scaling operations, and that we allow scaling to .Where(entry => entry.Node.ShortestDistanceFromSource != double.PositiveInfinity && IsScalingAllowed(currentClusterSize, entry.Size, modelContext)) // Compute the cost of taking the given route .Select(entry => (entry.Size, entry.Node, Cost: CostFunction(currentClusterSize, entry.Size, modelContext, shortestPaths))) .ToList(); // Rank them by cost ascending var costSorted = eligibleClusterSizes .OrderBy(pair => pair.Cost) .ToList(); if (costSorted.Count == 0) { return(new Result <ModelOutput>(errorMessage: "No cluster size available for scaling")); } return(new ModelOutput( targetClusterSize: costSorted[0].Size, modelContext: modelContext, cost: costSorted[0].Cost, scalePath: RedisScalingUtilities.ComputeShortestPath(shortestPaths, currentClusterSize, costSorted[0].Size))); }
private Result <ModelOutput> Predict(RedisClusterSize currentClusterSize, ModelContext modelContext) { var shortestPaths = ComputeAllowedPaths(currentClusterSize, modelContext); var eligibleClusterSizes = shortestPaths .Select(kvp => (Size: kvp.Key, Node: kvp.Value)) // Find all plans that we can reach from the current one via scaling operations, and that we allow scaling to .Where(entry => entry.Node.ShortestDistanceFromSource != double.PositiveInfinity && IsScalingAllowed(currentClusterSize, entry.Size, modelContext)) // Compute the cost of taking the given route .Select(entry => (entry.Size, entry.Node, Cost: CostFunction(currentClusterSize, entry.Size, modelContext, shortestPaths))) .ToList(); // Rank them by cost ascending var costSorted = eligibleClusterSizes .OrderBy(pair => pair.Cost) .ToList(); if (costSorted.Count == 0) { return(new Result <ModelOutput>(errorMessage: "No cluster size available for scaling")); } return(new ModelOutput( targetClusterSize: costSorted[0].Size, modelContext: modelContext, cost: costSorted[0].Cost, scalePath: RedisScalingUtilities.ComputeShortestPath(shortestPaths, currentClusterSize, costSorted[0].Size))); }
public void FailsOnNonExistantRoute() { var from = RedisClusterSize.Parse("P1/1"); var to = RedisClusterSize.Parse("P3/3"); var path = RedisScalingUtilities.ComputeShortestPath(from, to, size => new RedisClusterSize[] { }, (f, t) => 1); path.Should().BeEmpty(); }
public void SucceedsOnSimpleRoute() { var from = RedisClusterSize.Parse("P1/1"); var to = RedisClusterSize.Parse("P3/3"); var path = RedisScalingUtilities.ComputeShortestPath(from, to, size => size.ScaleEligibleSizes, (f, t) => 1); path.Should().BeEquivalentTo(new RedisClusterSize[] { RedisClusterSize.Parse("P3/1"), RedisClusterSize.Parse("P3/3") }); }
public void CanFindEmptyRoute() { var from = RedisClusterSize.Parse("P1/1"); var to = RedisClusterSize.Parse("P1/1"); var path = RedisScalingUtilities.ComputeShortestPath(from, to, size => size.ScaleEligibleSizes, (f, t) => 1); path.Should().BeEmpty(); }
public void CanFindSingleRoute() { var from = RedisClusterSize.Parse("P1/1"); var to = RedisClusterSize.Parse("P1/2"); var path = RedisScalingUtilities.ComputeShortestPath(from, to, size => size.ScaleEligibleSizes, (f, t) => 1); path.Count.Should().Be(1); path[0].Should().Be(to); }
/// <summary> /// This function embodies the concept of "how much does it cost to switch from /// <paramref name="current"/> to <paramref name="target"/>". At this point, we can assume that: /// - The two input sizes are valid states to be in /// - We can reach the target from current via some amount of autoscaling operations /// Hence, we're just ranking amonst the many potential states. /// </summary> private static double CostFunction(RedisClusterSize current, RedisClusterSize target, ModelContext modelContext, IReadOnlyDictionary <RedisClusterSize, RedisScalingUtilities.Node> shortestPaths) { // Switching to the same size (i.e. no op) is free if (current.Equals(target)) { return(0); } var shortestPath = RedisScalingUtilities.ComputeShortestPath(shortestPaths, current, target); Contract.Assert(shortestPath.Count > 0); // Positive if we are spending more money, negative if we are saving return((double)(target.MonthlyCostUsd - current.MonthlyCostUsd)); }