From 09b210d5de6f8d21c07613fecdd94f6cdde59752 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Wed, 4 May 2022 10:13:27 +0100 Subject: [PATCH] Hack fix - SQLite deadlock issue for login after session timeout. --- src/Umbraco.Core/Services/UserService.cs | 7 +++-- .../Repositories/Implement/UserRepository.cs | 26 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 6179f5560e..f0b5cc6a32 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -509,11 +509,14 @@ namespace Umbraco.Cms.Core.Services public bool ValidateLoginSession(int userId, Guid sessionId) { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { - return _userRepository.ValidateLoginSession(userId, sessionId); + var result = _userRepository.ValidateLoginSession(userId, sessionId); + scope.Complete(); + return result; } } + public IDictionary GetUserStates() { using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 106d1c906f..5f44dc6781 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -36,6 +36,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement private readonly IRuntimeState _runtimeState; private string? _passwordConfigJson; private bool _passwordConfigInitialized; + private readonly object _sqliteValidateSessionLock = new(); /// /// Initializes a new instance of the class. @@ -217,6 +218,23 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 } public bool ValidateLoginSession(int userId, Guid sessionId) + { + // HACK: Avoid a deadlock - BackOfficeCookieOptions OnValidatePrincipal + // After existing session times out and user logs in again ~ 4 requests come in at once that hit the + // "update the validate date" code path, check up the call stack there are a few variables that can make this not occur. + // TODO: more generic fix, do something with ForUpdate? wait on a mutex? add a distributed lock? etc. + if (Database.DatabaseType.IsSqlite()) + { + lock (_sqliteValidateSessionLock) + { + return ValidateLoginSessionInternal(userId, sessionId); + } + } + + return ValidateLoginSessionInternal(userId, sessionId); + } + + private bool ValidateLoginSessionInternal(int userId, Guid sessionId) { // with RepeatableRead transaction mode, read-then-update operations can // cause deadlocks, and the ForUpdate() hint is required to tell the database @@ -236,15 +254,17 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 if (found == null || found.UserId != userId || found.LoggedOutUtc.HasValue) return false; - //now detect if there's been a timeout + // now detect if there's been a timeout if (DateTime.UtcNow - found.LastValidatedUtc > _globalSettings.TimeOut) { - //timeout detected, update the record + // timeout detected, update the record + Logger.LogDebug("ClearLoginSession for sessionId {sessionId}", sessionId); ClearLoginSession(sessionId); return false; } - //update the validate date + // update the validate date + Logger.LogDebug("Updating LastValidatedUtc for sessionId {sessionId}", sessionId); found.LastValidatedUtc = DateTime.UtcNow; Database.Update(found); return true;