fix: make transcode leases ownership-checked and cleanup-aware
Cleanup never matched a live session because the controller registered empty manifest and segment paths, renewal was a read-modify-write that could revert a takeover, and the takeover script compared an ISO date to a number, so it errored. - populate the session record's manifest and segment paths from the playlist path - renew the lease via a Lua compare-and-set on the owning pod - store the lease expiry as unix milliseconds so the scripts can compare it - retain the session record past its lease so an orphan can still be taken over - test the Redis store against a real Redis, including the renew-vs-takeover race - drop the live stream record nothing ever read back
This commit is contained in:
@@ -8,7 +8,6 @@ using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Events;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Session;
|
||||
@@ -45,8 +44,7 @@ public class IdlePlaybackTests
|
||||
Mock.Of<IServerApplicationHost>(),
|
||||
Mock.Of<IDeviceManager>(),
|
||||
Mock.Of<IMediaSourceManager>(),
|
||||
Mock.Of<IHostApplicationLifetime>(),
|
||||
new NullTranscodeSessionStore());
|
||||
Mock.Of<IHostApplicationLifetime>());
|
||||
var session = await sessionManager.LogSessionActivity(
|
||||
"Test Client",
|
||||
"1.0.0",
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Events;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Tests.SessionManager;
|
||||
|
||||
public class LiveStreamHaRecordTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CloseLiveStreamIfNeededAsync_Should_DeleteDurableRecord()
|
||||
{
|
||||
var store = new Mock<ITranscodeSessionStore>();
|
||||
var mediaSourceManager = new Mock<IMediaSourceManager>();
|
||||
await using var sessionManager = CreateSessionManager(store.Object, mediaSourceManager.Object);
|
||||
|
||||
await sessionManager.CloseLiveStreamIfNeededAsync("stream-1", "session-1");
|
||||
|
||||
store.Verify(s => s.DeleteLiveStreamAsync("stream-1", "session-1", It.IsAny<CancellationToken>()), Times.Once);
|
||||
mediaSourceManager.Verify(m => m.CloseLiveStream("stream-1"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CloseLiveStreamIfNeededAsync_Should_CloseStream_WhenDurableStoreFails()
|
||||
{
|
||||
var store = new Mock<ITranscodeSessionStore>();
|
||||
store.Setup(s => s.DeleteLiveStreamAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new InvalidOperationException("redis unreachable"));
|
||||
var mediaSourceManager = new Mock<IMediaSourceManager>();
|
||||
await using var sessionManager = CreateSessionManager(store.Object, mediaSourceManager.Object);
|
||||
|
||||
await sessionManager.CloseLiveStreamIfNeededAsync("stream-2", "session-2");
|
||||
|
||||
mediaSourceManager.Verify(m => m.CloseLiveStream("stream-2"), Times.Once);
|
||||
}
|
||||
|
||||
private static Emby.Server.Implementations.Session.SessionManager CreateSessionManager(
|
||||
ITranscodeSessionStore transcodeSessionStore,
|
||||
IMediaSourceManager mediaSourceManager)
|
||||
=> new Emby.Server.Implementations.Session.SessionManager(
|
||||
NullLogger<Emby.Server.Implementations.Session.SessionManager>.Instance,
|
||||
Mock.Of<IEventManager>(),
|
||||
Mock.Of<IUserDataManager>(),
|
||||
Mock.Of<IServerConfigurationManager>(),
|
||||
Mock.Of<ILibraryManager>(),
|
||||
Mock.Of<IUserManager>(),
|
||||
Mock.Of<IMusicManager>(),
|
||||
Mock.Of<IDtoService>(),
|
||||
Mock.Of<IImageProcessor>(),
|
||||
Mock.Of<IServerApplicationHost>(),
|
||||
Mock.Of<IDeviceManager>(),
|
||||
mediaSourceManager,
|
||||
Mock.Of<IHostApplicationLifetime>(),
|
||||
transcodeSessionStore);
|
||||
}
|
||||
@@ -11,7 +11,6 @@ using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Events;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Session;
|
||||
@@ -42,8 +41,7 @@ public class SessionManagerTests
|
||||
Mock.Of<IServerApplicationHost>(),
|
||||
Mock.Of<IDeviceManager>(),
|
||||
Mock.Of<IMediaSourceManager>(),
|
||||
Mock.Of<IHostApplicationLifetime>(),
|
||||
new NullTranscodeSessionStore());
|
||||
Mock.Of<IHostApplicationLifetime>());
|
||||
|
||||
await Assert.ThrowsAsync(exceptionType, () => sessionManager.GetAuthorizationToken(
|
||||
new User("test", "default", "default"),
|
||||
@@ -70,8 +68,7 @@ public class SessionManagerTests
|
||||
Mock.Of<IServerApplicationHost>(),
|
||||
Mock.Of<IDeviceManager>(),
|
||||
Mock.Of<IMediaSourceManager>(),
|
||||
Mock.Of<IHostApplicationLifetime>(),
|
||||
new NullTranscodeSessionStore());
|
||||
Mock.Of<IHostApplicationLifetime>());
|
||||
|
||||
await Assert.ThrowsAsync(exceptionType, () => sessionManager.AuthenticateNewSessionInternal(authenticationRequest, false));
|
||||
}
|
||||
@@ -241,8 +238,7 @@ public class SessionManagerTests
|
||||
Mock.Of<IServerApplicationHost>(),
|
||||
Mock.Of<IDeviceManager>(),
|
||||
Mock.Of<IMediaSourceManager>(),
|
||||
Mock.Of<IHostApplicationLifetime>(),
|
||||
new NullTranscodeSessionStore());
|
||||
Mock.Of<IHostApplicationLifetime>());
|
||||
}
|
||||
|
||||
// All sessions are logged with the same client and device id on purpose, those values are taken
|
||||
|
||||
Reference in New Issue
Block a user