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))