diff --git a/Orleans.sln b/Orleans.sln
index d5e8786f4a..fdb05321bf 100644
--- a/Orleans.sln
+++ b/Orleans.sln
@@ -242,6 +242,12 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Orleans.Serialization.FShar
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Orleans.Serialization.MessagePack", "src\Orleans.Serialization.MessagePack\Orleans.Serialization.MessagePack.csproj", "{F50F81B6-E9B5-4143-B66B-A1AD913F6E9C}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ActivationRebalancing", "ActivationRebalancing", "{B0DC8B8D-29CD-4CA3-A874-471F75595829}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ActivationRebalancing.Cluster", "playground\ActivationRebalancing\ActivationRebalancing.Cluster\ActivationRebalancing.Cluster.csproj", "{2D109E60-E9BF-4F57-BBCD-DF5FA7768B00}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ActivationRebalancing.Frontend", "playground\ActivationRebalancing\ActivationRebalancing.Frontend\ActivationRebalancing.Frontend.csproj", "{DFAF9FFC-EBD9-45F0-A121-010D29A296C1}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ChaoticCluster", "ChaoticCluster", "{2579A7F6-EBE8-485A-BB20-A5D19DB5612B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChaoticCluster.AppHost", "playground\ChaoticCluster\ChaoticCluster.AppHost\ChaoticCluster.AppHost.csproj", "{4E79EC4B-2DC4-41E3-9AE6-17C1FFF17B02}"
@@ -642,6 +648,14 @@ Global
{F50F81B6-E9B5-4143-B66B-A1AD913F6E9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F50F81B6-E9B5-4143-B66B-A1AD913F6E9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F50F81B6-E9B5-4143-B66B-A1AD913F6E9C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2D109E60-E9BF-4F57-BBCD-DF5FA7768B00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2D109E60-E9BF-4F57-BBCD-DF5FA7768B00}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2D109E60-E9BF-4F57-BBCD-DF5FA7768B00}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2D109E60-E9BF-4F57-BBCD-DF5FA7768B00}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DFAF9FFC-EBD9-45F0-A121-010D29A296C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DFAF9FFC-EBD9-45F0-A121-010D29A296C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DFAF9FFC-EBD9-45F0-A121-010D29A296C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DFAF9FFC-EBD9-45F0-A121-010D29A296C1}.Release|Any CPU.Build.0 = Release|Any CPU
{4E79EC4B-2DC4-41E3-9AE6-17C1FFF17B02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E79EC4B-2DC4-41E3-9AE6-17C1FFF17B02}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E79EC4B-2DC4-41E3-9AE6-17C1FFF17B02}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -774,6 +788,9 @@ Global
{84B44F1D-B7FE-40E3-82F0-730A55AC8613} = {316CDCC7-323F-4264-9FC9-667662BB1F80}
{B2D53D3C-E44A-4C9B-AAEE-28FB8C1BDF62} = {A6573187-FD0D-4DF7-91D1-03E07E470C0A}
{F50F81B6-E9B5-4143-B66B-A1AD913F6E9C} = {4CD3AA9E-D937-48CA-BB6C-158E12257D23}
+ {B0DC8B8D-29CD-4CA3-A874-471F75595829} = {A41DE3D1-F8AA-4234-BE6F-3C9646A1507A}
+ {2D109E60-E9BF-4F57-BBCD-DF5FA7768B00} = {B0DC8B8D-29CD-4CA3-A874-471F75595829}
+ {DFAF9FFC-EBD9-45F0-A121-010D29A296C1} = {B0DC8B8D-29CD-4CA3-A874-471F75595829}
{2579A7F6-EBE8-485A-BB20-A5D19DB5612B} = {A41DE3D1-F8AA-4234-BE6F-3C9646A1507A}
{4E79EC4B-2DC4-41E3-9AE6-17C1FFF17B02} = {2579A7F6-EBE8-485A-BB20-A5D19DB5612B}
{76A549FA-69F1-4967-82B6-161A8B52C86B} = {2579A7F6-EBE8-485A-BB20-A5D19DB5612B}
diff --git a/playground/ActivationRebalancing/ActivationRebalancing.Cluster/ActivationRebalancing.Cluster.csproj b/playground/ActivationRebalancing/ActivationRebalancing.Cluster/ActivationRebalancing.Cluster.csproj
new file mode 100644
index 0000000000..2a145fa58d
--- /dev/null
+++ b/playground/ActivationRebalancing/ActivationRebalancing.Cluster/ActivationRebalancing.Cluster.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ true
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/playground/ActivationRebalancing/ActivationRebalancing.Cluster/Program.cs b/playground/ActivationRebalancing/ActivationRebalancing.Cluster/Program.cs
new file mode 100644
index 0000000000..3d36dc2e62
--- /dev/null
+++ b/playground/ActivationRebalancing/ActivationRebalancing.Cluster/Program.cs
@@ -0,0 +1,170 @@
+using System.Diagnostics;
+using System.Net;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Orleans.Configuration;
+using Orleans.Runtime.Placement;
+
+#nullable enable
+
+// Ledjon: The silos will run in the same process so they will have the same memory usage.
+// I previously had 4 console apps to run the example, but didn't want to add so many proj into the solution.
+// I am sure with something like Aspire that would be easier, but for now I'll leave them like this.
+// You (the reader) feel free to run this in different processes for a more realistic example.
+
+var host0 = await StartSiloHost(0);
+var host1 = await StartSiloHost(1);
+var host2 = await StartSiloHost(2);
+var host3 = await StartSiloHost(3);
+IHost? host5 = null;
+
+Console.WriteLine("All silos have started.");
+
+var grainFactory = host0.Services.GetRequiredService();
+var mgmtGrain = grainFactory.GetGrain(0);
+
+var silos = await mgmtGrain.GetHosts(onlyActive: true);
+Debug.Assert(silos.Count == 4);
+var addresses = silos.Select(x => x.Key).ToArray();
+
+var tasks = new List();
+RequestContext.Set(IPlacementDirector.PlacementHintKey, addresses[0]);
+for (var i = 0; i < 300; i++)
+{
+ tasks.Add(grainFactory.GetGrain(Guid.NewGuid()).Ping());
+}
+
+RequestContext.Set(IPlacementDirector.PlacementHintKey, addresses[1]);
+for (var i = 0; i < 30; i++)
+{
+ tasks.Add(grainFactory.GetGrain(Guid.NewGuid()).Ping());
+}
+
+RequestContext.Set(IPlacementDirector.PlacementHintKey, addresses[2]);
+for (var i = 0; i < 410; i++)
+{
+ tasks.Add(grainFactory.GetGrain(Guid.NewGuid()).Ping());
+}
+
+RequestContext.Set(IPlacementDirector.PlacementHintKey, addresses[3]);
+for (var i = 0; i < 120; i++)
+{
+ tasks.Add(grainFactory.GetGrain(Guid.NewGuid()).Ping());
+}
+
+var sessionCount = 0;
+while (true)
+{
+ if (sessionCount == 25)
+ {
+ RequestContext.Set(IPlacementDirector.PlacementHintKey, addresses[0]);
+ for (var i = 0; i < 50; i++)
+ {
+ tasks.Add(grainFactory.GetGrain(Guid.NewGuid()).Ping());
+ }
+
+ RequestContext.Set(IPlacementDirector.PlacementHintKey, addresses[1]);
+ for (var i = 0; i < 50; i++)
+ {
+ tasks.Add(grainFactory.GetGrain(Guid.NewGuid()).Ping());
+ }
+ }
+
+ if (sessionCount == 35)
+ {
+ RequestContext.Set(IPlacementDirector.PlacementHintKey, addresses[1]);
+ for (var i = 0; i < 50; i++)
+ {
+ tasks.Add(grainFactory.GetGrain(Guid.NewGuid()).Ping());
+ }
+
+ RequestContext.Set(IPlacementDirector.PlacementHintKey, addresses[2]);
+ for (var i = 0; i < 50; i++)
+ {
+ tasks.Add(grainFactory.GetGrain(Guid.NewGuid()).Ping());
+ }
+ }
+
+ if (sessionCount == 40)
+ {
+ host5 = await StartSiloHost(4);
+ }
+
+ if (sessionCount == 45)
+ {
+ RequestContext.Set(IPlacementDirector.PlacementHintKey, addresses[2]);
+ for (var i = 0; i < 50; i++)
+ {
+ tasks.Add(grainFactory.GetGrain(Guid.NewGuid()).Ping());
+ }
+
+ RequestContext.Set(IPlacementDirector.PlacementHintKey, addresses[3]);
+ for (var i = 0; i < 50; i++)
+ {
+ tasks.Add(grainFactory.GetGrain(Guid.NewGuid()).Ping());
+ }
+ }
+
+ await Task.Delay(5000); // session duration
+ sessionCount++;
+
+ if (sessionCount > 55)
+ {
+ break;
+ }
+}
+
+Console.WriteLine("Simulation has finished. Press Enter to terminate...");
+Console.ReadLine();
+
+await host0.StopAsync();
+await host1.StopAsync();
+await host2.StopAsync();
+await host3.StopAsync();
+
+if (host5 != null)
+{
+ await host5.StopAsync();
+}
+
+static async Task StartSiloHost(int num)
+{
+ #pragma warning disable ORLEANSEXP002
+ var host = Host.CreateDefaultBuilder()
+ .ConfigureLogging(builder => builder
+ .AddFilter("", LogLevel.Error)
+ .AddFilter("Orleans.Runtime.Placement.Rebalancing", LogLevel.Trace)
+ .AddConsole())
+ .UseOrleans(builder => builder
+ .Configure(o =>
+ {
+ o.RebalancerDueTime = TimeSpan.FromSeconds(5);
+ o.SessionCyclePeriod = TimeSpan.FromSeconds(5);
+ // uncomment these below, if you want higher migration rate
+ //o.CycleNumberWeight = 1;
+ //o.SiloNumberWeight = 0;
+ })
+ .UseLocalhostClustering(
+ siloPort: EndpointOptions.DEFAULT_SILO_PORT + num,
+ gatewayPort: EndpointOptions.DEFAULT_GATEWAY_PORT + num,
+ primarySiloEndpoint: new IPEndPoint(IPAddress.Loopback, EndpointOptions.DEFAULT_SILO_PORT))
+ .AddActivationRebalancer())
+ .Build();
+ #pragma warning restore ORLEANSEXP002
+
+ await host.StartAsync();
+ Console.WriteLine($"Silo{num} started.");
+
+ return host;
+}
+
+public interface IRebalancingTestGrain : IGrainWithGuidKey
+{
+ Task Ping();
+}
+
+public class RebalancingTestGrain : Grain, IRebalancingTestGrain
+{
+ public Task Ping() => Task.CompletedTask;
+}
\ No newline at end of file
diff --git a/playground/ActivationRebalancing/ActivationRebalancing.Frontend/ActivationRebalancing.Frontend.csproj b/playground/ActivationRebalancing/ActivationRebalancing.Frontend/ActivationRebalancing.Frontend.csproj
new file mode 100644
index 0000000000..63b5c91516
--- /dev/null
+++ b/playground/ActivationRebalancing/ActivationRebalancing.Frontend/ActivationRebalancing.Frontend.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/playground/ActivationRebalancing/ActivationRebalancing.Frontend/Controllers/StatsController.cs b/playground/ActivationRebalancing/ActivationRebalancing.Frontend/Controllers/StatsController.cs
new file mode 100644
index 0000000000..7d7261ae0b
--- /dev/null
+++ b/playground/ActivationRebalancing/ActivationRebalancing.Frontend/Controllers/StatsController.cs
@@ -0,0 +1,36 @@
+using Microsoft.AspNetCore.Mvc;
+using Orleans.Runtime;
+using Orleans;
+
+namespace ActivationRebalancing.Frontend.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+public class StatsController(IClusterClient clusterClient) : ControllerBase
+{
+ [HttpGet("silos")]
+ public async Task GetStats()
+ {
+ var grainStats = await clusterClient
+ .GetGrain(0)
+ .GetDetailedGrainStatistics();
+
+ var siloData = grainStats.GroupBy(stat => stat.SiloAddress)
+ .Select(g => new SiloData(g.Key.ToString(), g.Count()))
+ .ToList();
+
+ if (siloData.Count == 4)
+ {
+ siloData = [.. siloData, new SiloData("x", 0)];
+ }
+
+ if (siloData.Count > 5)
+ {
+ throw new NotSupportedException("The frontend cant support more than 6 silos");
+ }
+
+ return Ok(siloData);
+ }
+}
+
+public record SiloData(string Host, int Activations);
\ No newline at end of file
diff --git a/playground/ActivationRebalancing/ActivationRebalancing.Frontend/Program.cs b/playground/ActivationRebalancing/ActivationRebalancing.Frontend/Program.cs
new file mode 100644
index 0000000000..2dbec59e39
--- /dev/null
+++ b/playground/ActivationRebalancing/ActivationRebalancing.Frontend/Program.cs
@@ -0,0 +1,17 @@
+using Orleans.Hosting;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.UseOrleansClient(clientBuilder => clientBuilder.UseLocalhostClustering());
+builder.Services.AddControllers();
+
+var app = builder.Build();
+
+var options = new DefaultFilesOptions();
+options.DefaultFileNames.Clear();
+options.DefaultFileNames.Add("index.html");
+
+app.UseDefaultFiles(options);
+app.UseStaticFiles();
+app.MapControllers();
+app.Run();
diff --git a/playground/ActivationRebalancing/ActivationRebalancing.Frontend/Properties/launchSettings.json b/playground/ActivationRebalancing/ActivationRebalancing.Frontend/Properties/launchSettings.json
new file mode 100644
index 0000000000..730bdb6499
--- /dev/null
+++ b/playground/ActivationRebalancing/ActivationRebalancing.Frontend/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "index.html",
+ "applicationUrl": "http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/playground/ActivationRebalancing/ActivationRebalancing.Frontend/appsettings.Development.json b/playground/ActivationRebalancing/ActivationRebalancing.Frontend/appsettings.Development.json
new file mode 100644
index 0000000000..0c208ae918
--- /dev/null
+++ b/playground/ActivationRebalancing/ActivationRebalancing.Frontend/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/playground/ActivationRebalancing/ActivationRebalancing.Frontend/appsettings.json b/playground/ActivationRebalancing/ActivationRebalancing.Frontend/appsettings.json
new file mode 100644
index 0000000000..10f68b8c8b
--- /dev/null
+++ b/playground/ActivationRebalancing/ActivationRebalancing.Frontend/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/playground/ActivationRebalancing/ActivationRebalancing.Frontend/wwwroot/index.html b/playground/ActivationRebalancing/ActivationRebalancing.Frontend/wwwroot/index.html
new file mode 100644
index 0000000000..6ca0d92a03
--- /dev/null
+++ b/playground/ActivationRebalancing/ActivationRebalancing.Frontend/wwwroot/index.html
@@ -0,0 +1,213 @@
+
+
+
+
+
+ Orleans Activation Rebalancing
+
+
+
+
+ Orleans Activation Rebalancing
+
+
+
+
+
+
diff --git a/playground/ActivationRebalancing/ActivationRebalancing.Frontend/wwwroot/worker.js b/playground/ActivationRebalancing/ActivationRebalancing.Frontend/wwwroot/worker.js
new file mode 100644
index 0000000000..90696044c0
--- /dev/null
+++ b/playground/ActivationRebalancing/ActivationRebalancing.Frontend/wwwroot/worker.js
@@ -0,0 +1,29 @@
+self.onmessage = function (e) {
+ try {
+ const data = e.data;
+ const totalActivations = data.reduce((sum, d) => sum + d.activations, 0);
+ const densityMatrix = Array.from({ length: 20 }, () => Array(20).fill('white'));
+
+ data.forEach((d, i) => {
+ const numCells = Math.round(d.activations / totalActivations * 400);
+ const color = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff'][i % 5];
+
+ let cellsFilled = 0;
+ const maxFillWhiteCellAttempts = 500;
+
+ for (let attempt = 0; attempt < maxFillWhiteCellAttempts && cellsFilled < numCells; attempt++) {
+ const x = Math.floor(Math.random() * 20);
+ const y = Math.floor(Math.random() * 20);
+ if (densityMatrix[y][x] === 'white') {
+ densityMatrix[y][x] = color;
+ cellsFilled++;
+ }
+ }
+
+ });
+
+ postMessage({ densityMatrix });
+ } catch (error) {
+ postMessage({ error: error.message });
+ }
+};
diff --git a/playground/DashboardToy/DashboardToy.Frontend/wwwroot/index.html b/playground/DashboardToy/DashboardToy.Frontend/wwwroot/index.html
index 121803cbe7..1dc88976af 100644
--- a/playground/DashboardToy/DashboardToy.Frontend/wwwroot/index.html
+++ b/playground/DashboardToy/DashboardToy.Frontend/wwwroot/index.html
@@ -3,7 +3,7 @@
- Orleans Activation Repartitioning
+ Orleans Activation Rebalancing
diff --git a/src/Orleans.Core/Placement/Rebalancing/IActivationRebalancer.cs b/src/Orleans.Core/Placement/Rebalancing/IActivationRebalancer.cs
new file mode 100644
index 0000000000..49b9257676
--- /dev/null
+++ b/src/Orleans.Core/Placement/Rebalancing/IActivationRebalancer.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Orleans.Placement.Rebalancing;
+
+///
+/// A gateway to interface with the activation rebalancer.
+///
+/// This is available only on the silo.
+public interface IActivationRebalancer
+{
+ ///
+ /// Returns the rebalancing report.
+ /// The report can lag behind if you choose a session cycle period less than .
+ ///
+ /// If set to returns the most current report.
+ /// Using incurs an asynchronous operation.
+ ValueTask GetRebalancingReport(bool force = false);
+
+ ///
+ Task ResumeRebalancing();
+
+ ///
+ Task SuspendRebalancing(TimeSpan? duration = null);
+
+ ///
+ /// Subscribe to activation rebalancer reports.
+ ///
+ /// The component that will be notified.
+ void SubscribeToReports(IActivationRebalancerReportListener listener);
+
+ ///
+ /// Unsubscribe from activation rebalancer reports.
+ ///
+ /// The already subscribed component.
+ void UnsubscribeFromReports(IActivationRebalancerReportListener listener);
+}
diff --git a/src/Orleans.Core/Placement/Rebalancing/IActivationRebalancerMonitor.cs b/src/Orleans.Core/Placement/Rebalancing/IActivationRebalancerMonitor.cs
new file mode 100644
index 0000000000..a795c15814
--- /dev/null
+++ b/src/Orleans.Core/Placement/Rebalancing/IActivationRebalancerMonitor.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Threading.Tasks;
+
+#nullable enable
+
+namespace Orleans.Placement.Rebalancing;
+
+[Alias("IActivationRebalancerMonitor")]
+internal interface IActivationRebalancerMonitor : ISystemTarget, IActivationRebalancer
+{
+ ///
+ /// The period on which the must report back to the monitor.
+ ///
+ public static readonly TimeSpan WorkerReportPeriod = TimeSpan.FromSeconds(30);
+
+ ///
+ /// Invoked periodically by the .
+ ///
+ [Alias("Report")] Task Report(RebalancingReport report);
+}
diff --git a/src/Orleans.Core/Placement/Rebalancing/IActivationRebalancerReportListener.cs b/src/Orleans.Core/Placement/Rebalancing/IActivationRebalancerReportListener.cs
new file mode 100644
index 0000000000..13bcbdd419
--- /dev/null
+++ b/src/Orleans.Core/Placement/Rebalancing/IActivationRebalancerReportListener.cs
@@ -0,0 +1,13 @@
+namespace Orleans.Placement.Rebalancing;
+
+///
+/// Interface for types which listen to rebalancer status changes.
+///
+public interface IActivationRebalancerReportListener
+{
+ ///
+ /// Triggered when rebalancer has provided a new .
+ ///
+ /// Latest report from the rebalancer.
+ void OnReport(RebalancingReport report);
+}
\ No newline at end of file
diff --git a/src/Orleans.Core/Placement/Rebalancing/IActivationRebalancerWorker.cs b/src/Orleans.Core/Placement/Rebalancing/IActivationRebalancerWorker.cs
new file mode 100644
index 0000000000..68f54a132d
--- /dev/null
+++ b/src/Orleans.Core/Placement/Rebalancing/IActivationRebalancerWorker.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Threading.Tasks;
+using Orleans.Concurrency;
+
+namespace Orleans.Placement.Rebalancing;
+
+[Alias("IActivationRebalancerWorker")]
+internal interface IActivationRebalancerWorker : IGrainWithIntegerKey
+{
+ ///
+ /// Returns the most recent rebalancing report.
+ ///
+ /// Acts also as a way to wake up the rebalancer, if its deactivated.
+ [AlwaysInterleave, Alias("GetReport")] ValueTask GetReport();
+
+ ///
+ /// Resumes rebalancing if its suspended, otherwise its a no-op.
+ ///
+ [Alias("ResumeRebalancing")] Task ResumeRebalancing();
+
+ ///
+ /// Suspends rebalancing if its running, otherwise its a no-op.
+ ///
+ ///
+ /// The amount of time to suspend the rebalancer.
+ /// means suspend indefinitely.
+ ///
+ [Alias("SuspendRebalancing")] Task SuspendRebalancing(TimeSpan? duration);
+}
\ No newline at end of file
diff --git a/src/Orleans.Core/Placement/Rebalancing/IFailedSessionBackoffProvider.cs b/src/Orleans.Core/Placement/Rebalancing/IFailedSessionBackoffProvider.cs
new file mode 100644
index 0000000000..0dc9e94188
--- /dev/null
+++ b/src/Orleans.Core/Placement/Rebalancing/IFailedSessionBackoffProvider.cs
@@ -0,0 +1,12 @@
+using Orleans.Internal;
+
+namespace Orleans.Placement.Rebalancing;
+
+///
+/// Determines how long to wait between successive rebalancing sessions, if an aprior session has failed.
+///
+///
+/// A session is considered "failed" if n-consecutive number of cycles yielded no significant improvement
+/// to the cluster's entropy.
+///
+public interface IFailedSessionBackoffProvider : IBackoffProvider { }
\ No newline at end of file
diff --git a/src/Orleans.Core/Placement/Rebalancing/RebalancingReport.cs b/src/Orleans.Core/Placement/Rebalancing/RebalancingReport.cs
new file mode 100644
index 0000000000..ff422d34b8
--- /dev/null
+++ b/src/Orleans.Core/Placement/Rebalancing/RebalancingReport.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Immutable;
+using Orleans.Runtime;
+
+namespace Orleans.Placement.Rebalancing;
+
+///
+/// The status of the .
+///
+[GenerateSerializer]
+public enum RebalancerStatus : byte
+{
+ ///
+ /// It is executing.
+ ///
+ Executing = 0,
+ ///
+ /// It is suspended.
+ ///
+ Suspended = 1
+}
+
+///
+/// A report of the current state of the activation rebalancer.
+///
+[GenerateSerializer, Immutable, Alias("RebalancingReport")]
+public readonly struct RebalancingReport
+{
+ ///
+ /// The silo where the rebalancer is currently located.
+ ///
+ [Id(0)] public required SiloAddress Host { get; init; }
+
+ ///
+ /// The current status of the rebalancer.
+ ///
+ [Id(1)] public required RebalancerStatus Status { get; init; }
+
+ ///
+ /// The amount of time the rebalancer is suspended (if at all).
+ ///
+ /// This will always be if is .
+ [Id(2)] public TimeSpan? SuspensionDuration { get; init; }
+
+ ///
+ /// The current view of the cluster's imbalance.
+ ///
+ /// Range: [0-1]
+ [Id(3)] public required double ClusterImbalance { get; init; }
+
+ ///
+ /// Latest rebalancing statistics.
+ ///
+ [Id(4)] public required ImmutableArray Statistics { get; init; }
+}
+
+///
+/// Rebalancing statistics for the given .
+///
+///
+/// Used for diagnostics / metrics purposes. Note that statistics are an approximation.
+[GenerateSerializer, Immutable, Alias("RebalancingStatistics")]
+public readonly struct RebalancingStatistics
+{
+ ///
+ /// The time these statistics were assembled.
+ ///
+ [Id(0)] public required DateTime TimeStamp { get; init; }
+
+ ///
+ /// The silo to which these statistics belong to.
+ ///
+ [Id(1)] public required SiloAddress SiloAddress { get; init; }
+
+ ///
+ /// The approximate number of activations that have been dispersed from this silo thus far.
+ ///
+ [Id(2)] public required ulong DispersedActivations { get; init; }
+
+ ///
+ /// The approximate number of activations that have been acquired by this silo thus far.
+ ///
+ [Id(3)] public required ulong AcquiredActivations { get; init; }
+}
\ No newline at end of file
diff --git a/src/Orleans.Core/Placement/Repartitioning/IActivationRepartitionerSystemTarget.cs b/src/Orleans.Core/Placement/Repartitioning/IActivationRepartitionerSystemTarget.cs
index a5964ae495..23cffbbe5a 100644
--- a/src/Orleans.Core/Placement/Repartitioning/IActivationRepartitionerSystemTarget.cs
+++ b/src/Orleans.Core/Placement/Repartitioning/IActivationRepartitionerSystemTarget.cs
@@ -40,7 +40,7 @@ static IActivationRepartitionerSystemTarget GetReference(IGrainFactory grainFact
ValueTask> GetGrainCallFrequencies();
///
- /// For sue in testing only! Flushes buffered messages.
+ /// For use in testing only! Flushes buffered messages.
///
ValueTask FlushBuffers();
}
diff --git a/src/Orleans.Core/Runtime/Constants.cs b/src/Orleans.Core/Runtime/Constants.cs
index 198908e73a..1d6f2f9153 100644
--- a/src/Orleans.Core/Runtime/Constants.cs
+++ b/src/Orleans.Core/Runtime/Constants.cs
@@ -26,6 +26,7 @@ internal static class Constants
public static readonly GrainType ManifestProviderType = SystemTargetGrainId.CreateGrainType("manifest");
public static readonly GrainType ActivationMigratorType = SystemTargetGrainId.CreateGrainType("migrator");
public static readonly GrainType ActivationRepartitionerType = SystemTargetGrainId.CreateGrainType("repartitioner");
+ public static readonly GrainType ActivationRebalancerMonitorType = SystemTargetGrainId.CreateGrainType("rebalancer-monitor");
public static readonly GrainType GrainDirectoryPartition = SystemTargetGrainId.CreateGrainType("dir.grain.part");
public static readonly GrainType GrainDirectory = SystemTargetGrainId.CreateGrainType("dir.grain");
@@ -55,6 +56,7 @@ internal static class Constants
{ManifestProviderType, "ManifestProvider"},
{ActivationMigratorType, "ActivationMigrator"},
{ActivationRepartitionerType, "ActivationRepartitioner"},
+ {ActivationRebalancerMonitorType, "ActivationRebalancerMonitor"},
{GrainDirectory, "GrainDirectory"},
}.ToFrozenDictionary();
@@ -62,4 +64,4 @@ internal static class Constants
public static bool IsSingletonSystemTarget(GrainType id) => SingletonSystemTargetNames.ContainsKey(id);
}
}
-
+
diff --git a/src/Orleans.Core/SystemTargetInterfaces/ISiloControl.cs b/src/Orleans.Core/SystemTargetInterfaces/ISiloControl.cs
index 007f147b29..d7de973c4b 100644
--- a/src/Orleans.Core/SystemTargetInterfaces/ISiloControl.cs
+++ b/src/Orleans.Core/SystemTargetInterfaces/ISiloControl.cs
@@ -21,6 +21,7 @@ internal interface ISiloControl : ISystemTarget, IVersionManager
Task GetDetailedGrainReport(GrainId grainId);
Task GetActivationCount();
+ Task MigrateRandomActivations(SiloAddress target, int count);
Task