Bladeren bron

Implement Device Cache to replace EFCoreSecondLevelCacheInterceptor

The EFCoreSecondLevelCacheInterceptor will place a huge lock even for reading. Implement a ConcurrentDictionary cache to replace it.

Signed-off-by: gnattu <gnattuoc@me.com>
gnattu 1 jaar geleden
bovenliggende
commit
5a62c7a146

+ 15 - 15
Emby.Server.Implementations/Session/SessionManager.cs

@@ -237,7 +237,7 @@ namespace Emby.Server.Implementations.Session
             ArgumentException.ThrowIfNullOrEmpty(deviceId);
 
             var activityDate = DateTime.UtcNow;
-            var session = await GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
+            var session = GetSessionInfo(appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
             var lastActivityDate = session.LastActivityDate;
             session.LastActivityDate = activityDate;
 
@@ -435,7 +435,7 @@ namespace Emby.Server.Implementations.Session
         /// <param name="remoteEndPoint">The remote end point.</param>
         /// <param name="user">The user.</param>
         /// <returns>SessionInfo.</returns>
-        private async Task<SessionInfo> GetSessionInfo(
+        private SessionInfo GetSessionInfo(
             string appName,
             string appVersion,
             string deviceId,
@@ -453,7 +453,7 @@ namespace Emby.Server.Implementations.Session
 
             if (!_activeConnections.TryGetValue(key, out var sessionInfo))
             {
-                sessionInfo = await CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user).ConfigureAwait(false);
+                sessionInfo = CreateSession(key, appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
                 _activeConnections[key] = sessionInfo;
             }
 
@@ -478,7 +478,7 @@ namespace Emby.Server.Implementations.Session
             return sessionInfo;
         }
 
-        private async Task<SessionInfo> CreateSession(
+        private SessionInfo CreateSession(
             string key,
             string appName,
             string appVersion,
@@ -508,7 +508,7 @@ namespace Emby.Server.Implementations.Session
                 deviceName = "Network Device";
             }
 
-            var deviceOptions = await _deviceManager.GetDeviceOptions(deviceId).ConfigureAwait(false);
+            var deviceOptions = _deviceManager.GetDeviceOptions(deviceId);
             if (string.IsNullOrEmpty(deviceOptions.CustomName))
             {
                 sessionInfo.DeviceName = deviceName;
@@ -1297,7 +1297,7 @@ namespace Emby.Server.Implementations.Session
             return new[] { item };
         }
 
-        private IEnumerable<BaseItem> TranslateItemForInstantMix(Guid id, User user)
+        private List<BaseItem> TranslateItemForInstantMix(Guid id, User user)
         {
             var item = _libraryManager.GetItemById(id);
 
@@ -1307,7 +1307,7 @@ namespace Emby.Server.Implementations.Session
                 return new List<BaseItem>();
             }
 
-            return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false });
+            return _musicManager.GetInstantMixFromItem(item, user, new DtoOptions(false) { EnableImages = false }).ToList();
         }
 
         /// <inheritdoc />
