Skip to content

Commit

Permalink
Upgrade GPS Tracker to dotnet 9 and add Aspire orchestration
Browse files Browse the repository at this point in the history
  • Loading branch information
egil committed Jan 26, 2025
1 parent 0a82190 commit 8a3efda
Show file tree
Hide file tree
Showing 21 changed files with 321 additions and 157 deletions.
25 changes: 25 additions & 0 deletions orleans/GPSTracker/GPSTracker.AppHost/GPSTracker.AppHost.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<UserSecretsId>afde51c8-abcf-4e8d-97a3-fd9301b96ffd</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.0.0" />
<PackageReference Include="Aspire.Hosting.Azure.Storage" Version="9.0.0" />
<PackageReference Include="Aspire.Hosting.Orleans" Version="9.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\GPSTracker.FakeDeviceGateway\GPSTracker.FakeDeviceGateway.csproj" />
<ProjectReference Include="..\GPSTracker.Service\GPSTracker.Service.csproj" />
</ItemGroup>

</Project>
19 changes: 19 additions & 0 deletions orleans/GPSTracker/GPSTracker.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
var builder = DistributedApplication.CreateBuilder(args);

// https://learn.microsoft.com/en-us/dotnet/aspire/frameworks/orleans?tabs=dotnet-cli
var storage = builder.AddAzureStorage("storage")
.RunAsEmulator();
var clusteringTable = storage.AddTables("clustering");
var orleans = builder.AddOrleans("default")
.WithClustering(clusteringTable);

var service = builder.AddProject<Projects.GPSTracker_Service>("gpstracker-service")
.WithReference(orleans)
.WithReplicas(3);

var deviceGateway = builder.AddProject<Projects.GPSTracker_FakeDeviceGateway>("device-gateway")
.WithReference(orleans.AsClient())
.WithExternalHttpEndpoints()
.WaitFor(service);

builder.Build().Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17244;http://localhost:15133",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21285",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22279"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15133",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19294",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20112"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions orleans/GPSTracker/GPSTracker.AppHost/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
4 changes: 2 additions & 2 deletions orleans/GPSTracker/GPSTracker.Common/GPSTracker.Common.csproj
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Orleans.Sdk" Version="8.0.0" />
<PackageReference Include="Microsoft.Orleans.Sdk" Version="9.0.1" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion orleans/GPSTracker/GPSTracker.Common/LoadDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ private static void UpdateDevicePosition(Model model, double delta)

public static double NextDouble(double min, double max) => Random.NextDouble() * (max - min) + min;

