fix(devices): read devices and device options through to the database
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful

The device cache was filled once at construction, so a token minted by one
replica was unknown to every other replica already running and a token revoked
on one replica stayed valid on the others until they restarted.

- drop the eager device and device options dictionaries
- read devices and device options from the database on every query
- push the device query filters and ordering into SQL
- open the request's database context only for the api key fallback
- cover both directions against real PostgreSQL with two manager instances
This commit is contained in:
2026-09-13 13:08:47 +10:00
parent 25269263ce
commit 56919b9565
7 changed files with 406 additions and 177 deletions
@@ -256,7 +256,7 @@ namespace Emby.Server.Implementations.Session
ArgumentException.ThrowIfNullOrEmpty(deviceId);
var activityDate = DateTime.UtcNow;
var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
var lastActivityDate = session.LastActivityDate;
session.LastActivityDate = activityDate;
@@ -491,7 +491,7 @@ namespace Emby.Server.Implementations.Session
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
/// <returns>SessionInfo.</returns>
private SessionInfo GetSessionInfo(
private async Task<SessionInfo> GetSessionInfo(
string appName,
string appVersion,
string deviceId,
@@ -504,7 +504,7 @@ namespace Emby.Server.Implementations.Session
ArgumentException.ThrowIfNullOrEmpty(deviceId);
var key = GetSessionKey(appName, deviceId, user?.Id ?? Guid.Empty);
SessionInfo newSession = CreateSessionInfo(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
SessionInfo newSession = await CreateSessionInfo(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
SessionInfo sessionInfo = _activeConnections.GetOrAdd(key, newSession);
if (ReferenceEquals(newSession, sessionInfo))
{
@@ -532,7 +532,7 @@ namespace Emby.Server.Implementations.Session
return sessionInfo;
}
private SessionInfo CreateSessionInfo(
private async Task<SessionInfo> CreateSessionInfo(
string key,
string appName,
string appVersion,
@@ -562,7 +562,7 @@ namespace Emby.Server.Implementations.Session
deviceName = "Network Device";
}
var deviceOptions = _deviceManager.GetDeviceOptions(deviceId) ?? new()
var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false) ?? new()
{
DeviceId = deviceId
};
@@ -1768,12 +1768,12 @@ namespace Emby.Server.Implementations.Session
// This should be validated above, but if it isn't don't delete all tokens.
ArgumentException.ThrowIfNullOrEmpty(deviceId);
var existing = _deviceManager.GetDevices(
var existing = (await _deviceManager.GetDevices(
new DeviceQuery
{
DeviceId = deviceId,
UserId = user.Id
}).Items;
}).ConfigureAwait(false)).Items;
foreach (var auth in existing)
{
@@ -1801,12 +1801,12 @@ namespace Emby.Server.Implementations.Session
ArgumentException.ThrowIfNullOrEmpty(accessToken);
var existing = _deviceManager.GetDevices(
var existing = (await _deviceManager.GetDevices(
new DeviceQuery
{
Limit = 1,
AccessToken = accessToken
}).Items;
}).ConfigureAwait(false)).Items;
if (existing.Count > 0)
{
@@ -1845,10 +1845,10 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
var existing = _deviceManager.GetDevices(new DeviceQuery
var existing = await _deviceManager.GetDevices(new DeviceQuery
{
UserId = userId
});
}).ConfigureAwait(false);
foreach (var info in existing.Items)
{
@@ -2045,11 +2045,11 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
{
var items = _deviceManager.GetDevices(new DeviceQuery
var items = (await _deviceManager.GetDevices(new DeviceQuery
{
AccessToken = token,
Limit = 1
}).Items;
}).ConfigureAwait(false)).Items;
if (items.Count == 0)
{