@@ -1520,12 +1520,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 = (await _deviceManager.GetDevices(
+            var existing = _deviceManager.GetDevices(
                 new DeviceQuery
                 {
                     DeviceId = deviceId,
                     UserId = user.Id
-                }).ConfigureAwait(false)).Items;
+                }).Items;
 
             foreach (var auth in existing)
             {
@@ -1553,12 +1553,12 @@ namespace Emby.Server.Implementations.Session
 
             ArgumentException.ThrowIfNullOrEmpty(accessToken);
 
-            var existing = (await _deviceManager.GetDevices(
+            var existing = _deviceManager.GetDevices(
                 new DeviceQuery
                 {
                     Limit = 1,
                     AccessToken = accessToken
-                }).ConfigureAwait(false)).Items;
+                }).Items;
 
             if (existing.Count > 0)
             {
@@ -1597,10 +1597,10 @@ namespace Emby.Server.Implementations.Session
         {
             CheckDisposed();
 
-            var existing = await _deviceManager.GetDevices(new DeviceQuery
+            var existing = _deviceManager.GetDevices(new DeviceQuery
             {
                 UserId = userId
-            }).ConfigureAwait(false);
+            });
 
             foreach (var info in existing.Items)
             {
@@ -1787,11 +1787,11 @@ namespace Emby.Server.Implementations.Session
         /// <inheritdoc />
         public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
         {
-            var items = (await _deviceManager.GetDevices(new DeviceQuery
+            var items = _deviceManager.GetDevices(new DeviceQuery
             {
                 AccessToken = token,
                 Limit = 1
-            }).ConfigureAwait(false)).Items;
+            }).Items;
 
             if (items.Count == 0)
             {

+ 8 - 8
Jellyfin.Api/Controllers/DevicesController.cs

@@ -47,10 +47,10 @@ public class DevicesController : BaseJellyfinApiController
     /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
     [HttpGet]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] Guid? userId)
+    public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] Guid? userId)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        return await _deviceManager.GetDevicesForUser(userId).ConfigureAwait(false);
+        return _deviceManager.GetDevicesForUser(userId);
     }
 
     /// <summary>
@@ -63,9 +63,9 @@ public class DevicesController : BaseJellyfinApiController
     [HttpGet("Info")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
-    public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id)
+    public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
     {
-        var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false);
+        var deviceInfo = _deviceManager.GetDevice(id);
         if (deviceInfo is null)
         {
             return NotFound();
@@ -84,9 +84,9 @@ public class DevicesController : BaseJellyfinApiController
     [HttpGet("Options")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status404NotFound)]
-    public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id)
+    public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
     {
-        var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false);
+        var deviceInfo = _deviceManager.GetDeviceOptions(id);
         if (deviceInfo is null)
         {
             return NotFound();
@@ -124,13 +124,13 @@ public class DevicesController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status404NotFound)]
     public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
     {
-        var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false);
+        var existingDevice = _deviceManager.GetDevice(id);
         if (existingDevice is null)
         {
             return NotFound();
         }
 
-        var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
+        var sessions = _deviceManager.GetDevices(new DeviceQuery { DeviceId = id });
 
         foreach (var session in sessions.Items)
         {

+ 85 - 68
Jellyfin.Server.Implementations/Devices/DeviceManager.cs

@@ -27,6 +27,8 @@ namespace Jellyfin.Server.Implementations.Devices
         private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
         private readonly IUserManager _userManager;
         private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
+        private readonly IDictionary<int, Device> _devices;
+        private readonly IDictionary<string, DeviceOptions> _deviceOptions;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="DeviceManager"/> class.
@@ -37,6 +39,24 @@ namespace Jellyfin.Server.Implementations.Devices
         {
             _dbProvider = dbProvider;
             _userManager = userManager;
+            _devices = new ConcurrentDictionary<int, Device>();
+            _deviceOptions = new ConcurrentDictionary<string, DeviceOptions>();
+
+            using var dbContext = _dbProvider.CreateDbContext();
+            foreach (var device in dbContext.Devices
+                         .Include(d => d.User)
+                         .OrderBy(d => d.Id)
+                         .AsEnumerable())
+            {
+                _devices.Add(device.Id, device);
+            }
+
+            foreach (var deviceOption in dbContext.DeviceOptions
+                         .OrderBy(d => d.Id)
+                         .AsEnumerable())
+            {
+                _deviceOptions.Add(deviceOption.DeviceId, deviceOption);
+            }
         }
 
         /// <inheritdoc />
@@ -66,6 +86,8 @@ namespace Jellyfin.Server.Implementations.Devices
                 await dbContext.SaveChangesAsync().ConfigureAwait(false);
             }
 
+            _deviceOptions[deviceId] = deviceOptions;
+
             DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
         }
 
