diff --git a/src/OrchardCore/OrchardCore.Abstractions/Shell/Builders/ShellContext.cs b/src/OrchardCore/OrchardCore.Abstractions/Shell/Builders/ShellContext.cs index 7ffb9c2ab3f..76374006698 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Shell/Builders/ShellContext.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Shell/Builders/ShellContext.cs @@ -44,6 +44,8 @@ public PlaceHolder() _released = true; _disposed = true; } + + public bool PreCreated { get; init; } } /// diff --git a/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellContextFactoryExtensions.cs b/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellContextFactoryExtensions.cs index 1ca0e9c83f7..2393b8d658f 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellContextFactoryExtensions.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellContextFactoryExtensions.cs @@ -30,7 +30,10 @@ public static async Task CreateMaximumContextAsync(this IShellCont public static Task CreateMinimumContextAsync(this IShellContextFactory shellContextFactory, ShellSettings shellSettings) => shellContextFactory.CreateDescribedContextAsync(shellSettings, new ShellDescriptor()); - private static async Task GetShellDescriptorAsync(this IShellContextFactory shellContextFactory, ShellSettings shellSettings) + /// + /// Gets the shell descriptor from the store. + /// + public static async Task GetShellDescriptorAsync(this IShellContextFactory shellContextFactory, ShellSettings shellSettings) { ShellDescriptor shellDescriptor = null; diff --git a/src/OrchardCore/OrchardCore/Shell/Builders/ShellContextFactory.cs b/src/OrchardCore/OrchardCore/Shell/Builders/ShellContextFactory.cs index 81488235eb2..81a6c3548c4 100644 --- a/src/OrchardCore/OrchardCore/Shell/Builders/ShellContextFactory.cs +++ b/src/OrchardCore/OrchardCore/Shell/Builders/ShellContextFactory.cs @@ -34,7 +34,7 @@ async Task IShellContextFactory.CreateShellContextAsync(ShellSetti _logger.LogInformation("Creating shell context for tenant '{TenantName}'", settings.Name); } - var describedContext = await CreateDescribedContextAsync(settings, MinimumShellDescriptor()); + var describedContext = await CreateDescribedContextAsync(settings, new ShellDescriptor()); ShellDescriptor currentDescriptor = null; await describedContext.CreateScope().UsingServiceScopeAsync(async scope => @@ -52,13 +52,13 @@ await describedContext.CreateScope().UsingServiceScopeAsync(async scope => return describedContext; } - // TODO: This should be provided by a ISetupService that returns a set of ShellFeature instances. Task IShellContextFactory.CreateSetupContextAsync(ShellSettings settings) { if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug("No shell settings available. Creating shell context for setup"); } + var descriptor = MinimumShellDescriptor(); return CreateDescribedContextAsync(settings, descriptor); diff --git a/src/OrchardCore/OrchardCore/Shell/Distributed/DistributedContext.cs b/src/OrchardCore/OrchardCore/Shell/Distributed/DistributedContext.cs index a3b4f2e6e42..9ab3f8d9ea8 100644 --- a/src/OrchardCore/OrchardCore/Shell/Distributed/DistributedContext.cs +++ b/src/OrchardCore/OrchardCore/Shell/Distributed/DistributedContext.cs @@ -33,6 +33,8 @@ public DistributedContext(ShellContext context) DistributedCache = distributedCache; } + public ShellContext Context => _context; + public IDistributedCache DistributedCache { get; } public DistributedContext Acquire() diff --git a/src/OrchardCore/OrchardCore/Shell/Distributed/DistributedShellHostedService.cs b/src/OrchardCore/OrchardCore/Shell/Distributed/DistributedShellHostedService.cs index a1b6780cc18..186c0ba9cd1 100644 --- a/src/OrchardCore/OrchardCore/Shell/Distributed/DistributedShellHostedService.cs +++ b/src/OrchardCore/OrchardCore/Shell/Distributed/DistributedShellHostedService.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OrchardCore.Environment.Shell.Builders; +using OrchardCore.Environment.Shell.Descriptor.Models; using OrchardCore.Environment.Shell.Models; using OrchardCore.Environment.Shell.Removing; using OrchardCore.Modules; @@ -18,14 +19,14 @@ namespace OrchardCore.Environment.Shell.Distributed /// internal class DistributedShellHostedService : BackgroundService { - private const string ShellChangedIdKey = "SHELL_CHANGED_ID"; - private const string ShellCountChangedIdKey = "SHELL_COUNT_CHANGED_ID"; - private const string ReleaseIdKeySuffix = "_RELEASE_ID"; - private const string ReloadIdKeySuffix = "_RELOAD_ID"; + private const string _shellChangedIdKey = "SHELL_CHANGED_ID"; + private const string _shellCountChangedIdKey = "SHELL_COUNT_CHANGED_ID"; + private const string _releaseIdKeySuffix = "_RELEASE_ID"; + private const string _reloadIdKeySuffix = "_RELOAD_ID"; - private static readonly TimeSpan MinIdleTime = TimeSpan.FromSeconds(1); - private static readonly TimeSpan MaxRetryTime = TimeSpan.FromMinutes(1); - private static readonly TimeSpan MaxBusyTime = TimeSpan.FromSeconds(2); + private static readonly TimeSpan _minIdleTime = TimeSpan.FromSeconds(1); + private static readonly TimeSpan _maxRetryTime = TimeSpan.FromMinutes(1); + private static readonly TimeSpan _maxBusyTime = TimeSpan.FromSeconds(2); private readonly IShellHost _shellHost; private readonly IShellContextFactory _shellContextFactory; @@ -33,8 +34,8 @@ internal class DistributedShellHostedService : BackgroundService private readonly IShellRemovalManager _shellRemovingManager; private readonly ILogger _logger; - private readonly ConcurrentDictionary _identifiers = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _semaphores = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _identifiers = new(); + private readonly ConcurrentDictionary _semaphores = new(); private string _shellChangedId; private string _shellCountChangedId; @@ -78,7 +79,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) }); // Init the idle time. - var idleTime = MinIdleTime; + var idleTime = _minIdleTime; // Init the second counter used to sync the default tenant while it is 'Uninitialized'. var defaultTenantSyncingSeconds = 0; @@ -140,7 +141,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) string shellChangedId; try { - shellChangedId = await distributedCache.GetStringAsync(ShellChangedIdKey); + shellChangedId = await distributedCache.GetStringAsync(_shellChangedIdKey); } catch (Exception ex) when (!ex.IsFatal()) { @@ -150,7 +151,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } // Reset the idle time. - idleTime = MinIdleTime; + idleTime = _minIdleTime; // Check if at least one tenant has changed. if (shellChangedId == null || _shellChangedId == shellChangedId) @@ -162,7 +163,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) string shellCountChangedId; try { - shellCountChangedId = await distributedCache.GetStringAsync(ShellCountChangedIdKey); + shellCountChangedId = await distributedCache.GetStringAsync(_shellCountChangedIdKey); } catch (Exception ex) when (!ex.IsFatal()) { @@ -309,14 +310,14 @@ public async Task LoadingAsync() } // If there is no default tenant or it is not running, nothing to do. - var defautSettings = await _shellSettingsManager.LoadSettingsAsync(ShellHelper.DefaultShellName); - if (defautSettings?.State != TenantState.Running) + var defaultSettings = await _shellSettingsManager.LoadSettingsAsync(ShellHelper.DefaultShellName); + if (defaultSettings?.State != TenantState.Running) { return; } // Create a local distributed context because it is not yet initialized. - using var context = await CreateDistributedContextAsync(defautSettings); + var context = _context = await CreateDistributedContextAsync(defaultSettings); // If the required distributed features are not enabled, nothing to do. var distributedCache = context?.DistributedCache; @@ -328,8 +329,8 @@ public async Task LoadingAsync() try { // Retrieve the tenant global identifiers from the distributed cache. - var shellChangedId = await distributedCache.GetStringAsync(ShellChangedIdKey); - var shellCountChangedId = await distributedCache.GetStringAsync(ShellCountChangedIdKey); + var shellChangedId = await distributedCache.GetStringAsync(_shellChangedIdKey); + var shellCountChangedId = await distributedCache.GetStringAsync(_shellCountChangedIdKey); // Retrieve the names of all the tenants. var names = await _shellSettingsManager.LoadSettingsNamesAsync(); @@ -403,7 +404,7 @@ public async Task ReleasingAsync(string name) await distributedCache.SetStringAsync(ReleaseIdKey(name), identifier.ReleaseId); // Also update the global identifier specifying that a tenant has changed. - await distributedCache.SetStringAsync(ShellChangedIdKey, identifier.ReleaseId); + await distributedCache.SetStringAsync(_shellChangedIdKey, identifier.ReleaseId); } catch (Exception ex) when (!ex.IsFatal()) { @@ -457,11 +458,11 @@ public async Task ReloadingAsync(string name) if (name != ShellHelper.DefaultShellName && !_shellHost.TryGetSettings(name, out _)) { // Also update the global identifier specifying that a tenant has been created. - await distributedCache.SetStringAsync(ShellCountChangedIdKey, identifier.ReloadId); + await distributedCache.SetStringAsync(_shellCountChangedIdKey, identifier.ReloadId); } // Also update the global identifier specifying that a tenant has changed. - await distributedCache.SetStringAsync(ShellChangedIdKey, identifier.ReloadId); + await distributedCache.SetStringAsync(_shellChangedIdKey, identifier.ReloadId); } catch (Exception ex) when (!ex.IsFatal()) { @@ -508,10 +509,10 @@ public async Task RemovingAsync(string name) var removedId = IdGenerator.GenerateId(); // Also update the global identifier specifying that a tenant has been removed. - await distributedCache.SetStringAsync(ShellCountChangedIdKey, removedId); + await distributedCache.SetStringAsync(_shellCountChangedIdKey, removedId); // Also update the global identifier specifying that a tenant has changed. - await distributedCache.SetStringAsync(ShellChangedIdKey, removedId); + await distributedCache.SetStringAsync(_shellChangedIdKey, removedId); } catch (Exception ex) when (!ex.IsFatal()) { @@ -523,31 +524,43 @@ public async Task RemovingAsync(string name) } } - private static string ReleaseIdKey(string name) => name + ReleaseIdKeySuffix; - private static string ReloadIdKey(string name) => name + ReloadIdKeySuffix; + private static string ReleaseIdKey(string name) => name + _releaseIdKeySuffix; + private static string ReloadIdKey(string name) => name + _reloadIdKeySuffix; /// - /// Creates a distributed context based on the default tenant settings and descriptor. + /// Creates a distributed context based on the default tenant context. /// - private async Task CreateDistributedContextAsync(ShellContext defaultShell) + private async Task CreateDistributedContextAsync(ShellContext defaultContext) { - // Capture the descriptor as the blueprint may be set to null right after. - var descriptor = defaultShell.Blueprint?.Descriptor; - if (descriptor != null) + // Get the default tenant descriptor. + var descriptor = await GetDefaultShellDescriptorAsync(defaultContext); + + // If no descriptor. + if (descriptor == null) { - // Using the current shell descriptor prevents a database access, and a race condition - // when resolving `IStore` while the default tenant is activating and does migrations. - try - { - return new DistributedContext(await _shellContextFactory.CreateDescribedContextAsync(defaultShell.Settings, descriptor)); - } - catch - { - return null; - } + // Nothing to create. + return null; } - return await CreateDistributedContextAsync(defaultShell.Settings); + // Creates a new context based on the default settings and descriptor. + return await CreateDistributedContextAsync(defaultContext.Settings, descriptor); + } + + /// + /// Creates a distributed context based on the default tenant settings and descriptor. + /// + private async Task CreateDistributedContextAsync(ShellSettings defaultSettings, ShellDescriptor descriptor) + { + // Using the current shell descriptor prevents a database access, and a race condition + // when resolving `IStore` while the default tenant is activating and does migrations. + try + { + return new DistributedContext(await _shellContextFactory.CreateDescribedContextAsync(defaultSettings, descriptor)); + } + catch + { + return null; + } } /// @@ -566,7 +579,32 @@ private async Task CreateDistributedContextAsync(ShellSettin } /// - /// Gets the distributed context or creates a new one if the default tenant has changed. + /// Gets the default tenant descriptor. + /// + private async Task GetDefaultShellDescriptorAsync(ShellContext defaultContext) + { + // Capture the descriptor as the blueprint may be set to null right after. + var descriptor = defaultContext.Blueprint?.Descriptor; + + // No descriptor if the default context is a placeholder without blueprint. + if (descriptor == null) + { + try + { + // Get the default tenant descriptor from the store. + descriptor = await _shellContextFactory.GetShellDescriptorAsync(defaultContext.Settings); + } + catch + { + return null; + } + } + + return descriptor; + } + + /// + /// Gets or creates a new distributed context if the default tenant has changed. /// private async Task GetOrCreateDistributedContextAsync(ShellContext defaultContext) { @@ -575,18 +613,60 @@ private async Task GetOrCreateDistributedContextAsync(ShellC { var previousContext = _context; - // Create a new distributed context based on the default tenant. - _context = await CreateDistributedContextAsync(defaultContext); + // Reuse or create a new context based on the default tenant. + _context = await ReuseOrCreateDistributedContextAsync(defaultContext); - if (_context != null) + // Cache the default context. + _defaultContext = defaultContext; + + // If the context is not reused. + if (_context != previousContext) { - _defaultContext = defaultContext; + // Release the previous one. + previousContext?.Release(); } + } + + return _context; + } - // Release the previous one. - previousContext?.Release(); + /// + /// Reuses or creates a new distributed context based on the default tenant context. + /// + private async Task ReuseOrCreateDistributedContextAsync(ShellContext defaultContext) + { + // If no context. + if (_context == null) + { + // Create a new context based on the default context. + return await CreateDistributedContextAsync(defaultContext); + } + + // Check if the default context is still the placeholder pre-created on loading. + if (defaultContext is ShellContext.PlaceHolder placeholder && placeholder.PreCreated) + { + // Reuse the current context. + return _context; + } + + // Get the default tenant descriptor. + var descriptor = await GetDefaultShellDescriptorAsync(defaultContext); + + // If no descriptor. + if (descriptor == null) + { + // Nothing to create. + return null; + } + + // Check if the default tenant descriptor was updated. + if (_context.Context.Blueprint.Descriptor.SerialNumber != descriptor.SerialNumber) + { + // Creates a new context based on the default settings and descriptor. + return await CreateDistributedContextAsync(defaultContext.Settings, descriptor); } + // Reuse the current context. return _context; } @@ -595,9 +675,11 @@ private async Task GetOrCreateDistributedContextAsync(ShellC /// private Task AcquireOrCreateDistributedContextAsync(ShellContext defaultContext) { + // Acquire the current context. var distributedContext = _context?.Acquire(); if (distributedContext == null) { + // Create a new context based on the default context. return CreateDistributedContextAsync(defaultContext); } @@ -609,15 +691,15 @@ private Task AcquireOrCreateDistributedContextAsync(ShellCon /// private TimeSpan NextIdleTimeBeforeRetry(TimeSpan idleTime, Exception ex) { - if (idleTime < MaxRetryTime) + if (idleTime < _maxRetryTime) { // Log an error on each retry, but only before reaching the 'MaxRetryTime', to not fill out the log. _logger.LogError(ex, "Unable to read the distributed cache before checking if a tenant has changed."); idleTime *= 2; - if (idleTime > MaxRetryTime) + if (idleTime > _maxRetryTime) { - idleTime = MaxRetryTime; + idleTime = _maxRetryTime; } } @@ -629,9 +711,9 @@ private TimeSpan NextIdleTimeBeforeRetry(TimeSpan idleTime, Exception ex) /// private async Task TryWaitAfterBusyTime(CancellationToken stoppingToken) { - if (DateTime.UtcNow - _busyStartTime > MaxBusyTime) + if (DateTime.UtcNow - _busyStartTime > _maxBusyTime) { - if (!await TryWaitAsync(MinIdleTime, stoppingToken)) + if (!await TryWaitAsync(_minIdleTime, stoppingToken)) { return false; } diff --git a/src/OrchardCore/OrchardCore/Shell/ShellHost.cs b/src/OrchardCore/OrchardCore/Shell/ShellHost.cs index 438885bb878..bdcd1463fd6 100644 --- a/src/OrchardCore/OrchardCore/Shell/ShellHost.cs +++ b/src/OrchardCore/OrchardCore/Shell/ShellHost.cs @@ -345,7 +345,7 @@ private async Task PreCreateAndRegisterShellsAsync() // Pre-create and register all tenant shells. foreach (var settings in allSettings) { - AddAndRegisterShell(new ShellContext.PlaceHolder { Settings = settings }); + AddAndRegisterShell(new ShellContext.PlaceHolder { Settings = settings, PreCreated = true }); }; if (_logger.IsEnabled(LogLevel.Information))