Skip to content

Commit

Permalink
Restore caching for UserManager.cs
Browse files Browse the repository at this point in the history
It seems like the EFCore's second level cache does not really work, and we are having very heavy database query here.

Signed-off-by: gnattu <gnattuoc@me.com>
  • Loading branch information
gnattu committed May 15, 2024
1 parent 3f760e6 commit 0756174
Showing 1 changed file with 78 additions and 81 deletions.
159 changes: 78 additions & 81 deletions Jellyfin.Server.Implementations/Users/UserManager.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#pragma warning disable CA1307
#pragma warning disable CA1309 // Use ordinal string comparison - EF can't translate this

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
Expand Down Expand Up @@ -47,6 +47,8 @@ public partial class UserManager : IUserManager
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
private readonly IServerConfigurationManager _serverConfigurationManager;

private readonly IDictionary<Guid, User> _users;

/// <summary>
/// Initializes a new instance of the <see cref="UserManager"/> class.
/// </summary>
Expand Down Expand Up @@ -84,30 +86,29 @@ public UserManager(
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();

_users = new ConcurrentDictionary<Guid, User>();
using var dbContext = _dbProvider.CreateDbContext();
foreach (var user in dbContext.Users
.AsSplitQuery()
.Include(user => user.Permissions)
.Include(user => user.Preferences)
.Include(user => user.AccessSchedules)
.Include(user => user.ProfileImage)
.AsEnumerable())
{
_users.Add(user.Id, user);
}
}

/// <inheritdoc/>
public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;

/// <inheritdoc/>
public IEnumerable<User> Users
{
get
{
using var dbContext = _dbProvider.CreateDbContext();
return GetUsersInternal(dbContext).ToList();
}
}
public IEnumerable<User> Users => _users.Values;

/// <inheritdoc/>
public IEnumerable<Guid> UsersIds
{
get
{
using var dbContext = _dbProvider.CreateDbContext();
return dbContext.Users.Select(u => u.Id).ToList();
}
}
public IEnumerable<Guid> UsersIds => _users.Keys;

// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
Expand All @@ -123,8 +124,8 @@ public IEnumerable<Guid> UsersIds
throw new ArgumentException("Guid can't be empty", nameof(id));
}

using var dbContext = _dbProvider.CreateDbContext();
return GetUsersInternal(dbContext).FirstOrDefault(u => u.Id.Equals(id));
_users.TryGetValue(id, out var user);
return user;
}

/// <inheritdoc/>
Expand All @@ -135,9 +136,7 @@ public IEnumerable<Guid> UsersIds
throw new ArgumentException("Invalid username", nameof(name));
}

using var dbContext = _dbProvider.CreateDbContext();
return GetUsersInternal(dbContext)
.FirstOrDefault(u => string.Equals(u.Username, name));
return _users.Values.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase));
}

/// <inheritdoc/>
Expand Down Expand Up @@ -202,6 +201,8 @@ internal async Task<User> CreateUserInternalAsync(string name, JellyfinDbContext
user.AddDefaultPermissions();
user.AddDefaultPreferences();

_users.Add(user.Id, user);

return user;
}

Expand Down Expand Up @@ -236,46 +237,40 @@ public async Task<User> CreateUserAsync(string name)
/// <inheritdoc/>
public async Task DeleteUserAsync(Guid userId)
{
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
if (!_users.TryGetValue(userId, out var user))
{
throw new ResourceNotFoundException(nameof(userId));
}

await using (dbContext.ConfigureAwait(false))
if (_users.Count == 1)
{
var user = await dbContext.Users
.AsSingleQuery()
.Include(u => u.Permissions)
.FirstOrDefaultAsync(u => u.Id.Equals(userId))
.ConfigureAwait(false);
if (user is null)
{
throw new ResourceNotFoundException(nameof(userId));
}
throw new InvalidOperationException(string.Format(
CultureInfo.InvariantCulture,
"The user '{0}' cannot be deleted because there must be at least one user in the system.",
user.Username));
}

if (await dbContext.Users.CountAsync().ConfigureAwait(false) == 1)
{
throw new InvalidOperationException(string.Format(
if (user.HasPermission(PermissionKind.IsAdministrator)
&& Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
{
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture,
"The user '{0}' cannot be deleted because there must be at least one user in the system.",
user.Username));
}

if (user.HasPermission(PermissionKind.IsAdministrator)
&& await dbContext.Users
.CountAsync(u => u.Permissions.Any(p => p.Kind == PermissionKind.IsAdministrator && p.Value))
.ConfigureAwait(false) == 1)
{
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture,
"The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
user.Username),
nameof(userId));
}
"The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
user.Username),
nameof(userId));
}

var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
dbContext.Users.Remove(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false);

await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
}

_users.Remove(userId);

await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
}

/// <inheritdoc/>
Expand Down Expand Up @@ -542,23 +537,23 @@ public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
/// <inheritdoc />
public async Task InitializeAsync()
{
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
if (_users.Any())
{
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
{
return;
}
return;
}

var defaultName = Environment.UserName;
if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
{
defaultName = "MyJellyfinUser";
}
var defaultName = Environment.UserName;
if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
{
defaultName = "MyJellyfinUser";
}

_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);

var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
newUser.SetPermission(PermissionKind.IsAdministrator, true);
newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
Expand Down Expand Up @@ -605,9 +600,12 @@ public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var user = await GetUsersInternal(dbContext)
.FirstOrDefaultAsync(u => u.Id.Equals(userId))
.ConfigureAwait(false)
var user = dbContext.Users
.Include(u => u.Permissions)
.Include(u => u.Preferences)
.Include(u => u.AccessSchedules)
.Include(u => u.ProfileImage)
.FirstOrDefault(u => u.Id.Equals(userId))
?? throw new ArgumentException("No user exists with given Id!");

user.SubtitleMode = config.SubtitleMode;
Expand Down Expand Up @@ -635,6 +633,7 @@ public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);

dbContext.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
}
Expand All @@ -645,9 +644,12 @@ public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var user = await GetUsersInternal(dbContext)
.FirstOrDefaultAsync(u => u.Id.Equals(userId))
.ConfigureAwait(false)
var user = dbContext.Users
.Include(u => u.Permissions)
.Include(u => u.Preferences)
.Include(u => u.AccessSchedules)
.Include(u => u.ProfileImage)
.FirstOrDefault(u => u.Id.Equals(userId))
?? throw new ArgumentException("No user exists with given Id!");

// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
Expand Down Expand Up @@ -708,6 +710,7 @@ public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);

dbContext.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
}
Expand All @@ -728,6 +731,7 @@ public async Task ClearProfileImageAsync(User user)
}

user.ProfileImage = null;
_users[user.Id] = user;
}

internal static void ThrowIfInvalidUsername(string name)
Expand Down Expand Up @@ -874,15 +878,8 @@ private async Task IncrementInvalidLoginAttemptCount(User user)
private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
{
dbContext.Users.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}

private IQueryable<User> GetUsersInternal(JellyfinDbContext dbContext)
=> dbContext.Users
.AsSplitQuery()
.Include(user => user.Permissions)
.Include(user => user.Preferences)
.Include(user => user.AccessSchedules)
.Include(user => user.ProfileImage);
}
}

0 comments on commit 0756174

Please sign in to comment.