private class Model
private sealed class Model
{
public Stopwatch TimeSinceLastUpdate { get; } = Stopwatch.StartNew();
public int DeviceId { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\GPSTracker.Common\GPSTracker.Common.csproj" />
<ProjectReference Include="..\GPSTracker.ServiceDefaults\GPSTracker.ServiceDefaults.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aspire.Azure.Data.Tables" Version="9.0.0" />
<PackageReference Include="Microsoft.Orleans.Clustering.AzureStorage" Version="9.0.1" />
</ItemGroup>
</Project>
9 changes: 5 additions & 4 deletions orleans/GPSTracker/GPSTracker.FakeDeviceGateway/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using IHost host = Host.CreateDefaultBuilder(args)
.UseOrleansClient((ctx, clientBuilder) => clientBuilder.UseLocalhostClustering())
.UseConsoleLifetime()
.Build();
var builder = Host.CreateApplicationBuilder(args);
builder.AddServiceDefaults();
builder.AddKeyedAzureTableClient("clustering");
builder.UseOrleansClient();

using var host = builder.Build();
await host.StartAsync();

IHostApplicationLifetime lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();
Expand Down
12 changes: 6 additions & 6 deletions orleans/GPSTracker/GPSTracker.Service/GPSTracker.Service.csproj
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\GPSTracker.Common\GPSTracker.Common.csproj" />
<ProjectReference Include="..\GPSTracker.ServiceDefaults\GPSTracker.ServiceDefaults.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Orleans.Server" Version="8.0.0" />
<PackageReference Include="OpenTelemetry.Exporter.Zipkin" Version="1.7.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.7.0" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.4.0-rc.4" />
<PackageReference Include="Aspire.Azure.Data.Tables" Version="9.0.0" />
<PackageReference Include="Microsoft.Orleans.Clustering.AzureStorage" Version="9.0.1" />
<PackageReference Include="Microsoft.Orleans.Server" Version="9.0.1" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace GPSTracker.GrainImplementation;
[Reentrant]
public class DeviceGrain : Grain, IDeviceGrain
{
private DeviceMessage _lastMessage = null!;
private DeviceMessage? _lastMessage;

private readonly IPushNotifierGrain _pushNotifier;

Expand Down
12 changes: 3 additions & 9 deletions orleans/GPSTracker/GPSTracker.Service/Grains/HubListGrain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,12 @@

namespace GPSTracker.GrainImplementation;

public class HubListGrain : Grain, IHubListGrain
public class HubListGrain(IClusterMembershipService clusterMembershipService) : Grain, IHubListGrain
{
private readonly IClusterMembershipService _clusterMembership;
private readonly Dictionary<SiloAddress, IRemoteLocationHub> _hubs = new();
private readonly Dictionary<SiloAddress, IRemoteLocationHub> _hubs = [];
private MembershipVersion _cacheMembershipVersion;
private List<(SiloAddress Host, IRemoteLocationHub Hub)>? _cache;

public HubListGrain(IClusterMembershipService clusterMembershipService)
{
_clusterMembership = clusterMembershipService;
}

public ValueTask AddHub(SiloAddress host, IRemoteLocationHub hubReference)
{
// Invalidate the cache.
Expand All @@ -29,7 +23,7 @@ public ValueTask AddHub(SiloAddress host, IRemoteLocationHub hubReference)
private List<(SiloAddress Host, IRemoteLocationHub Hub)> GetCachedHubs()
{
// Returns a cached list of hubs if the cache is valid, otherwise builds a list of hubs.
ClusterMembershipSnapshot clusterMembers = _clusterMembership.CurrentSnapshot;
ClusterMembershipSnapshot clusterMembers = clusterMembershipService.CurrentSnapshot;
if (_cache is { } && clusterMembers.Version == _cacheMembershipVersion)
{
return _cache;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Orleans.Runtime;
using Orleans.Runtime;

namespace GPSTracker.GrainImplementation;

Expand All @@ -8,5 +8,6 @@ namespace GPSTracker.GrainImplementation;
public interface IHubListGrain : IGrainWithGuidKey
{
ValueTask AddHub(SiloAddress host, IRemoteLocationHub hubReference);

ValueTask<List<(SiloAddress Host, IRemoteLocationHub Hub)>> GetHubs();
}
44 changes: 22 additions & 22 deletions orleans/GPSTracker/GPSTracker.Service/Grains/PushNotifierGrain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,46 @@ namespace GPSTracker.GrainImplementation;

[Reentrant]
[StatelessWorker(maxLocalWorkers: 12)]
public class PushNotifierGrain : Grain, IPushNotifierGrain
public sealed class PushNotifierGrain(ILogger<PushNotifierGrain> logger) : Grain, IPushNotifierGrain, IDisposable
{
private readonly Queue<VelocityMessage> _messageQueue = new();
private readonly ILogger<PushNotifierGrain> _logger;
private List<(SiloAddress Host, IRemoteLocationHub Hub)> _hubs = new();
public PushNotifierGrain(ILogger<PushNotifierGrain> logger) => _logger = logger;
private Task _flushTask = Task.CompletedTask;
private IGrainTimer? _flushTimer;
private IGrainTimer? _refreshTimer;

public override async Task OnActivateAsync(CancellationToken cancellationToken)
{
// Set up a timer to regularly flush the message queue
RegisterTimer(
_ =>
{
Flush();
return Task.CompletedTask;
},
null,
TimeSpan.FromMilliseconds(15),
TimeSpan.FromMilliseconds(15));
_flushTimer = this.RegisterGrainTimer(
ct => Flush(),
dueTime: TimeSpan.FromMilliseconds(15),
period: TimeSpan.FromMilliseconds(15));

// Set up a timer to regularly refresh the hubs, to respond to azure infrastructure changes
await RefreshHubs();
RegisterTimer(
asyncCallback: async _ => await RefreshHubs(),
state: null,

_refreshTimer = this.RegisterGrainTimer(
async _ => await RefreshHubs(),
dueTime: TimeSpan.FromSeconds(60),
period: TimeSpan.FromSeconds(60));

await base.OnActivateAsync(cancellationToken);
}


public override async Task OnDeactivateAsync(DeactivationReason deactivationReason, CancellationToken cancellationToken)
{
await Flush();
await base.OnDeactivateAsync(deactivationReason, cancellationToken);
}

public void Dispose()
{
_flushTimer?.Dispose();
_refreshTimer?.Dispose();
}

private async ValueTask RefreshHubs()
{
// Discover the current infrastructure
Expand Down Expand Up @@ -77,16 +79,14 @@ async Task FlushInternal()
{
// Send all messages to all SignalR hubs
var messagesToSend = new List<VelocityMessage>(Math.Min(_messageQueue.Count, MaxMessagesPerBatch));
while (messagesToSend.Count < MaxMessagesPerBatch && _messageQueue.TryDequeue(out VelocityMessage? msg)) messagesToSend.Add(msg);

var tasks = new List<Task>(_hubs.Count);
var batch = new VelocityBatch(messagesToSend);

foreach ((SiloAddress Host, IRemoteLocationHub Hub) hub in _hubs)
while (messagesToSend.Count < MaxMessagesPerBatch && _messageQueue.TryDequeue(out VelocityMessage? msg))
{
tasks.Add(BroadcastUpdates(hub.Host, hub.Hub, batch, _logger));
messagesToSend.Add(msg);
}

var batch = new VelocityBatch(messagesToSend);
var tasks = _hubs.Select(hub => BroadcastUpdates(hub.Host, hub.Hub, batch, logger));

await Task.WhenAll(tasks);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,10 @@ namespace GPSTracker;
/// <summary>
/// Broadcasts location messages to clients which are connected to the local SignalR hub.
/// </summary>
internal sealed class RemoteLocationHub : IRemoteLocationHub
internal sealed class RemoteLocationHub(IHubContext<LocationHub> hub) : IRemoteLocationHub
{
private readonly IHubContext<LocationHub> _hub;

public RemoteLocationHub(IHubContext<LocationHub> hub) => _hub = hub;

// Send a message to every client which is connected to the hub
public ValueTask BroadcastUpdates(VelocityBatch messages) =>
new(_hub.Clients.All.SendAsync(
new(hub.Clients.All.SendAsync(
"locationUpdates", messages, CancellationToken.None));
}
32 changes: 10 additions & 22 deletions orleans/GPSTracker/GPSTracker.Service/HubListUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,19 @@ namespace GPSTracker;
/// <summary>
/// Periodically updates the <see cref="IHubListGrain"/> implementation with a reference to the local <see cref="RemoteLocationHub"/>.
/// </summary>
[Reentrant]
internal sealed class HubListUpdater : BackgroundService
internal sealed class HubListUpdater(
IGrainFactory grainFactory,
ILogger<HubListUpdater> logger,
ILocalSiloDetails localSiloDetails,
IHubContext<LocationHub> hubContext) : BackgroundService
{
private readonly IGrainFactory _grainFactory;
private readonly ILogger<HubListUpdater> _logger;
private readonly ILocalSiloDetails _localSiloDetails;
private readonly RemoteLocationHub _locationBroadcaster;

public HubListUpdater(
IGrainFactory grainFactory,
ILogger<HubListUpdater> logger,
ILocalSiloDetails localSiloDetails,
IHubContext<LocationHub> hubContext)
{
_grainFactory = grainFactory;
_logger = logger;
_localSiloDetails = localSiloDetails;
_locationBroadcaster = new RemoteLocationHub(hubContext);
}
private readonly RemoteLocationHub _locationBroadcaster = new RemoteLocationHub(hubContext);

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
IHubListGrain hubListGrain = _grainFactory.GetGrain<IHubListGrain>(Guid.Empty);
SiloAddress localSiloAddress = _localSiloDetails.SiloAddress;
IRemoteLocationHub selfReference = _grainFactory.CreateObjectReference<IRemoteLocationHub>(_locationBroadcaster);
IHubListGrain hubListGrain = grainFactory.GetGrain<IHubListGrain>(Guid.Empty);
SiloAddress localSiloAddress = localSiloDetails.SiloAddress;
IRemoteLocationHub selfReference = grainFactory.CreateObjectReference<IRemoteLocationHub>(_locationBroadcaster);

// This runs in a loop because the HubListGrain does not use any form of persistence, so if the
// host which it is activated on stops, then it will lose any internal state.
Expand All @@ -45,7 +33,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
}
catch (Exception exception) when (!stoppingToken.IsCancellationRequested)
{
_logger.LogError(exception, "Error polling location hub list");
logger.LogError(exception, "Error polling location hub list");
}

if (!stoppingToken.IsCancellationRequested)
Expand Down
Loading

0 comments on commit 8a3efda

Please sign in to comment.