@@ -80,21 +102,15 @@ namespace Jellyfin.Server.Implementations.Devices
                 await dbContext.SaveChangesAsync().ConfigureAwait(false);
             }
 
+            _devices.Add(device.Id, device);
+
             return device;
         }
 
         /// <inheritdoc />
-        public async Task<DeviceOptions> GetDeviceOptions(string deviceId)
+        public DeviceOptions GetDeviceOptions(string deviceId)
         {
-            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
-            DeviceOptions? deviceOptions;
-            await using (dbContext.ConfigureAwait(false))
-            {
-                deviceOptions = await dbContext.DeviceOptions
-                    .AsNoTracking()
-                    .FirstOrDefaultAsync(d => d.DeviceId == deviceId)
-                    .ConfigureAwait(false);
-            }
+            _deviceOptions.TryGetValue(deviceId, out var deviceOptions);
 
             return deviceOptions ?? new DeviceOptions(deviceId);
         }
@@ -108,57 +124,45 @@ namespace Jellyfin.Server.Implementations.Devices
         }
 
         /// <inheritdoc />
-        public async Task<DeviceInfo?> GetDevice(string id)
+        public DeviceInfo? GetDevice(string id)
         {
-            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
-            await using (dbContext.ConfigureAwait(false))
-            {
-                var device = await dbContext.Devices
-                    .Where(d => d.DeviceId == id)
-                    .OrderByDescending(d => d.DateLastActivity)
-                    .Include(d => d.User)
-                    .SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
-                    .FirstOrDefaultAsync()
-                    .ConfigureAwait(false);
+            var device = _devices.Values.OrderByDescending(d => d.DateLastActivity).FirstOrDefault(d => d.DeviceId == id);
+            _deviceOptions.TryGetValue(id, out var deviceOption);
 
-                var deviceInfo = device is null ? null : ToDeviceInfo(device.Device, device.Options);
-
-                return deviceInfo;
-            }
+            var deviceInfo = device is null ? null : ToDeviceInfo(device, deviceOption);
+            return deviceInfo;
         }
 
         /// <inheritdoc />
-        public async Task<QueryResult<Device>> GetDevices(DeviceQuery query)
+        public QueryResult<Device> GetDevices(DeviceQuery query)
         {
-            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
-            await using (dbContext.ConfigureAwait(false))
+            var devices = _devices.Values.OrderBy(d => d.Id)
+                .Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
+                .Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
+                .Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken);
+            var canGetCountDirectly = devices.TryGetNonEnumeratedCount(out var count);
+            if (!canGetCountDirectly)
             {
-                var devices = dbContext.Devices
-                    .OrderBy(d => d.Id)
-                    .Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value))
-                    .Where(device => query.DeviceId == null || device.DeviceId == query.DeviceId)
-                    .Where(device => query.AccessToken == null || device.AccessToken == query.AccessToken);
-
-                var count = await devices.CountAsync().ConfigureAwait(false);
-
-                if (query.Skip.HasValue)
-                {
-                    devices = devices.Skip(query.Skip.Value);
-                }
+                count = devices.Count();
+            }
 
-                if (query.Limit.HasValue)
-                {
-                    devices = devices.Take(query.Limit.Value);
-                }
+            if (query.Skip.HasValue)
+            {
+                devices = devices.Skip(query.Skip.Value);
+            }
 
-                return new QueryResult<Device>(query.Skip, count, await devices.ToListAsync().ConfigureAwait(false));
+            if (query.Limit.HasValue)
+            {
+                devices = devices.Take(query.Limit.Value);
             }
+
+            return new QueryResult<Device>(query.Skip, count, devices.ToList());
         }
 
         /// <inheritdoc />
