Update Dockerfile to use .NET 10.0 stable and Alpine 3.23, refactor SSE implementation

This commit is contained in:
2026-01-01 22:32:40 +03:30
parent 2fc57f5233
commit 1d0b42654a
3 changed files with 34 additions and 45 deletions
+1
View File
@@ -1,2 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADebugger_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ff9d2f95d72fa884d8b6ddefc717c56da3657fbb2d5fb683656c3589eb6587_003FDebugger_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpResponseStream_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003Fhome_003Fmahdium_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F912bd5c687f4cf55e0daddfb3f8eecd859debac3856d3a98f1a2ad5208413bd_003FHttpResponseStream_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpResponseStream_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003Fhome_003Fmahdium_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F912bd5c687f4cf55e0daddfb3f8eecd859debac3856d3a98f1a2ad5208413bd_003FHttpResponseStream_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
+2 -2
View File
@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0.100-preview.7-alpine3.22-aot AS build FROM mcr.microsoft.com/dotnet/dotnet/sdk:10.0-alpine3.23-aot AS build
# Install NativeAOT build prerequisites # Install NativeAOT build prerequisites
RUN apk update \ RUN apk update \
@@ -10,7 +10,7 @@ WORKDIR /source
COPY . . COPY . .
RUN dotnet publish -r linux-musl-x64 -o /app 'HoolIt.csproj' RUN dotnet publish -r linux-musl-x64 -o /app 'HoolIt.csproj'
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0.0-preview.7-alpine3.22 FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine3.23
WORKDIR /app WORKDIR /app
COPY --from=build /app . COPY --from=build /app .
ENTRYPOINT ["/app/HoolIt"] ENTRYPOINT ["/app/HoolIt"]
+31 -43
View File
@@ -1,7 +1,9 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Channels;
using HoolIt.Models; using HoolIt.Models;
var builder = WebApplication.CreateSlimBuilder(args); var builder = WebApplication.CreateSlimBuilder(args);
@@ -15,7 +17,7 @@ var app = builder.Build();
app.Urls.Clear(); app.Urls.Clear();
app.Urls.Add("http://0.0.0.0:5030"); app.Urls.Add("http://0.0.0.0:5030");
var subscribers = new ConcurrentDictionary<string, List<StreamWriter>>(); var subscribers = new ConcurrentDictionary<string, List<ChannelWriter<string>>>();
var cancellationSources = var cancellationSources =
new ConcurrentDictionary<string, CancellationTokenSource>(); // To manage cancellation per feedId new ConcurrentDictionary<string, CancellationTokenSource>(); // To manage cancellation per feedId
@@ -36,18 +38,15 @@ createApi.MapGet("/{feedId}", async (HttpContext context, string feedId) =>
try try
{ {
var chunkedQueryData = JsonSerializer.Serialize(dweet, AppJsonSerializerContext.Default.Dweet); var jsonQueryData = JsonSerializer.Serialize(dweet, AppJsonSerializerContext.Default.Dweet);
if (subscribers.TryGetValue(feedId, out var subscribersList)) if (subscribers.TryGetValue(feedId, out var subscribersList))
foreach (var writer in subscribersList) foreach (var writer in subscribersList)
{ await writer.WriteAsync(jsonQueryData);
await writer.WriteLineAsync(chunkedQueryData);
await writer.FlushAsync();
}
} }
catch (Exception e) catch (Exception e)
{ {
var faultResponse = new AddDweetFailedResponse() var faultResponse = new AddDweetFailedResponse
{ {
This = "failed", This = "failed",
With = "WeMessedUp", With = "WeMessedUp",
@@ -59,7 +58,6 @@ createApi.MapGet("/{feedId}", async (HttpContext context, string feedId) =>
context.Response.ContentType = "application/json"; context.Response.ContentType = "application/json";
await context.Response.WriteAsync(addFailedResponse); await context.Response.WriteAsync(addFailedResponse);
await context.Response.CompleteAsync(); await context.Response.CompleteAsync();
} }
var addSuccessResponse = new AddDweetSucceededResponse var addSuccessResponse = new AddDweetSucceededResponse
@@ -77,54 +75,44 @@ getLiveDataApi.MapGet("/{feedId}",
async (HttpContext context, string feedId, IHostApplicationLifetime appLifetime, async (HttpContext context, string feedId, IHostApplicationLifetime appLifetime,
CancellationToken reqCancellationToken) => CancellationToken reqCancellationToken) =>
{ {
context.Response.Headers.ContentType = "text/event-stream"; context.Response.StatusCode = 200;
context.Response.Headers.ContentType = "text/plain";
context.Response.Headers.CacheControl = "no-cache";
context.Response.Headers["X-Content-Type-Options"] = "nosniff";
var writer = new StreamWriter(context.Response.Body, Encoding.UTF8); // Disable response buffering
subscribers.GetOrAdd(feedId, _ => new List<StreamWriter>()).Add(writer); context.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature>()?
.DisableBuffering();
// How this cancellation token mess works: var channel = Channel.CreateUnbounded<string>();
// - reqCancellationToken is the cancellation token from the client request, it is used when a client closes the request. var writer = channel.Writer;
// - feedCts is the cancellation token from the feedId, it is used when the app is shutting down.
// - linkedCts is a linked token source that combines both reqCancellationToken and feedCts and gets canceled when either of them does.
// When the app is shutting down, the feedCts token source is canceled which means everything gets canceled altogether (Even that date you have been planning for the past few months; c'mon, you are probably a computer science student with no friends who barely touches grass).
var feedCts = cancellationSources.GetOrAdd(feedId, _ => new CancellationTokenSource()); subscribers.GetOrAdd(feedId, _ => new List<ChannelWriter<string>>()).Add(writer);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(reqCancellationToken, feedCts.Token); Console.WriteLine($"Added subscriber to feed {feedId}");
var linkedToken = linkedCts.Token;
appLifetime.ApplicationStopping.Register(() =>
{
if (cancellationSources.TryGetValue(feedId, out var existingFeedCts) &&
!existingFeedCts.IsCancellationRequested)
{
// It cancels
existingFeedCts.Cancel();
Console.WriteLine($"Cancellation signaled for feedId: {feedId} due to app shutdown.");
}
});
try try
{ {
while (!linkedToken.IsCancellationRequested) await Task.Delay(Timeout.Infinite, linkedToken); var reader = channel.Reader;
while (!reqCancellationToken.IsCancellationRequested &&
await reader.WaitToReadAsync(reqCancellationToken))
while (reader.TryRead(out var msg))
{
await context.Response.WriteAsync(msg + "\n", reqCancellationToken);
await context.Response.Body.FlushAsync(reqCancellationToken);
}
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
Console.WriteLine($"SSE connection for feedId: {feedId} canceled."); Console.WriteLine($"Cancellation requested for feed {feedId}");
} }
finally finally
{ {
if (subscribers.TryGetValue(feedId, out var subscribersList)) if (subscribers.TryGetValue(feedId, out var list))
{ {
subscribersList.Remove(writer); list.Remove(writer);
if (subscribersList.Count == 0) Console.WriteLine($"Removed subscriber from feed {feedId}");
{ if (list.Count == 0) subscribers.TryRemove(feedId, out _);
subscribers.TryRemove(feedId, out _);
cancellationSources.TryRemove(feedId, out _);
Console.WriteLine($"No more subscribers for feedId: {feedId}. CTS removed.");
}
await writer.DisposeAsync();
Console.WriteLine("Removed subscriber from feed " + feedId);
} }
} }
}); });