diff --git a/Management.Web/AkkaService.cs b/Management.Web/AkkaService.cs new file mode 100644 index 0000000..1441df7 --- /dev/null +++ b/Management.Web/AkkaService.cs @@ -0,0 +1,62 @@ + +using Akka.Actor; +using Akka.DependencyInjection; +namespace Management.Actors; + + +public class AkkaService : IHostedService, IActorBridge +{ + private ActorSystem? _actorSystem; + private readonly IConfiguration _configuration; + private readonly IServiceProvider _serviceProvider; + private IActorRef? _canvasApiActor; + private readonly IHostApplicationLifetime _applicationLifetime; + + public AkkaService(IServiceProvider serviceProvider, IHostApplicationLifetime appLifetime, IConfiguration configuration) + { + _serviceProvider = serviceProvider; + _applicationLifetime = appLifetime; + _configuration = configuration; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + var bootstrap = BootstrapSetup.Create(); + var dependencyInjectionSetup = DependencyResolverSetup.Create(_serviceProvider); + + var mergedSystemSetup = bootstrap.And(dependencyInjectionSetup); + + _actorSystem = ActorSystem.Create("canvas-managment-actors", mergedSystemSetup); + + // start top level supervisor actor + + // working here https://getakka.net/articles/actors/dependency-injection.html#integrating-with-microsoftextensionsdependencyinjection + var apiActorProps = DependencyResolver.For(_actorSystem).Props(); + _canvasApiActor = _actorSystem.ActorOf(apiActorProps, "canvas-api"); + + // crash if the actor system crashes, awaiting never returns... +#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods + _actorSystem.WhenTerminated.ContinueWith(tr => + { + _applicationLifetime.StopApplication(); + }); +#pragma warning restore CA2016 // Forward the 'CancellationToken' parameter to methods + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + + // add more methods to interact with actor processes, make them part of an interface + // these are the most generic forwarding of messages + public void Tell(object message) + { + _canvasApiActor.Tell(message); + } + + public Task Ask(object message) + { + return _canvasApiActor.Ask(message); + } +} diff --git a/Management.Web/IActorBridge.cs b/Management.Web/IActorBridge.cs new file mode 100644 index 0000000..d3ec6c6 --- /dev/null +++ b/Management.Web/IActorBridge.cs @@ -0,0 +1,7 @@ +namespace Management.Actors; + +public interface IActorBridge +{ + void Tell(object message); + Task Ask(object message); +} diff --git a/Management.Web/Management.Web.csproj b/Management.Web/Management.Web.csproj index ae2f6c5..35e39ea 100644 --- a/Management.Web/Management.Web.csproj +++ b/Management.Web/Management.Web.csproj @@ -5,9 +5,12 @@ + + + diff --git a/Management.Web/Pages/CanvasRequestsQueue.razor b/Management.Web/Pages/CanvasRequestsQueue.razor new file mode 100644 index 0000000..cef1137 --- /dev/null +++ b/Management.Web/Pages/CanvasRequestsQueue.razor @@ -0,0 +1,64 @@ +@page "/test" +@rendermode InteractiveServer + +@using Microsoft.AspNetCore.SignalR.Client + +@inject CanvasService canvas +@inject CoursePlanner planner +@inject FileStorageManager fileStorageManager +@inject IActorBridge bridge +@inject NavigationManager Navigation + +@code { + private HubConnection? hubConnection; + public string? CourseName = "1400"; + @* private bool loading = true; *@ + + protected override async Task OnInitializedAsync() + { + if (planner.LocalCourse == null) + { + System.Diagnostics.Activity.Current = null; + using var activity = DiagnosticsConfig.Source?.StartActivity("Loading Course"); + activity?.AddTag("CourseName", CourseName); + var courses = await fileStorageManager.LoadSavedCourses(); + planner.LocalCourse = courses.First(c => c.Settings.Name == CourseName); + } + + + Console.WriteLine(Navigation.BaseUri + "SignalRHub"); + hubConnection = new HubConnectionBuilder() + .WithUrl(Navigation.BaseUri + "SignalRHub") + .WithAutomaticReconnect() + .Build(); + + + hubConnection.On("SentFromActor", () => + { + Console.WriteLine("recieved from actor"); + }); + + await hubConnection.StartAsync(); + + base.OnInitialized(); + @* loading = false; *@ + } + + private async Task SendAkkaMessage() + { + System.Diagnostics.Activity.Current = null; + using var activity = DiagnosticsConfig.Source?.StartActivity("sending akka message from blazor"); + + if (planner.LocalCourse != null && planner.LocalCourse.Settings.CanvasId != null && hubConnection?.ConnectionId != null) + { + ulong id = (ulong)planner.LocalCourse.Settings.CanvasId; + + var message = new GetModulesMessage(0, id, (string)hubConnection.ConnectionId, activity?.TraceId, ParentSpan: + activity?.SpanId); + var response = await bridge.Ask(message); + Console.WriteLine(response); + } + } +} + + diff --git a/Management.Web/Pages/Course/Course.razor b/Management.Web/Pages/Course/Course.razor index 1e55414..e383966 100644 --- a/Management.Web/Pages/Course/Course.razor +++ b/Management.Web/Pages/Course/Course.razor @@ -6,12 +6,14 @@ @using Management.Web.Pages.Course.Module.ModuleItems @using Management.Web.Shared.Components + @inject FileStorageManager fileStorageManager @inject CanvasService canvas @inject CoursePlanner planner @inject NavigationManager navigtion @inject IConfiguration config + @code { [Parameter] public string? CourseName { get; set; } @@ -22,6 +24,9 @@ { if (planner.LocalCourse == null) { + System.Diagnostics.Activity.Current = null; + using var activity = DiagnosticsConfig.Source?.StartActivity("Loading Course"); + activity?.AddTag("CourseName", CourseName); var courses = await fileStorageManager.LoadSavedCourses(); planner.LocalCourse = courses.First(c => c.Settings.Name == CourseName); } @@ -38,37 +43,39 @@ } @CourseName + +
-@if (loading) -{ - -} + @if (loading) + { + + } -@if (planner.LocalCourse != null) -{ -
-
- - - - View In Canvas - -
- @planner.LocalCourse.Settings.Name + @if (planner.LocalCourse != null) + { +
+
+ + + + View In Canvas + +
+ @planner.LocalCourse.Settings.Name +
+
+ + @if (planner.LoadingCanvasData) + { + + }
-
- - @if (planner.LoadingCanvasData) - { - - } -
- -} + + }
diff --git a/Management.Web/Pages/Index.razor b/Management.Web/Pages/Index.razor index e6b11a3..03697ed 100644 --- a/Management.Web/Pages/Index.razor +++ b/Management.Web/Pages/Index.razor @@ -34,7 +34,9 @@ StateHasChanged(); } private int refreshKey; + } + Index
diff --git a/Management.Web/Program.cs b/Management.Web/Program.cs index 0b2bfe2..f4795ec 100644 --- a/Management.Web/Program.cs +++ b/Management.Web/Program.cs @@ -11,8 +11,10 @@ global using Management.Services.Canvas; global using Management.Web.Shared; global using Management.Web.Shared.Components; using dotenv.net; +using Management.Actors; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.ResponseCompression; using OpenTelemetry; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; @@ -91,6 +93,18 @@ builder.Services.AddScoped(); builder.Services.AddSingleton(); + +// exposing actor service to controllers +builder.Services.AddSingleton(); + +// starting actor service while enabling it to use dependency injection +builder.Services.AddHostedService(sp => (AkkaService)sp.GetRequiredService()); + +builder.Services.AddResponseCompression(opts => +{ + opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "application/octet-stream" }); +}); + builder.Services.AddSignalR(e => { e.MaximumReceiveMessageSize = 102400000; @@ -113,8 +127,10 @@ app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); +app.UseResponseCompression(); app.MapBlazorHub(); +app.MapHub("/SignalRHub"); app.MapFallbackToPage("/_Host"); diff --git a/Management.Web/_Imports.razor b/Management.Web/_Imports.razor index 8ee8f0c..9627494 100644 --- a/Management.Web/_Imports.razor +++ b/Management.Web/_Imports.razor @@ -8,3 +8,5 @@ @using Microsoft.JSInterop @using Management.Web @using Management.Web.Shared +@using Management.Actors +@using static Microsoft.AspNetCore.Components.Web.RenderMode diff --git a/Management/Actors/CanvasApiActor.cs b/Management/Actors/CanvasApiActor.cs new file mode 100644 index 0000000..5d16213 --- /dev/null +++ b/Management/Actors/CanvasApiActor.cs @@ -0,0 +1,50 @@ +using System.Diagnostics; +using System.Net.Http.Headers; +using Akka.Actor; +using Akka.DependencyInjection; +using Management.Services.Canvas; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.DependencyInjection; +namespace Management.Actors; + + +// RecieveActor configures messages in constructor +// UntypedActor configures messages in an onrecieved function +public class CanvasApiActor : ReceiveActor +{ + private readonly IServiceScope _scope; + private readonly ILogger _logger; + private readonly IHubContext _hub; + public CanvasApiActor(IServiceProvider serviceProvider) // props go here + { + _scope = serviceProvider.CreateScope(); + _logger = _scope.ServiceProvider.GetRequiredService>(); + _hub = _scope.ServiceProvider.GetRequiredService>(); + + _logger.LogInformation("creating canvas actor"); + + var canvasService = _scope.ServiceProvider.GetRequiredService(); + ReceiveAsync(async m => + { + using var activity = m.Activity("canvas actor getting modules from canvas api"); + + var modules = await canvasService.Modules.GetModules(m.CanvasCourseId); + Sender.Tell(new CanvasModulesMessage(m.RequestId, m.CanvasCourseId, modules, activity?.TraceId, activity?.SpanId)); + + await _hub.Clients.Client(m.ClientConnectionId).SendAsync("SentFromActor"); + }); + } + + + protected override void PostStop() + { + _scope.Dispose(); + base.PostStop(); + } + + // used to wrap the arguments in a comprehension for future instanciation of the actor + // does this work with DI? + // public static Props Props(CanvasService canvasService) => + // Akka.Actor.Props.Create(() => new CanvasApiActor(canvasService)); + +} diff --git a/Management/Actors/CanvasSupervisor.cs b/Management/Actors/CanvasSupervisor.cs new file mode 100644 index 0000000..f15ac7e --- /dev/null +++ b/Management/Actors/CanvasSupervisor.cs @@ -0,0 +1,15 @@ +using Akka.Actor; +using Akka.DependencyInjection; + +namespace Management.Actors; + +public class CanvasSupervisor : ReceiveActor +{ + // private IActorRef canvasApiActor; + + // public CanvasSupervisor() + // { + // // DependencyResolver + + // } +} diff --git a/Management/Actors/Messages/CanvasModulesMesasge.cs b/Management/Actors/Messages/CanvasModulesMesasge.cs new file mode 100644 index 0000000..2de9de3 --- /dev/null +++ b/Management/Actors/Messages/CanvasModulesMesasge.cs @@ -0,0 +1,12 @@ +using System.Diagnostics; +using CanvasModel.Modules; + +namespace Management.Actors; + +public sealed record CanvasModulesMessage( + long RequestId, + ulong CanvasCourseId, + IEnumerable CanvasModules, + ActivityTraceId? ParentTrace, + ActivitySpanId? ParentSpan +) : ITraceableMessage; diff --git a/Management/Actors/Messages/GetModulesMessage.cs b/Management/Actors/Messages/GetModulesMessage.cs new file mode 100644 index 0000000..05987ac --- /dev/null +++ b/Management/Actors/Messages/GetModulesMessage.cs @@ -0,0 +1,11 @@ +using System.Diagnostics; + +namespace Management.Actors; + +public sealed record GetModulesMessage( + long RequestId, + ulong CanvasCourseId, + string ClientConnectionId, + ActivityTraceId? ParentTrace, + ActivitySpanId? ParentSpan +) : ITraceableMessage; diff --git a/Management/Actors/Messages/ITraceableMessage.cs b/Management/Actors/Messages/ITraceableMessage.cs new file mode 100644 index 0000000..a9456f4 --- /dev/null +++ b/Management/Actors/Messages/ITraceableMessage.cs @@ -0,0 +1,8 @@ +using System.Diagnostics; + +public interface ITraceableMessage +{ + public ActivitySpanId? ParentSpan {get;} + public ActivityTraceId? ParentTrace {get;} +} + diff --git a/Management/DiagnosticsConfig.cs b/Management/DiagnosticsConfig.cs index d6678bf..6fc5726 100644 --- a/Management/DiagnosticsConfig.cs +++ b/Management/DiagnosticsConfig.cs @@ -1,7 +1,23 @@ using System.Diagnostics; +using System.Security.Policy; public static class DiagnosticsConfig { public const string SourceName = "canvas-management-source"; - public static ActivitySource Source = new ActivitySource(SourceName); + public readonly static ActivitySource Source = new(SourceName); + + public static Activity? Activity(this ITraceableMessage message, string activityName) + { + if (message.ParentTrace != null && message.ParentSpan != null) + { + ActivityContext parentContext = new ActivityContext( + (ActivityTraceId)message.ParentTrace, + (ActivitySpanId)message.ParentSpan, + ActivityTraceFlags.Recorded + ); + + return Source?.StartActivity(activityName, ActivityKind.Internal, parentContext); + } + return Source?.StartActivity(activityName); + } } diff --git a/Management/Features/Configuration/CoursePlanner.cs b/Management/Features/Configuration/CoursePlanner.cs index efb7b6c..2e042e8 100644 --- a/Management/Features/Configuration/CoursePlanner.cs +++ b/Management/Features/Configuration/CoursePlanner.cs @@ -44,7 +44,6 @@ public class CoursePlanner get => _localCourse; set { - using var activity = DiagnosticsConfig.Source?.StartActivity("Loading Course"); if (value == null) { _localCourse = null; diff --git a/Management/Management.csproj b/Management/Management.csproj index 82b1c84..6aaefdb 100644 --- a/Management/Management.csproj +++ b/Management/Management.csproj @@ -7,7 +7,11 @@ + + + + diff --git a/Management/SignalRHub.cs b/Management/SignalRHub.cs new file mode 100644 index 0000000..5379340 --- /dev/null +++ b/Management/SignalRHub.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.SignalR; + +public class SignalRHub : Hub +{ + public async Task SendMessage(string user, string message) + { + await Clients.All.SendAsync("ReceiveMessage", user, message); + } + public override Task OnConnectedAsync() + { + var connectionId = Context.ConnectionId; + // Store the connection ID for later use, e.g., in a database or in-memory store + return base.OnConnectedAsync(); + } +}