-        public async Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query)
+        public QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query)
         {
-            var devices = await GetDevices(query).ConfigureAwait(false);
+            var devices = GetDevices(query);
 
             return new QueryResult<DeviceInfo>(
                 devices.StartIndex,
@@ -167,38 +171,38 @@ namespace Jellyfin.Server.Implementations.Devices
         }
 
         /// <inheritdoc />
-        public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId)
+        public QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId)
         {
-            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
-            await using (dbContext.ConfigureAwait(false))
+            var devices = _devices.Values
+                .OrderByDescending(d => d.DateLastActivity)
+                .ThenBy(d => d.DeviceId)
+                .AsEnumerable();
+
+            if (!userId.IsNullOrEmpty())
             {
-                var sessions = dbContext.Devices
-                    .Include(d => d.User)
-                    .OrderByDescending(d => d.DateLastActivity)
-                    .ThenBy(d => d.DeviceId)
-                    .SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
-                    .AsAsyncEnumerable();
-
-                if (!userId.IsNullOrEmpty())
+                var user = _userManager.GetUserById(userId.Value);
+                if (user is null)
                 {
-                    var user = _userManager.GetUserById(userId.Value);
-                    if (user is null)
-                    {
-                        throw new ResourceNotFoundException();
-                    }
-
-                    sessions = sessions.Where(i => CanAccessDevice(user, i.Device.DeviceId));
+                    throw new ResourceNotFoundException();
                 }
 
-                var array = await sessions.Select(device => ToDeviceInfo(device.Device, device.Options)).ToArrayAsync().ConfigureAwait(false);
-
-                return new QueryResult<DeviceInfo>(array);
+                devices = devices.Where(i => CanAccessDevice(user, i.DeviceId));
             }
+
+            var array = devices.Select(device =>
+                {
+                    _deviceOptions.TryGetValue(device.DeviceId, out var option);
+                    return ToDeviceInfo(device, option);
+                }).ToArray();
+
+            return new QueryResult<DeviceInfo>(array);
         }
 
         /// <inheritdoc />
         public async Task DeleteDevice(Device device)
         {
+            var id = _devices.FirstOrDefault(x => x.Value.Equals(device)).Key;
+            _devices.Remove(id);
             var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
             await using (dbContext.ConfigureAwait(false))
             {
@@ -207,6 +211,19 @@ namespace Jellyfin.Server.Implementations.Devices
             }
         }
 
+        /// <inheritdoc />
+        public async Task UpdateDevice(Device device)
+        {
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                dbContext.Devices.Update(device);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
+
+            _devices[device.Id] = device;
+        }
+
         /// <inheritdoc />
         public bool CanAccessDevice(User user, string deviceId)
         {

+ 2 - 18
Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs

@@ -1,6 +1,5 @@
 using System;
 using System.IO;
-using EFCoreSecondLevelCacheInterceptor;
 using MediaBrowser.Common.Configuration;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.DependencyInjection;
@@ -16,28 +15,13 @@ public static class ServiceCollectionExtensions
     /// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
     /// </summary>
     /// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param>
-    /// <param name="disableSecondLevelCache">Whether second level cache disabled..</param>
     /// <returns>The updated service collection.</returns>
-    public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection, bool disableSecondLevelCache)
+    public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection)
     {
-        if (!disableSecondLevelCache)
-        {
-            serviceCollection.AddEFSecondLevelCache(options =>
-                options.UseMemoryCacheProvider()
-                    .CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
-                    .UseCacheKeyPrefix("EF_")
-                    // Don't cache null values. Remove this optional setting if it's not necessary.
-                    .SkipCachingResults(result => result.Value is null or EFTableRows { RowsCount: 0 }));
-        }
-
         serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
         {
             var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
-            var dbOpt = opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
-            if (!disableSecondLevelCache)
-            {
-                dbOpt.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
-            }
+            opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
         });
 
         return serviceCollection;

+ 0 - 1
Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj

@@ -27,7 +27,6 @@
 
   <ItemGroup>
     <PackageReference Include="AsyncKeyedLock" />
-    <PackageReference Include="EFCoreSecondLevelCacheInterceptor" />
     <PackageReference Include="System.Linq.Async" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />

