fix(core): harden direct session tracking
Build ErsatzTV Image / Build & test (.NET) (pull_request) Failing after 3s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 3m38s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped

refs #99
This commit is contained in:
2026-07-04 20:05:19 +02:00
parent 9cd107bc8d
commit 6350845101
4 changed files with 58 additions and 5 deletions
@@ -71,4 +71,28 @@ public class DirectStreamSessionTrackerTests
sessions.Single().ChannelNumber.ShouldBe("2");
sessions.Single().StreamingMode.ShouldBe(StreamingMode.HttpLiveStreamingDirect);
}
[Test]
public void Should_Not_Orphan_Session_When_Last_Registration_Is_Removed_During_Register()
{
DirectStreamSessionTracker tracker = null;
IDisposable existingSession = null;
tracker = new TestDirectStreamSessionTracker(() => existingSession?.Dispose());
existingSession = tracker.Register("1", StreamingMode.TransportStream);
using IDisposable newSession = tracker.Register("1", StreamingMode.HttpLiveStreamingDirect);
tracker.IsActive("1").ShouldBeTrue();
tracker.GetViewerCount("1").ShouldBe(1);
IReadOnlyCollection<DirectStreamSession> sessions = tracker.GetActiveSessions("1");
sessions.Count.ShouldBe(1);
sessions.Single().StreamingMode.ShouldBe(StreamingMode.HttpLiveStreamingDirect);
}
private sealed class TestDirectStreamSessionTracker(Action onRegisteringSession) : DirectStreamSessionTracker
{
protected override void OnRegisteringSession() => onRegisteringSession();
}
}
@@ -15,11 +15,17 @@ public class DirectStreamSessionTracker : IDirectStreamSessionTracker
ConcurrentDictionary<Guid, DirectStreamSession> channelSessions =
_sessions.GetOrAdd(channelNumber, _ => new ConcurrentDictionary<Guid, DirectStreamSession>());
OnRegisteringSession();
channelSessions.TryAdd(session.Id, session);
return new Registration(this, session);
}
protected virtual void OnRegisteringSession()
{
}
public bool IsActive(string channelNumber) => GetViewerCount(channelNumber) > 0;
public int GetViewerCount(string channelNumber) =>
@@ -43,10 +49,6 @@ public class DirectStreamSessionTracker : IDirectStreamSessionTracker
}
channelSessions.TryRemove(session.Id, out _);
if (channelSessions.IsEmpty)
{
_sessions.TryRemove(session.ChannelNumber, out _);
}
}
private sealed class Registration(DirectStreamSessionTracker tracker, DirectStreamSession session) : IDisposable
@@ -1,10 +1,12 @@
using ErsatzTV.Controllers;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Streaming;
using ErsatzTV.Core.Streaming;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
@@ -78,9 +80,27 @@ public class TrackedFileStreamResultTests
tracker.GetViewerCount("1").ShouldBe(0);
}
private static ActionContext GetActionContext()
[Test]
public async Task Should_Not_Track_Session_For_Head_Request()
{
IDirectStreamSessionTracker tracker = Substitute.For<IDirectStreamSessionTracker>();
var stream = new BlockingReadStream();
TrackedFileStreamResult result = new(
stream,
"video/mp2t",
tracker,
"1",
StreamingMode.TransportStream);
await result.ExecuteResultAsync(GetActionContext(HttpMethods.Head));
tracker.DidNotReceive().Register(Arg.Any<string>(), Arg.Any<StreamingMode>());
}
private static ActionContext GetActionContext(string method = "GET")
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = method;
httpContext.RequestServices = new ServiceCollection()
.AddLogging()
.AddControllers()
@@ -1,5 +1,6 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Streaming;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace ErsatzTV.Controllers;
@@ -13,6 +14,12 @@ public class TrackedFileStreamResult(
{
public override async Task ExecuteResultAsync(ActionContext context)
{
if (HttpMethods.IsHead(context.HttpContext.Request.Method))
{
await base.ExecuteResultAsync(context);
return;
}
using IDisposable registration = directStreamSessionTracker.Register(channelNumber, streamingMode);
await base.ExecuteResultAsync(context);