+ 12 - 3
Jellyfin.Server.Implementations/Security/AuthorizationContext.cs

@@ -4,7 +4,10 @@ using System;
 using System.Collections.Generic;
 using System.Net;
 using System.Threading.Tasks;
+using Jellyfin.Data.Queries;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller;
+using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using Microsoft.AspNetCore.Http;
@@ -17,15 +20,18 @@ namespace Jellyfin.Server.Implementations.Security
     {
         private readonly IDbContextFactory<JellyfinDbContext> _jellyfinDbProvider;
         private readonly IUserManager _userManager;
+        private readonly IDeviceManager _deviceManager;
         private readonly IServerApplicationHost _serverApplicationHost;
 
         public AuthorizationContext(
             IDbContextFactory<JellyfinDbContext> jellyfinDb,
             IUserManager userManager,
+            IDeviceManager deviceManager,
             IServerApplicationHost serverApplicationHost)
         {
             _jellyfinDbProvider = jellyfinDb;
             _userManager = userManager;
+            _deviceManager = deviceManager;
             _serverApplicationHost = serverApplicationHost;
         }
 
@@ -121,7 +127,11 @@ namespace Jellyfin.Server.Implementations.Security
             var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
             await using (dbContext.ConfigureAwait(false))
             {
-                var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
+                var device = _deviceManager.GetDevices(
+                    new DeviceQuery
+                    {
+                        AccessToken = token
+                    }).Items.FirstOrDefault();
 
                 if (device is not null)
                 {
@@ -178,8 +188,7 @@ namespace Jellyfin.Server.Implementations.Security
 
                     if (updateToken)
                     {
-                        dbContext.Devices.Update(device);
-                        await dbContext.SaveChangesAsync().ConfigureAwait(false);
+                        await _deviceManager.UpdateDevice(device).ConfigureAwait(false);
                     }
                 }
                 else

+ 2 - 2
Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs

@@ -60,10 +60,10 @@ public sealed class DeviceAccessHost : IHostedService
 
     private async Task UpdateDeviceAccess(User user)
     {
-        var existing = (await _deviceManager.GetDevices(new DeviceQuery
+        var existing = _deviceManager.GetDevices(new DeviceQuery
         {
             UserId = user.Id
-        }).ConfigureAwait(false)).Items;
+        }).Items;
 
         foreach (var device in existing)
         {

+ 7 - 5
MediaBrowser.Controller/Devices/IDeviceManager.cs

@@ -44,26 +44,28 @@ namespace MediaBrowser.Controller.Devices
         /// </summary>
         /// <param name="id">The identifier.</param>
         /// <returns>DeviceInfo.</returns>
-        Task<DeviceInfo> GetDevice(string id);
+        DeviceInfo GetDevice(string id);
 
         /// <summary>
         /// Gets devices based on the provided query.
         /// </summary>
         /// <param name="query">The device query.</param>
         /// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
-        Task<QueryResult<Device>> GetDevices(DeviceQuery query);
+        QueryResult<Device> GetDevices(DeviceQuery query);
 
-        Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query);
+        QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query);
 
         /// <summary>
         /// Gets the devices.
         /// </summary>
         /// <param name="userId">The user's id, or <c>null</c>.</param>
         /// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
-        Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId);
+        QueryResult<DeviceInfo> GetDevicesForUser(Guid? userId);
 
         Task DeleteDevice(Device device);
 
+        Task UpdateDevice(Device device);
+
         /// <summary>
         /// Determines whether this instance [can access device] the specified user identifier.
         /// </summary>
@@ -74,6 +76,6 @@ namespace MediaBrowser.Controller.Devices
 
         Task UpdateDeviceOptions(string deviceId, string deviceName);
 
-        Task<DeviceOptions> GetDeviceOptions(string deviceId);
+        DeviceOptions GetDeviceOptions(string deviceId);
     }
 }