diff --git a/src/OrchardCore.Modules/OrchardCore.Admin/AdminPageRouteModelProvider.cs b/src/OrchardCore.Modules/OrchardCore.Admin/AdminPageRouteModelProvider.cs index bb2d1c43cc3..cac2bc9785f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Admin/AdminPageRouteModelProvider.cs +++ b/src/OrchardCore.Modules/OrchardCore.Admin/AdminPageRouteModelProvider.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.Extensions.Hosting; using OrchardCore.Mvc; @@ -13,8 +14,6 @@ namespace OrchardCore.Admin { internal class AdminPageRouteModelProvider : IPageRouteModelProvider { - private const string RazorPageDocumentKind = "mvc.1.0.razor-page"; - private readonly IHostEnvironment _hostingEnvironment; private readonly ApplicationPartManager _applicationManager; @@ -75,7 +74,8 @@ public void OnProvidersExecuted(PageRouteModelProviderContext context) { } - private static IEnumerable GetPageDescriptors(ApplicationPartManager applicationManager) where T : ViewsFeature, new() + private static IEnumerable GetPageDescriptors(ApplicationPartManager applicationManager) + where T : ViewsFeature, new() { if (applicationManager == null) { @@ -100,7 +100,8 @@ public void OnProvidersExecuted(PageRouteModelProviderContext context) } } - private static bool IsRazorPage(CompiledViewDescriptor viewDescriptor) => viewDescriptor.Item?.Kind == RazorPageDocumentKind; + private static bool IsRazorPage(CompiledViewDescriptor viewDescriptor) => + viewDescriptor.Item?.Kind == RazorPageDocumentClassifierPass.RazorPageDocumentKind; private static T GetViewFeature(ApplicationPartManager applicationManager) where T : ViewsFeature, new() { diff --git a/src/OrchardCore.Modules/OrchardCore.AutoSetup/AutoSetupMiddleware.cs b/src/OrchardCore.Modules/OrchardCore.AutoSetup/AutoSetupMiddleware.cs index 9d4bccc8cdb..fc639cba1f6 100644 --- a/src/OrchardCore.Modules/OrchardCore.AutoSetup/AutoSetupMiddleware.cs +++ b/src/OrchardCore.Modules/OrchardCore.AutoSetup/AutoSetupMiddleware.cs @@ -128,7 +128,10 @@ public async Task InvokeAsync(HttpContext httpContext) } // Check if the tenant was installed by another instance. - var settings = await _shellSettingsManager.LoadSettingsAsync(_shellSettings.Name); + using var settings = (await _shellSettingsManager + .LoadSettingsAsync(_shellSettings.Name)) + .AsDisposable(); + if (!settings.IsUninitialized()) { await _shellHost.ReloadShellContextAsync(_shellSettings, eventSource: false); @@ -204,9 +207,10 @@ public async Task SetupTenantAsync(ISetupService setupService, TenantSetup /// The . public async Task CreateTenantSettingsAsync(TenantSetupOptions setupOptions) { - var shellSettings = _shellSettingsManager + using var shellSettings = _shellSettingsManager .CreateDefaultSettings() - .AsUninitialized(); + .AsUninitialized() + .AsDisposable(); shellSettings.Name = setupOptions.ShellName; shellSettings.RequestUrlHost = setupOptions.RequestUrlHost; diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Tenants/Controllers/AdminController.cs index 1a87a18fde0..f1d5bcd90c3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Tenants/Controllers/AdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Controllers/AdminController.cs @@ -299,7 +299,10 @@ public async Task Create() var recipes = recipeCollections.SelectMany(x => x).Where(x => x.IsSetupRecipe).OrderBy(r => r.DisplayName).ToArray(); // Creates a default shell settings based on the configuration. - var shellSettings = _shellSettingsManager.CreateDefaultSettings(); + using var shellSettings = _shellSettingsManager + .CreateDefaultSettings() + .AsUninitialized() + .AsDisposable(); var currentFeatureProfiles = shellSettings.GetFeatureProfiles(); var featureProfiles = await GetFeatureProfilesAsync(currentFeatureProfiles); @@ -348,9 +351,10 @@ public async Task Create(EditTenantViewModel model) if (ModelState.IsValid) { // Creates a default shell settings based on the configuration. - var shellSettings = _shellSettingsManager + using var shellSettings = _shellSettingsManager .CreateDefaultSettings() - .AsUninitialized(); + .AsUninitialized() + .AsDisposable(); shellSettings.Name = model.Name; shellSettings.RequestUrlHost = model.RequestUrlHost; diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Controllers/ApiController.cs b/src/OrchardCore.Modules/OrchardCore.Tenants/Controllers/ApiController.cs index 1a75da8c984..ab745349c36 100644 --- a/src/OrchardCore.Modules/OrchardCore.Tenants/Controllers/ApiController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Controllers/ApiController.cs @@ -105,9 +105,10 @@ public async Task Create(TenantApiModel model) if (model.IsNewTenant) { // Creates a default shell settings based on the configuration. - var shellSettings = _shellSettingsManager + using var shellSettings = _shellSettingsManager .CreateDefaultSettings() - .AsUninitialized(); + .AsUninitialized() + .AsDisposable(); shellSettings.Name = model.Name; shellSettings.RequestUrlHost = model.RequestUrlHost; @@ -124,14 +125,15 @@ public async Task Create(TenantApiModel model) shellSettings["FeatureProfile"] = string.Join(',', model.FeatureProfiles ?? Array.Empty()); await _shellHost.UpdateShellSettingsAsync(shellSettings); + var reloadedSettings = _shellHost.GetSettings(shellSettings.Name); - var token = CreateSetupToken(shellSettings); + var token = CreateSetupToken(reloadedSettings); - return Ok(GetEncodedUrl(shellSettings, token)); + return Ok(GetEncodedUrl(reloadedSettings, token)); } else { - // Site already exists, return 201 for indempotency purposes. + // Site already exists, return 201 for idempotency purposes. var token = CreateSetupToken(settings); diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Services/TenantValidator.cs b/src/OrchardCore.Modules/OrchardCore.Tenants/Services/TenantValidator.cs index 64247c03e56..af26f01d9a3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Tenants/Services/TenantValidator.cs +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Services/TenantValidator.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -82,7 +81,11 @@ public async Task> ValidateAsync(TenantModelBase model) if (existingShellSettings is null) { // Set the settings to be validated. - shellSettings = _shellSettingsManager.CreateDefaultSettings(); + shellSettings = _shellSettingsManager + .CreateDefaultSettings() + .AsUninitialized() + .AsDisposable(); + shellSettings.Name = model.Name; } else if (existingShellSettings.IsDefaultShell()) @@ -106,6 +109,9 @@ public async Task> ValidateAsync(TenantModelBase model) if (shellSettings is not null) { + // A newly loaded settings from the configuration should be disposed. + using var disposable = existingShellSettings is null ? shellSettings : null; + var validationContext = new DbConnectionValidatorContext(shellSettings, model); await ValidateConnectionAsync(validationContext, errors); } diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Activities/CreateTenantTask.cs b/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Activities/CreateTenantTask.cs index d14fd80838c..6e158d335eb 100644 --- a/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Activities/CreateTenantTask.cs +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Workflows/Activities/CreateTenantTask.cs @@ -118,7 +118,10 @@ public async override Task ExecuteAsync(WorkflowExecuti var featureProfile = (await ExpressionEvaluator.EvaluateAsync(FeatureProfile, workflowContext, null))?.Trim(); // Creates a default shell settings based on the configuration. - var shellSettings = ShellSettingsManager.CreateDefaultSettings(); + using var shellSettings = ShellSettingsManager + .CreateDefaultSettings() + .AsUninitialized() + .AsDisposable(); shellSettings.Name = tenantName; @@ -172,9 +175,10 @@ public async override Task ExecuteAsync(WorkflowExecuti shellSettings["Secret"] = Guid.NewGuid().ToString(); await ShellHost.UpdateShellSettingsAsync(shellSettings); + var reloadedSettings = ShellHost.GetSettings(shellSettings.Name); - workflowContext.LastResult = shellSettings; - workflowContext.CorrelationId = shellSettings.Name; + workflowContext.LastResult = reloadedSettings; + workflowContext.CorrelationId = reloadedSettings.Name; return Outcomes("Done"); } diff --git a/src/OrchardCore/OrchardCore.Abstractions/Shell/Builders/ShellContext.cs b/src/OrchardCore/OrchardCore.Abstractions/Shell/Builders/ShellContext.cs index 0915b0ba5a0..2eee80c5512 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Shell/Builders/ShellContext.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Shell/Builders/ShellContext.cs @@ -12,14 +12,24 @@ namespace OrchardCore.Environment.Shell.Builders /// public class ShellContext : IDisposable, IAsyncDisposable { - private bool _disposed; private List> _dependents; private readonly SemaphoreSlim _semaphore = new(1); + private bool _disposed; internal volatile int _refCount; internal volatile int _terminated; internal bool _released; + /// + /// Initializes a new . + /// + public ShellContext() => UtcTicks = DateTime.UtcNow.Ticks; + + /// + /// The creation date and time of this shell context in ticks. + /// + public long UtcTicks { get; } + /// /// The holding the tenant settings and configuration. /// @@ -56,7 +66,6 @@ public class PlaceHolder : ShellContext public PlaceHolder() { _released = true; - _disposed = true; } /// @@ -101,7 +110,12 @@ public async Task CreateScopeAsync() public int ActiveScopes => _refCount; /// - /// Mark the as released and then a candidate to be disposed. + /// Whether or not this instance uses shared that should not be disposed. + /// + public bool SharedSettings { get; internal set; } + + /// + /// Marks the as released and then a candidate to be disposed. /// public Task ReleaseAsync() => ReleaseInternalAsync(); @@ -111,6 +125,14 @@ public async Task CreateScopeAsync() internal async Task ReleaseInternalAsync(ReleaseMode mode = ReleaseMode.Normal) { + // A 'PlaceHolder' is always released. + if (this is PlaceHolder) + { + // But still try to dispose the settings. + await DisposeAsync(); + return; + } + if (_released) { // Prevent infinite loops with circular dependencies @@ -239,7 +261,6 @@ private void Close() _disposed = true; - // Disposes all the services registered for this shell. if (ServiceProvider is IDisposable disposable) { disposable.Dispose(); @@ -257,7 +278,6 @@ private async ValueTask CloseAsync() _disposed = true; - // Disposes all the services registered for this shell. if (ServiceProvider is IAsyncDisposable asyncDisposable) { await asyncDisposable.DisposeAsync(); @@ -276,6 +296,15 @@ private void Terminate() IsActivated = false; Blueprint = null; Pipeline = null; + + _semaphore?.Dispose(); + + if (SharedSettings) + { + return; + } + + Settings?.Dispose(); } ~ShellContext() => Close(); diff --git a/src/OrchardCore/OrchardCore.Abstractions/Shell/Configuration/Internal/UpdatableDataProvider.cs b/src/OrchardCore/OrchardCore.Abstractions/Shell/Configuration/Internal/UpdatableDataProvider.cs index 9ef4a86234d..c328e033a2c 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Shell/Configuration/Internal/UpdatableDataProvider.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Shell/Configuration/Internal/UpdatableDataProvider.cs @@ -3,24 +3,23 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Threading; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; namespace OrchardCore.Environment.Shell.Configuration.Internal { - internal class UpdatableDataProvider : IConfigurationProvider, IEnumerable> + internal class UpdatableDataProvider : IConfigurationProvider, IConfigurationSource, IEnumerable> { - private ConfigurationReloadToken _reloadToken = new(); + public UpdatableDataProvider() => Data = new(StringComparer.OrdinalIgnoreCase); public UpdatableDataProvider(IEnumerable> initialData) { - Data = new ConcurrentDictionary(initialData, StringComparer.OrdinalIgnoreCase); + initialData ??= Enumerable.Empty>(); + Data = new(initialData, StringComparer.OrdinalIgnoreCase); } - protected IDictionary Data { get; set; } - - public void Add(string key, string value) => Data.Add(key, value); + protected ConcurrentDictionary Data { get; set; } public IEnumerator> GetEnumerator() => Data.GetEnumerator(); @@ -51,17 +50,10 @@ private static string Segment(string key, int prefixLength) return indexOf < 0 ? key[prefixLength..] : key[prefixLength..indexOf]; } - public IChangeToken GetReloadToken() - { - return _reloadToken; - } - - protected void OnReload() - { - var previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken()); - previousToken.OnReload(); - } + public IChangeToken GetReloadToken() => NullChangeToken.Singleton; public override string ToString() => $"{GetType().Name}"; + + public IConfigurationProvider Build(IConfigurationBuilder builder) => this; } } diff --git a/src/OrchardCore/OrchardCore.Abstractions/Shell/Configuration/ShellConfiguration.cs b/src/OrchardCore/OrchardCore.Abstractions/Shell/Configuration/ShellConfiguration.cs index 86be2631f19..1d3767f76be 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Shell/Configuration/ShellConfiguration.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Shell/Configuration/ShellConfiguration.cs @@ -18,12 +18,11 @@ public class ShellConfiguration : IShellConfiguration { private IConfigurationRoot _configuration; private UpdatableDataProvider _updatableData; - private readonly IEnumerable> _initialData; private readonly string _name; - private readonly Func> _configBuilderFactory; - private readonly IEnumerable _configurationProviders; + private readonly Func, Task> _factoryAsync; private readonly SemaphoreSlim _semaphore = new(1); + private bool _released; public ShellConfiguration() { @@ -31,15 +30,24 @@ public ShellConfiguration() public ShellConfiguration(IConfiguration configuration) { - _configurationProviders = new ConfigurationBuilder() + _updatableData = new UpdatableDataProvider(); + + _configuration = new ConfigurationBuilder() .AddConfiguration(configuration) - .Build().Providers; + .Add(_updatableData) + .Build(); + } + + public ShellConfiguration(IConfigurationBuilder builder) + { + _updatableData = new UpdatableDataProvider(); + _configuration = builder.Add(_updatableData).Build(); } - public ShellConfiguration(string name, Func> factory) + public ShellConfiguration(string name, Func, Task> factoryAsync) { _name = name; - _configBuilderFactory = factory; + _factoryAsync = factoryAsync; } public ShellConfiguration(ShellConfiguration configuration) : this(null, configuration) @@ -50,29 +58,29 @@ public ShellConfiguration(string name, ShellConfiguration configuration) { _name = name; - if (configuration._configuration != null) + if (configuration._configuration is not null) { - _configurationProviders = configuration._configuration.Providers - .Where(p => p is not UpdatableDataProvider).ToArray(); + _updatableData = new UpdatableDataProvider(configuration._updatableData.ToArray()); - _initialData = configuration._updatableData.ToArray(); + _configuration = new ConfigurationBuilder() + .AddConfiguration(configuration._configuration, shouldDisposeConfiguration: true) + .Add(_updatableData) + .Build(); return; } - if (name == null) + if (name is null) { - _configurationProviders = configuration._configurationProviders; - _initialData = configuration._initialData; return; } - _configBuilderFactory = configuration._configBuilderFactory; + _factoryAsync = configuration._factoryAsync; } private void EnsureConfiguration() { - if (_configuration != null) + if (_configuration is not null) { return; } @@ -82,7 +90,7 @@ private void EnsureConfiguration() internal async Task EnsureConfigurationAsync() { - if (_configuration != null) + if (_configuration is not null) { return; } @@ -90,30 +98,16 @@ internal async Task EnsureConfigurationAsync() await _semaphore.WaitAsync(); try { - if (_configuration != null) + if (_configuration is not null) { return; } - var providers = new List(); - - if (_configBuilderFactory != null) - { - providers.AddRange(new ConfigurationBuilder() - .AddConfiguration((await _configBuilderFactory.Invoke(_name)).Build()) - .Build().Providers); - } - - if (_configurationProviders != null) - { - providers.AddRange(_configurationProviders); - } - - _updatableData = new UpdatableDataProvider(_initialData ?? Enumerable.Empty>()); + _updatableData = new UpdatableDataProvider(); - providers.Add(_updatableData); - - _configuration = new ConfigurationRoot(providers); + _configuration = _factoryAsync is not null + ? await _factoryAsync(_name, builder => builder.Add(_updatableData)) + : new ConfigurationBuilder().Add(_updatableData).Build(); } finally { @@ -122,7 +116,7 @@ internal async Task EnsureConfigurationAsync() } /// - /// The tenant lazily built . + /// The tenant configuration lazily built . /// private IConfiguration Configuration { @@ -150,19 +144,23 @@ public string this[string key] } } - public IConfigurationSection GetSection(string key) - { - return Configuration.GetSectionCompat(key); - } + public IConfigurationSection GetSection(string key) => Configuration.GetSectionCompat(key); - public IEnumerable GetChildren() - { - return Configuration.GetChildren(); - } + public IEnumerable GetChildren() => Configuration.GetChildren(); + + public IChangeToken GetReloadToken() => Configuration.GetReloadToken(); - public IChangeToken GetReloadToken() + public void Release() { - return Configuration.GetReloadToken(); + if (_released) + { + return; + } + + _released = true; + + (_configuration as IDisposable)?.Dispose(); + _semaphore?.Dispose(); } } } diff --git a/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellContextExtensions.cs b/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellContextExtensions.cs index 9fb4d1461a4..e32832fec8d 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellContextExtensions.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellContextExtensions.cs @@ -28,4 +28,25 @@ public static class ShellContextExtensions /// Whether or not the tenant is in use in at least one active scope. /// public static bool IsActive(this ShellContext context) => context is { ActiveScopes: > 0 }; + + /// + /// Marks this instance as using unshared that can be disposed. + /// + /// + /// This is the default but can be used if may have been called. + /// + public static ShellContext WithoutSharedSettings(this ShellContext context) + { + context.SharedSettings = false; + return context; + } + + /// + /// Marks this instance as using shared that should not be disposed. + /// + public static ShellContext WithSharedSettings(this ShellContext context) + { + context.SharedSettings = true; + return context; + } } diff --git a/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellContextFactoryExtensions.cs b/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellContextFactoryExtensions.cs index 1a37aa343c8..41285b75999 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellContextFactoryExtensions.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellContextFactoryExtensions.cs @@ -9,26 +9,44 @@ namespace OrchardCore.Environment.Shell; public static class ShellContextFactoryExtensions { /// - /// Creates a maximum shell context composed of all installed features. + /// Creates a maximum shell context composed of all installed features, and + /// marked by default as using shared settings that should not be disposed. /// - public static async Task CreateMaximumContextAsync(this IShellContextFactory shellContextFactory, ShellSettings shellSettings) + public static async Task CreateMaximumContextAsync( + this IShellContextFactory shellContextFactory, ShellSettings shellSettings, bool sharedSettings = true) { var shellDescriptor = await shellContextFactory.GetShellDescriptorAsync(shellSettings); if (shellDescriptor is null) { - return await shellContextFactory.CreateMinimumContextAsync(shellSettings); + return await shellContextFactory.CreateMinimumContextAsync(shellSettings, sharedSettings); } shellDescriptor = new ShellDescriptor { Features = shellDescriptor.Installed }; - return await shellContextFactory.CreateDescribedContextAsync(shellSettings, shellDescriptor); + var context = await shellContextFactory.CreateDescribedContextAsync(shellSettings, shellDescriptor); + if (sharedSettings) + { + context.WithSharedSettings(); + } + + return context; } /// - /// Creates a minimum shell context without any feature. + /// Creates a minimum shell context without any feature, and marked + /// by default as using shared settings that should not be disposed. /// - public static Task CreateMinimumContextAsync(this IShellContextFactory shellContextFactory, ShellSettings shellSettings) => - shellContextFactory.CreateDescribedContextAsync(shellSettings, new ShellDescriptor()); + public static async Task CreateMinimumContextAsync( + this IShellContextFactory shellContextFactory, ShellSettings shellSettings, bool sharedSettings = true) + { + var context = await shellContextFactory.CreateDescribedContextAsync(shellSettings, new ShellDescriptor()); + if (sharedSettings) + { + context.WithSharedSettings(); + } + + return context; + } /// /// Gets the shell descriptor from the store. diff --git a/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellHostExtensions.cs b/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellHostExtensions.cs index cfd15f4453d..b300b277e62 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellHostExtensions.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellHostExtensions.cs @@ -1,11 +1,27 @@ using System; using System.Threading.Tasks; using OrchardCore.Environment.Shell.Scope; +using static System.Formats.Asn1.AsnWriter; namespace OrchardCore.Environment.Shell { public static class ShellHostExtensions { + /// + /// Tries to create a standalone service scope that can be used to resolve local services. + /// + public static async Task<(ShellScope scope, bool success)> TryGetScopeAsync(this IShellHost shellHost, string tenant) + { + try + { + return (await shellHost.GetScopeAsync(shellHost.GetSettings(tenant)), true); + } + catch + { + return (null, false); + } + } + /// /// Creates a standalone service scope that can be used to resolve local services. /// diff --git a/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellSettingsExtensions.cs b/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellSettingsExtensions.cs index 52b437ac98d..edd08480ba6 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellSettingsExtensions.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Shell/Extensions/ShellSettingsExtensions.cs @@ -40,6 +40,11 @@ public static class ShellSettingsExtensions /// public static bool IsRemovable(this ShellSettings settings) => settings.IsUninitialized() || settings.IsDisabled(); + /// + /// Whether or not the tenant configuration has not been disposed. + /// + public static bool HasConfiguration(this ShellSettings settings) => settings is { Disposed: false }; + /// /// Whether or not the tenant has the provided url prefix. /// @@ -136,4 +141,13 @@ public static ShellSettings AsDisabled(this ShellSettings settings) settings.State = TenantState.Disabled; return settings; } + + /// + /// Marks the tenant settings to be disposable. + /// + public static ShellSettings AsDisposable(this ShellSettings settings) + { + settings.Disposable = true; + return settings; + } } diff --git a/src/OrchardCore/OrchardCore.Abstractions/Shell/ShellSettings.cs b/src/OrchardCore/OrchardCore.Abstractions/Shell/ShellSettings.cs index 8c15df79b80..092e3ba4ca5 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Shell/ShellSettings.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Shell/ShellSettings.cs @@ -13,7 +13,7 @@ namespace OrchardCore.Environment.Shell /// by regular configuration sources, then by default settings of all tenants are stored in 'App_Data/tenants.json', /// while each tenant configuration is stored in the related site folder 'App_Data/Sites/{tenant}/appsettings.json'. /// - public class ShellSettings + public class ShellSettings : IDisposable { /// /// The name of the 'Default' tenant. @@ -27,6 +27,8 @@ public class ShellSettings private readonly ShellConfiguration _settings; private readonly ShellConfiguration _configuration; + internal volatile int _shellCreating; + private bool _disposed; /// /// Initializes a new . @@ -62,6 +64,18 @@ public ShellSettings(ShellSettings settings) /// public string Name { get; set; } + /// + /// Whether this instance has been disposed or not. + /// + [JsonIgnore] + public bool Disposed => _disposed; + + /// + /// Whether this instance is disposable or not. + /// + [JsonIgnore] + public bool Disposable { get; internal set; } + /// /// The tenant version identifier. /// @@ -133,5 +147,32 @@ public string this[string key] /// Ensures that the tenant configuration is initialized. /// public Task EnsureConfigurationAsync() => _configuration.EnsureConfigurationAsync(); + + public void Dispose() + { + // Disposable on reloading or if never registered. + if (!Disposable || _shellCreating > 0) + { + return; + } + + Close(); + GC.SuppressFinalize(this); + } + + private void Close() + { + if (_disposed) + { + return; + } + + _disposed = true; + + _settings?.Release(); + _configuration?.Release(); + } + + ~ShellSettings() => Close(); } } diff --git a/src/OrchardCore/OrchardCore.DisplayManagement.Liquid/LiquidViewsFeatureProvider.cs b/src/OrchardCore/OrchardCore.DisplayManagement.Liquid/LiquidViewsFeatureProvider.cs index a33f06568bf..3c00f40f6ae 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement.Liquid/LiquidViewsFeatureProvider.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement.Liquid/LiquidViewsFeatureProvider.cs @@ -5,8 +5,8 @@ using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Razor.Hosting; using Microsoft.Extensions.Options; -using OrchardCore.DisplayManagement.Razor; using OrchardCore.Modules; using OrchardCore.Mvc.FileProviders; @@ -48,7 +48,7 @@ public void PopulateFeature(IEnumerable parts, ViewsFeature fea feature.ViewDescriptors.Add(new CompiledViewDescriptor { RelativePath = DefaultRazorViewPath, - Item = new RazorViewCompiledItem(typeof(LiquidPage), @"mvc.1.0.view", DefaultLiquidViewPath) + Item = new TenantRazorCompiledItem(typeof(LiquidPage), DefaultLiquidViewPath) }); foreach (var path in _sharedPaths) @@ -59,7 +59,7 @@ public void PopulateFeature(IEnumerable parts, ViewsFeature fea feature.ViewDescriptors.Add(new CompiledViewDescriptor { RelativePath = viewPath, - Item = new RazorViewCompiledItem(typeof(LiquidPage), @"mvc.1.0.view", viewPath) + Item = new TenantRazorCompiledItem(typeof(LiquidPage), viewPath) }); } } diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Razor/RazorViewCompiledItem.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Razor/RazorViewCompiledItem.cs deleted file mode 100644 index 14921f14581..00000000000 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Razor/RazorViewCompiledItem.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.AspNetCore.Razor.Hosting; - -namespace OrchardCore.DisplayManagement.Razor -{ - public class RazorViewCompiledItem : RazorCompiledItem - { - public RazorViewCompiledItem(Type type, string kind, string identifier, object[] metadata = null) - { - Type = type; - Kind = kind; - Identifier = identifier; - Metadata = metadata ?? Array.Empty(); - } - - public override string Identifier { get; } - - public override string Kind { get; } - - public override IReadOnlyList Metadata { get; } - - public override Type Type { get; } - } -} diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Theming/ThemingViewsFeatureProvider.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Theming/ThemingViewsFeatureProvider.cs index cdb26100ccb..5738daffee4 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Theming/ThemingViewsFeatureProvider.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Theming/ThemingViewsFeatureProvider.cs @@ -3,8 +3,8 @@ using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Razor.Hosting; using Microsoft.Extensions.Primitives; -using OrchardCore.DisplayManagement.Razor; namespace OrchardCore.DisplayManagement.Theming { @@ -25,14 +25,14 @@ public void PopulateFeature(IEnumerable parts, ViewsFeature fea { ExpirationTokens = Array.Empty(), RelativePath = "/_ViewStart" + RazorViewEngine.ViewExtension, - Item = new RazorViewCompiledItem(typeof(ThemeViewStart), @"mvc.1.0.view", "/_ViewStart") + Item = new TenantRazorCompiledItem(typeof(ThemeViewStart), "/_ViewStart") }); feature.ViewDescriptors.Add(new CompiledViewDescriptor() { ExpirationTokens = Array.Empty(), RelativePath = '/' + ThemeLayoutFileName, - Item = new RazorViewCompiledItem(typeof(ThemeLayout), @"mvc.1.0.view", '/' + ThemeLayoutFileName) + Item = new TenantRazorCompiledItem(typeof(ThemeLayout), '/' + ThemeLayoutFileName) }); } } diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Extensions/DatabaseShellContextFactoryExtensions.cs b/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Extensions/DatabaseShellContextFactoryExtensions.cs index e495433031d..3887e63607a 100644 --- a/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Extensions/DatabaseShellContextFactoryExtensions.cs +++ b/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Extensions/DatabaseShellContextFactoryExtensions.cs @@ -9,16 +9,17 @@ namespace OrchardCore.Shells.Database.Extensions { public static class DatabaseShellContextFactoryExtensions { - internal static Task GetDatabaseContextAsync(this IShellContextFactory shellContextFactory, DatabaseShellsStorageOptions options) + internal static Task GetDatabaseContextAsync( + this IShellContextFactory shellContextFactory, DatabaseShellsStorageOptions options) { - if (options.DatabaseProvider == null) + if (options.DatabaseProvider is null) { - throw new ArgumentNullException(nameof(options.DatabaseProvider), - "The 'OrchardCore.Shells.Database' configuration section should define a 'DatabaseProvider'"); + throw new InvalidOperationException("The 'OrchardCore.Shells.Database' configuration section should define a 'DatabaseProvider'"); } var settings = new ShellSettings() .AsDefaultShell() + .AsDisposable() .AsRunning(); settings["DatabaseProvider"] = options.DatabaseProvider; diff --git a/src/OrchardCore/OrchardCore.Mvc.Core/Overrides/Razor/TenantCompiledRazorAssemblyPart.cs b/src/OrchardCore/OrchardCore.Mvc.Core/Overrides/Razor/TenantCompiledRazorAssemblyPart.cs new file mode 100644 index 00000000000..e70869cc50d --- /dev/null +++ b/src/OrchardCore/OrchardCore.Mvc.Core/Overrides/Razor/TenantCompiledRazorAssemblyPart.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.AspNetCore.Razor.Hosting; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts; + +/// +/// An for compiled Razor assemblies. +/// +public class TenantCompiledRazorAssemblyPart : ApplicationPart, IRazorCompiledItemProvider +{ + /// + /// Initializes a new instance of . + /// + /// The The assembly. + public TenantCompiledRazorAssemblyPart(Assembly assembly) + { + Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); + } + + /// + /// Gets the . + /// + public Assembly Assembly { get; } + + /// + public override string Name => Assembly.GetName().Name!; + + IEnumerable IRazorCompiledItemProvider.CompiledItems + { + get + { + var loader = new TenantRazorCompiledItemLoader(); + return loader.LoadItems(Assembly); + } + } +} diff --git a/src/OrchardCore/OrchardCore.Mvc.Core/Overrides/Razor/TenantRazorCompiledItem.cs b/src/OrchardCore/OrchardCore.Mvc.Core/Overrides/Razor/TenantRazorCompiledItem.cs new file mode 100644 index 00000000000..618240a0e79 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Mvc.Core/Overrides/Razor/TenantRazorCompiledItem.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; + +namespace Microsoft.AspNetCore.Razor.Hosting; + +public class TenantRazorCompiledItem : RazorCompiledItem +{ + private object[] _metadata; + + public TenantRazorCompiledItem(Type type, string kind, string identifier) + { + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(kind); + ArgumentNullException.ThrowIfNull(identifier); + + Type = type; + Kind = kind; + Identifier = identifier; + } + + public TenantRazorCompiledItem(Type type, string identifier) + { + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(identifier); + + Type = type; + Kind = MvcViewDocumentClassifierPass.MvcViewDocumentKind; + Identifier = identifier; + } + + public override string Identifier { get; } + + public override string Kind { get; } + + public override IReadOnlyList Metadata => _metadata ??= Type.GetCustomAttributes(inherit: true); + + public override Type Type { get; } +} diff --git a/src/OrchardCore/OrchardCore.Mvc.Core/Overrides/Razor/TenantRazorCompiledItemLoader.cs b/src/OrchardCore/OrchardCore.Mvc.Core/Overrides/Razor/TenantRazorCompiledItemLoader.cs new file mode 100644 index 00000000000..1ac9997eb54 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Mvc.Core/Overrides/Razor/TenantRazorCompiledItemLoader.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; + +namespace Microsoft.AspNetCore.Razor.Hosting; + +/// +/// A loader implementation that can load objects from an +/// using reflection. +/// +/// +/// +/// Inherit from to customize the behavior when loading +/// objects from an . The default implementations of methods +/// defined by this class use reflection in a trivial way to load attributes from the assembly. +/// +/// +/// Inheriting from is useful when an implementation needs to consider +/// additional configuration or data outside of the being loaded. +/// +/// +/// Subclasses of can return subclasses of +/// with additional data members by overriding . +/// +/// +public class TenantRazorCompiledItemLoader +{ + /// + /// Loads a list of objects from the provided . + /// + /// The assembly to search. + /// A list of objects. + public virtual IReadOnlyList LoadItems(Assembly assembly) + { + ArgumentNullException.ThrowIfNull(assembly); + + var items = new List(); + foreach (var attribute in LoadAttributes(assembly)) + { + items.Add(CreateItem(attribute)); + } + + return items; + } + + /// + /// Creates a from a . + /// + /// The . + /// A created from . + protected virtual RazorCompiledItem CreateItem(RazorCompiledItemAttribute attribute) + { + ArgumentNullException.ThrowIfNull(attribute); + + if (attribute.Kind == MvcViewDocumentClassifierPass.MvcViewDocumentKind) + { + return new TenantRazorCompiledItem(attribute.Type, attribute.Identifier); + } + + return new TenantRazorCompiledItem(attribute.Type, attribute.Kind, attribute.Identifier); + } + + /// + /// Retrieves the list of attributes defined for the provided + /// . + /// + /// The to search. + /// A list of attributes. + protected static IEnumerable LoadAttributes(Assembly assembly) + { + ArgumentNullException.ThrowIfNull(assembly); + + return assembly.GetCustomAttributes(); + } +} diff --git a/src/OrchardCore/OrchardCore.Mvc.Core/SharedViewCompilerProvider.cs b/src/OrchardCore/OrchardCore.Mvc.Core/SharedViewCompilerProvider.cs index 259112c2975..77e407fe8b8 100644 --- a/src/OrchardCore/OrchardCore.Mvc.Core/SharedViewCompilerProvider.cs +++ b/src/OrchardCore/OrchardCore.Mvc.Core/SharedViewCompilerProvider.cs @@ -21,13 +21,18 @@ public SharedViewCompilerProvider(IServiceProvider services) public IViewCompiler GetCompiler() { - if (_compiler != null) + if (_compiler is not null) { return _compiler; } lock (_synLock) { + if (_compiler is not null) + { + return _compiler; + } + _compiler = _services .GetServices() .FirstOrDefault() diff --git a/src/OrchardCore/OrchardCore.Mvc.Core/ShellViewFeatureProvider.cs b/src/OrchardCore/OrchardCore.Mvc.Core/ShellViewFeatureProvider.cs index 6cddde3286d..857ec1e46dd 100644 --- a/src/OrchardCore/OrchardCore.Mvc.Core/ShellViewFeatureProvider.cs +++ b/src/OrchardCore/OrchardCore.Mvc.Core/ShellViewFeatureProvider.cs @@ -31,6 +31,21 @@ public void PopulateFeature(IEnumerable parts, ViewsFeature fea { EnsureScopedServices(); + // Check if the feature can be retrieved from the shell scope. + var viewsFeature = ShellScope.GetFeature(); + if (viewsFeature is not null) + { + foreach (var descriptor in viewsFeature.ViewDescriptors) + { + feature.ViewDescriptors.Add(descriptor); + } + + return; + } + + // Set it as a shell scope feature to be used later on. + ShellScope.SetFeature(feature); + PopulateFeatureInternal(feature); // Apply views feature providers registered at the tenant level. @@ -123,7 +138,7 @@ private void PopulateFeatureInternal(ViewsFeature feature) foreach (var assembly in assembliesWithViews) { - var applicationPart = new ApplicationPart[] { new CompiledRazorAssemblyPart(assembly) }; + var applicationPart = new ApplicationPart[] { new TenantCompiledRazorAssemblyPart(assembly) }; foreach (var provider in mvcFeatureProviders) { diff --git a/src/OrchardCore/OrchardCore.Mvc.Core/Startup.cs b/src/OrchardCore/OrchardCore.Mvc.Core/Startup.cs index 5aef467f1a7..9d11747fa13 100644 --- a/src/OrchardCore/OrchardCore.Mvc.Core/Startup.cs +++ b/src/OrchardCore/OrchardCore.Mvc.Core/Startup.cs @@ -108,11 +108,12 @@ public override void ConfigureServices(IServiceCollection services) if (_hostingEnvironment.IsDevelopment() && refsFolderExists) { builder.AddRazorRuntimeCompilation(); - - // Shares across tenants the same compiler and its 'IMemoryCache' instance. - services.AddSingleton(); } + // Share across tenants a static compiler even if there is no runtime compilation + // because the compiler still uses its internal cache to retrieve compiled items. + services.AddSingleton(); + services.TryAddEnumerable( ServiceDescriptor.Transient, RazorCompilationOptionsSetup>()); diff --git a/src/OrchardCore/OrchardCore.ReCaptcha.Core/ServiceCollectionExtensions.cs b/src/OrchardCore/OrchardCore.ReCaptcha.Core/ServiceCollectionExtensions.cs index 803c0361494..7167a21f82a 100644 --- a/src/OrchardCore/OrchardCore.ReCaptcha.Core/ServiceCollectionExtensions.cs +++ b/src/OrchardCore/OrchardCore.ReCaptcha.Core/ServiceCollectionExtensions.cs @@ -16,9 +16,9 @@ public static IServiceCollection AddReCaptcha(this IServiceCollection services, services.AddHttpClient() .AddTransientHttpErrorPolicy(policy => policy.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(0.5 * attempt))); - services.AddTransient(); + services.AddSingleton(); services.AddTransient, ReCaptchaSettingsConfiguration>(); - services.AddTransient(); + services.AddSingleton(); services.AddTagHelpers(); if (configure != null) diff --git a/src/OrchardCore/OrchardCore.ReCaptcha.Core/Services/ReCaptchaService.cs b/src/OrchardCore/OrchardCore.ReCaptcha.Core/Services/ReCaptchaService.cs index dba626685b9..c1c01667664 100644 --- a/src/OrchardCore/OrchardCore.ReCaptcha.Core/Services/ReCaptchaService.cs +++ b/src/OrchardCore/OrchardCore.ReCaptcha.Core/Services/ReCaptchaService.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -14,16 +15,19 @@ namespace OrchardCore.ReCaptcha.Services { public class ReCaptchaService { - private readonly ReCaptchaClient _reCaptchaClient; private readonly ReCaptchaSettings _settings; private readonly IEnumerable _robotDetectors; private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; protected readonly IStringLocalizer S; - public ReCaptchaService(ReCaptchaClient reCaptchaClient, IOptions optionsAccessor, IEnumerable robotDetectors, IHttpContextAccessor httpContextAccessor, ILogger logger, IStringLocalizer stringLocalizer) + public ReCaptchaService( + IOptions optionsAccessor, + IEnumerable robotDetectors, + IHttpContextAccessor httpContextAccessor, + ILogger logger, + IStringLocalizer stringLocalizer) { - _reCaptchaClient = reCaptchaClient; _settings = optionsAccessor.Value; _robotDetectors = robotDetectors; _httpContextAccessor = httpContextAccessor; @@ -63,9 +67,15 @@ public void ThisIsAHuman() /// /// /// - public async Task VerifyCaptchaResponseAsync(string reCaptchaResponse) + public Task VerifyCaptchaResponseAsync(string reCaptchaResponse) { - return !string.IsNullOrWhiteSpace(reCaptchaResponse) && await _reCaptchaClient.VerifyAsync(reCaptchaResponse, _settings.SecretKey); + if (string.IsNullOrWhiteSpace(reCaptchaResponse)) + { + return Task.FromResult(false); + } + + var reCaptchaClient = _httpContextAccessor.HttpContext.RequestServices.GetRequiredService(); + return reCaptchaClient.VerifyAsync(reCaptchaResponse, _settings.SecretKey); } /// diff --git a/src/OrchardCore/OrchardCore.Setup.Core/SetupService.cs b/src/OrchardCore/OrchardCore.Setup.Core/SetupService.cs index 8b530db5e37..8146b395d25 100644 --- a/src/OrchardCore/OrchardCore.Setup.Core/SetupService.cs +++ b/src/OrchardCore/OrchardCore.Setup.Core/SetupService.cs @@ -245,10 +245,9 @@ await scope.ServiceProvider.GetService() // Update the shell state. await _shellHost.UpdateShellSettingsAsync(shellSettings.AsRunning()); - await (await _shellHost.GetScopeAsync(shellSettings)).UsingAsync(async scope => + await (await _shellHost.GetScopeAsync(shellSettings.Name)).UsingAsync(async scope => { var handlers = scope.ServiceProvider.GetServices(); - await handlers.InvokeAsync((handler) => handler.SucceededAsync(), _logger); }); diff --git a/src/OrchardCore/OrchardCore/Modules/Extensions/ServiceCollectionExtensions.cs b/src/OrchardCore/OrchardCore/Modules/Extensions/ServiceCollectionExtensions.cs index 0175ae0f60c..94c6cb64934 100644 --- a/src/OrchardCore/OrchardCore/Modules/Extensions/ServiceCollectionExtensions.cs +++ b/src/OrchardCore/OrchardCore/Modules/Extensions/ServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; @@ -145,7 +146,7 @@ private static void AddDefaultServices(OrchardCoreBuilder builder) services.AddSingleton(); services.AddScoped(); - services.AddScoped(); + services.AddSingleton(); builder.ConfigureServices((services, serviceProvider) => { @@ -331,6 +332,11 @@ sd is ClonedSingletonDescriptor && { collection.Remove(descriptor); } + + // Make the http client factory 'IDisposable'. + collection.AddSingleton(); + collection.AddSingleton(sp => sp.GetRequiredService()); + collection.AddSingleton(sp => sp.GetRequiredService()); }, order: int.MinValue + 100); } diff --git a/src/OrchardCore/OrchardCore/Modules/ModularBackgroundService.cs b/src/OrchardCore/OrchardCore/Modules/ModularBackgroundService.cs index 4a626e6a8a5..62714126e1f 100644 --- a/src/OrchardCore/OrchardCore/Modules/ModularBackgroundService.cs +++ b/src/OrchardCore/OrchardCore/Modules/ModularBackgroundService.cs @@ -13,7 +13,7 @@ using Microsoft.Extensions.Primitives; using OrchardCore.BackgroundTasks; using OrchardCore.Environment.Shell; -using OrchardCore.Environment.Shell.Builders; +using OrchardCore.Locking; using OrchardCore.Locking.Distributed; using OrchardCore.Settings; @@ -79,7 +79,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } } - var previousShells = Array.Empty(); + var previousShells = Array.Empty<(string Tenant, long UtcTicks)>(); while (!stoppingToken.IsCancellationRequested) { // Init the delay first to be also waited on exception. @@ -100,11 +100,15 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } } - private async Task RunAsync(IEnumerable runningShells, CancellationToken stoppingToken) + private async Task RunAsync(IEnumerable<(string Tenant, long UtcTicks)> runningShells, CancellationToken stoppingToken) { - await GetShellsToRun(runningShells).ForEachAsync(async shell => + await GetShellsToRun(runningShells).ForEachAsync(async tenant => { - var tenant = shell.Settings.Name; + // Check if the shell is still registered and running. + if (!_shellHost.TryGetShellContext(tenant, out var shell) || !shell.Settings.IsRunning()) + { + return; + } // Create a new 'HttpContext' to be used in the background. _httpContextAccessor.HttpContext = shell.CreateHttpContext(); @@ -117,20 +121,39 @@ await GetShellsToRun(runningShells).ForEachAsync(async shell => break; } - var shellScope = await _shellHost.GetScopeAsync(shell.Settings); - if (!_options.ShellWarmup && !shellScope.ShellContext.HasPipeline()) + // Try to create a shell scope on this shell context. + var (shellScope, success) = await _shellHost.TryGetScopeAsync(shell.Settings.Name); + if (!success) { break; } - var distributedLock = shellScope.ShellContext.ServiceProvider.GetRequiredService(); + // Check if the shell has no pipeline and should not be warmed up. + if (!_options.ShellWarmup && !shellScope.ShellContext.HasPipeline()) + { + await shellScope.TerminateShellAsync(); + break; + } - // Try to acquire a lock before using the scope, so that a next process gets the last committed data. - (var locker, var locked) = await distributedLock.TryAcquireBackgroundTaskLockAsync(scheduler.Settings); - if (!locked) + var locked = false; + ILocker locker = null; + try { - _logger.LogInformation("Timeout to acquire a lock on background task '{TaskName}' on tenant '{TenantName}'.", scheduler.Name, tenant); - return; + // Try to acquire a lock before using the scope, so that a next process gets the last committed data. + var distributedLock = shellScope.ShellContext.ServiceProvider.GetRequiredService(); + (locker, locked) = await distributedLock.TryAcquireBackgroundTaskLockAsync(scheduler.Settings); + if (!locked) + { + await shellScope.TerminateShellAsync(); + _logger.LogInformation("Timeout to acquire a lock on background task '{TaskName}' on tenant '{TenantName}'.", scheduler.Name, tenant); + break; + } + } + catch (Exception ex) when (!ex.IsFatal()) + { + await shellScope.TerminateShellAsync(); + _logger.LogError(ex, "Failed to acquire a lock on background task '{TaskName}' on tenant '{TenantName}'.", scheduler.Name, tenant); + break; } await using var acquiredLock = locker; @@ -209,28 +232,42 @@ await shellScope.UsingAsync(async scope => }); } - private async Task UpdateAsync(ShellContext[] previousShells, ShellContext[] runningShells, CancellationToken stoppingToken) + private async Task UpdateAsync( + (string Tenant, long UtcTicks)[] previousShells, + (string Tenant, long UtcTicks)[] runningShells, CancellationToken stoppingToken) { var referenceTime = DateTime.UtcNow; - await GetShellsToUpdate(previousShells, runningShells).ForEachAsync(async shell => + await GetShellsToUpdate(previousShells, runningShells).ForEachAsync(async tenant => { - var tenant = shell.Settings.Name; - if (stoppingToken.IsCancellationRequested) { return; } - // Create a new 'HttpContext' to be used in the background. - _httpContextAccessor.HttpContext = shell.CreateHttpContext(); + // Check if the shell is still registered and running. + if (!_shellHost.TryGetShellContext(tenant, out var shell) || !shell.Settings.IsRunning()) + { + return; + } - var shellScope = await _shellHost.GetScopeAsync(shell.Settings); + // Try to create a shell scope on this shell context. + var (shellScope, success) = await _shellHost.TryGetScopeAsync(shell.Settings.Name); + if (!success) + { + return; + } + + // Check if the shell has no pipeline and should not be warmed up. if (!_options.ShellWarmup && !shellScope.ShellContext.HasPipeline()) { + await shellScope.TerminateShellAsync(); return; } + // Create a new 'HttpContext' to be used in the background. + _httpContextAccessor.HttpContext = shell.CreateHttpContext(); + await shellScope.UsingAsync(async scope => { var tasks = scope.ServiceProvider.GetServices(); @@ -316,45 +353,64 @@ private static async Task WaitAsync(Task pollingDelay, CancellationToken stoppin } } - private ShellContext[] GetRunningShells() => _shellHost + private (string Tenant, long UtcTicks)[] GetRunningShells() => _shellHost .ListShellContexts() - .Where(s => s.Settings.IsRunning() && (_options.ShellWarmup || s.HasPipeline())) + .Where(shell => shell.Settings.IsRunning() && (_options.ShellWarmup || shell.HasPipeline())) + .Select(shell => (shell.Settings.Name, shell.UtcTicks)) .ToArray(); - private ShellContext[] GetShellsToRun(IEnumerable shells) + private string[] GetShellsToRun(IEnumerable<(string Tenant, long UtcTicks)> shells) { var tenantsToRun = _schedulers - .Where(s => s.Value.CanRun()) - .Select(s => s.Value.Tenant) + .Where(scheduler => scheduler.Value.CanRun()) + .Select(scheduler => scheduler.Value.Tenant) .Distinct() .ToArray(); - return shells.Where(s => tenantsToRun.Contains(s.Settings.Name)).ToArray(); + return shells + .Select(shell => shell.Tenant) + .Where(tenant => tenantsToRun.Contains(tenant)) + .ToArray(); } - private ShellContext[] GetShellsToUpdate(ShellContext[] previousShells, ShellContext[] runningShells) + private string[] GetShellsToUpdate((string Tenant, long UtcTicks)[] previousShells, (string Tenant, long UtcTicks)[] runningShells) { - var released = previousShells.Where(s => s.Released).Select(s => s.Settings.Name).ToArray(); - if (released.Length > 0) + var previousTenants = previousShells.Select(shell => shell.Tenant); + + var releasedTenants = new List(); + foreach (var (tenant, utcTicks) in previousShells) + { + if (_shellHost.TryGetShellContext(tenant, out var existing) && + existing.UtcTicks == utcTicks && + !existing.Released) + { + continue; + } + + releasedTenants.Add(tenant); + } + + if (releasedTenants.Count > 0) { - UpdateSchedulers(released, s => s.Released = true); + UpdateSchedulers(releasedTenants.ToArray(), scheduler => scheduler.Released = true); } - var changed = _changeTokens.Where(t => t.Value.HasChanged).Select(t => t.Key).ToArray(); - if (changed.Length > 0) + var changedTenants = _changeTokens.Where(token => token.Value.HasChanged).Select(token => token.Key).ToArray(); + if (changedTenants.Length > 0) { - UpdateSchedulers(changed, s => s.Updated = false); + UpdateSchedulers(changedTenants, scheduler => scheduler.Updated = false); } - var valid = previousShells.Select(s => s.Settings.Name).Except(released).Except(changed); - var tenantsToUpdate = runningShells.Select(s => s.Settings.Name).Except(valid).ToArray(); + var runningTenants = runningShells.Select(shell => shell.Tenant); + var validTenants = previousTenants.Except(releasedTenants).Except(changedTenants); + var tenantsToUpdate = runningTenants.Except(validTenants).ToArray(); - return runningShells.Where(s => tenantsToUpdate.Contains(s.Settings.Name)).ToArray(); + return runningTenants.Where(tenant => tenantsToUpdate.Contains(tenant)).ToArray(); } private BackgroundTaskScheduler[] GetSchedulersToRun(string tenant) => _schedulers - .Where(s => s.Value.Tenant == tenant && s.Value.CanRun()) - .Select(s => s.Value) + .Where(scheduler => scheduler.Value.Tenant == tenant && scheduler.Value.CanRun()) + .Select(scheduler => scheduler.Value) .ToArray(); private void UpdateSchedulers(string[] tenants, Action action) diff --git a/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/JsonConfigurationFileParser.cs b/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/JsonConfigurationFileParser.cs new file mode 100644 index 00000000000..770fcb255cc --- /dev/null +++ b/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/JsonConfigurationFileParser.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text.Json; + +#nullable enable + +namespace Microsoft.Extensions.Configuration.Json +{ + internal sealed class JsonConfigurationFileParser + { + private JsonConfigurationFileParser() { } + + private readonly Dictionary _data = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Stack _paths = new Stack(); + + public static IDictionary Parse(Stream input) + => new JsonConfigurationFileParser().ParseStream(input); + + private Dictionary ParseStream(Stream input) + { + var jsonDocumentOptions = new JsonDocumentOptions + { + CommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + }; + + using (var reader = new StreamReader(input)) + using (JsonDocument doc = JsonDocument.Parse(reader.ReadToEnd(), jsonDocumentOptions)) + { + if (doc.RootElement.ValueKind != JsonValueKind.Object) + { + // throw new FormatException(SR.Format(SR.Error_InvalidTopLevelJSONElement, doc.RootElement.ValueKind)); + throw new FormatException($"Top-level JSON element must be an object. Instead, '{doc.RootElement.ValueKind}' was found."); + } + VisitObjectElement(doc.RootElement); + } + + return _data; + } + + private void VisitObjectElement(JsonElement element) + { + var isEmpty = true; + + foreach (JsonProperty property in element.EnumerateObject()) + { + isEmpty = false; + EnterContext(property.Name); + VisitValue(property.Value); + ExitContext(); + } + + SetNullIfElementIsEmpty(isEmpty); + } + + private void VisitArrayElement(JsonElement element) + { + int index = 0; + + foreach (JsonElement arrayElement in element.EnumerateArray()) + { + EnterContext(index.ToString()); + VisitValue(arrayElement); + ExitContext(); + index++; + } + + SetNullIfElementIsEmpty(isEmpty: index == 0); + } + + private void SetNullIfElementIsEmpty(bool isEmpty) + { + if (isEmpty && _paths.Count > 0) + { + _data[_paths.Peek()] = null; + } + } + + private void VisitValue(JsonElement value) + { + Debug.Assert(_paths.Count > 0); + + switch (value.ValueKind) + { + case JsonValueKind.Object: + VisitObjectElement(value); + break; + + case JsonValueKind.Array: + VisitArrayElement(value); + break; + + case JsonValueKind.Number: + case JsonValueKind.String: + case JsonValueKind.True: + case JsonValueKind.False: + case JsonValueKind.Null: + string key = _paths.Peek(); + if (_data.ContainsKey(key)) + { + // throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key)); + throw new FormatException($"A duplicate key '{key}' was found."); + } + _data[key] = value.ToString(); + break; + + default: + // throw new FormatException(SR.Format(SR.Error_UnsupportedJSONToken, value.ValueKind)); + throw new FormatException($"Unsupported JSON token '{value.ValueKind}' was found."); + } + } + + private void EnterContext(string context) => + _paths.Push(_paths.Count > 0 ? + _paths.Peek() + ConfigurationPath.KeyDelimiter + context : + context); + + private void ExitContext() => _paths.Pop(); + } +} diff --git a/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationExtensions.cs b/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationExtensions.cs new file mode 100644 index 00000000000..ba63c908549 --- /dev/null +++ b/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationExtensions.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using Microsoft.Extensions.Configuration.Json; +using Microsoft.Extensions.FileProviders; + +#nullable enable + +namespace Microsoft.Extensions.Configuration +{ + /// + /// Extension methods for adding . + /// + public static class TenantJsonConfigurationExtensions + { + /// + /// Adds the JSON configuration provider at to . + /// + /// The to add to. + /// Path relative to the base path stored in + /// of . + /// The . + public static IConfigurationBuilder AddTenantJsonFile(this IConfigurationBuilder builder, string path) + { + return AddTenantJsonFile(builder, provider: null, path: path, optional: false, reloadOnChange: false); + } + + /// + /// Adds the JSON configuration provider at to . + /// + /// The to add to. + /// Path relative to the base path stored in + /// of . + /// Whether the file is optional. + /// The . + public static IConfigurationBuilder AddTenantJsonFile(this IConfigurationBuilder builder, string path, bool optional) + { + return AddTenantJsonFile(builder, provider: null, path: path, optional: optional, reloadOnChange: false); + } + + /// + /// Adds the JSON configuration provider at to . + /// + /// The to add to. + /// Path relative to the base path stored in + /// of . + /// Whether the file is optional. + /// Whether the configuration should be reloaded if the file changes. + /// The . + public static IConfigurationBuilder AddTenantJsonFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange) + { + return AddTenantJsonFile(builder, provider: null, path: path, optional: optional, reloadOnChange: reloadOnChange); + } + + /// + /// Adds a JSON configuration source to . + /// + /// The to add to. + /// The to use to access the file. + /// Path relative to the base path stored in + /// of . + /// Whether the file is optional. + /// Whether the configuration should be reloaded if the file changes. + /// The . + public static IConfigurationBuilder AddTenantJsonFile(this IConfigurationBuilder builder, IFileProvider? provider, string path, bool optional, bool reloadOnChange) + { + // ThrowHelper.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(builder); + + if (string.IsNullOrEmpty(path)) + { + // throw new ArgumentException(SR.Error_InvalidFilePath, nameof(path)); + throw new ArgumentException($"File path must be a non-empty string.", nameof(path)); + } + + return builder.AddTenantJsonFile(s => + { + s.FileProvider = provider; + s.Path = path; + s.Optional = optional; + s.ReloadOnChange = reloadOnChange; + s.ResolveFileProvider(); + }); + } + + /// + /// Adds a JSON configuration source to . + /// + /// The to add to. + /// Configures the source. + /// The . + public static IConfigurationBuilder AddTenantJsonFile(this IConfigurationBuilder builder, Action? configureSource) + => builder.Add(configureSource); + } +} diff --git a/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationProvider.cs b/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationProvider.cs new file mode 100644 index 00000000000..427f05b29da --- /dev/null +++ b/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationProvider.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace Microsoft.Extensions.Configuration.Json +{ + /// + /// A JSON file based . + /// + public class TenantJsonConfigurationProvider : FileConfigurationProvider + { + /// + /// Initializes a new instance with the specified source. + /// + /// The source settings. + public TenantJsonConfigurationProvider(TenantJsonConfigurationSource source) : base(source) { } + + /// + /// Loads the JSON data from a stream. + /// + /// The stream to read. + public override void Load(Stream stream) + { + try + { + Data = JsonConfigurationFileParser.Parse(stream); + } + catch (JsonException e) + { + // throw new FormatException(SR.Error_JSONParseError, e); + throw new FormatException("Could not parse the JSON file.", e); + } + } + + /// + /// Dispose the provider. + /// + /// true if invoked from . + protected override void Dispose(bool disposing) + { + base.Dispose(true); + + // OC: Will be part of 'FileConfigurationProvider'. + (Source.FileProvider as IDisposable)?.Dispose(); + } + } +} diff --git a/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationSource.cs b/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationSource.cs new file mode 100644 index 00000000000..25ac5bddf27 --- /dev/null +++ b/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationSource.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Configuration.Json +{ + /// + /// Represents a JSON file as an . + /// + public class TenantJsonConfigurationSource : FileConfigurationSource + { + /// + /// Builds the for this source. + /// + /// The . + /// A + public override IConfigurationProvider Build(IConfigurationBuilder builder) + { + EnsureDefaults(builder); + return new TenantJsonConfigurationProvider(this); + } + } +} diff --git a/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/ActiveHandlerTrackingEntry.cs b/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/ActiveHandlerTrackingEntry.cs new file mode 100644 index 00000000000..1a2493023d6 --- /dev/null +++ b/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/ActiveHandlerTrackingEntry.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Threading; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; + +namespace Microsoft.Extensions.Http +{ + // Thread-safety: We treat this class as immutable except for the timer. Creating a new object + // for the 'expiry' pool simplifies the threading requirements significantly. + internal sealed class ActiveHandlerTrackingEntry : IDisposable + { + private static readonly TimerCallback _timerCallback = (s) => ((ActiveHandlerTrackingEntry)s!).Timer_Tick(); + private readonly object _lock; + private bool _timerInitialized; + private Timer _timer; + private TimerCallback _callback; + + // OC: Implement IDisposable. + private bool _disposed; + + public ActiveHandlerTrackingEntry( + string name, + LifetimeTrackingHttpMessageHandler handler, + IServiceScope scope, + TimeSpan lifetime) + { + Name = name; + Handler = handler; + Scope = scope; + Lifetime = lifetime; + + _lock = new object(); + } + + public LifetimeTrackingHttpMessageHandler Handler { get; private set; } + + public TimeSpan Lifetime { get; } + + public string Name { get; } + + public IServiceScope Scope { get; set; } + + public void StartExpiryTimer(TimerCallback callback) + { + if (Lifetime == Timeout.InfiniteTimeSpan) + { + return; // never expires. + } + + if (Volatile.Read(ref _timerInitialized)) + { + return; + } + + StartExpiryTimerSlow(callback); + } + + private void StartExpiryTimerSlow(TimerCallback callback) + { + Debug.Assert(Lifetime != Timeout.InfiniteTimeSpan); + + lock (_lock) + { + if (Volatile.Read(ref _timerInitialized)) + { + return; + } + + _callback = callback; + _timer = NonCapturingTimer.Create(_timerCallback, this, Lifetime, Timeout.InfiniteTimeSpan); + _timerInitialized = true; + } + } + + private void Timer_Tick() + { + // OC: Check if disposed. + if (_disposed) + { + return; + } + + Debug.Assert(_callback != null); + Debug.Assert(_timer != null); + + lock (_lock) + { + if (_timer != null) + { + _timer.Dispose(); + _timer = null; + + _callback(this); + } + } + } + + // OC: Implement IDisposable. + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + _timer?.Dispose(); + _timer = null; + Handler?.Dispose(); + Handler = null; + Scope?.Dispose(); + Scope = null; + } + } +} diff --git a/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/ExpiredHandlerTrackingEntry.cs b/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/ExpiredHandlerTrackingEntry.cs new file mode 100644 index 00000000000..15d8724ea60 --- /dev/null +++ b/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/ExpiredHandlerTrackingEntry.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.Http +{ + // Thread-safety: This class is immutable. + internal sealed class ExpiredHandlerTrackingEntry + { + private readonly WeakReference _livenessTracker; + + // IMPORTANT: don't cache a reference to `other` or `other.Handler` here. + // We need to allow it to be collected by the GC. + public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other) + { + Name = other.Name; + Scope = other.Scope; + + _livenessTracker = new WeakReference(other.Handler); + InnerHandler = other.Handler.InnerHandler!; + } + + public bool CanDispose => !_livenessTracker.IsAlive; + + public HttpMessageHandler InnerHandler { get; set; } + + public string Name { get; } + + public IServiceScope Scope { get; set; } + } +} diff --git a/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/LifetimeTrackingHttpMessageHandler.cs b/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/LifetimeTrackingHttpMessageHandler.cs new file mode 100644 index 00000000000..68190dbc5b1 --- /dev/null +++ b/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/LifetimeTrackingHttpMessageHandler.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; + +namespace Microsoft.Extensions.Http +{ + // This is a marker used to check if the underlying handler should be disposed. HttpClients + // share a reference to an instance of this class, and when it goes out of scope the inner handler + // is eligible to be disposed. + internal sealed class LifetimeTrackingHttpMessageHandler : DelegatingHandler + { + public LifetimeTrackingHttpMessageHandler(HttpMessageHandler innerHandler) + : base(innerHandler) + { + } + + protected override void Dispose(bool disposing) + { + // The lifetime of this is tracked separately by ActiveHandlerTrackingEntry. + } + } +} diff --git a/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/NonCapturingTimer.cs b/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/NonCapturingTimer.cs new file mode 100644 index 00000000000..991657c9076 --- /dev/null +++ b/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/NonCapturingTimer.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; + +namespace Microsoft.Extensions.Internal +{ + // A convenience API for interacting with System.Threading.Timer in a way + // that doesn't capture the ExecutionContext. We should be using this (or equivalent) + // everywhere we use timers to avoid rooting any values stored in async locals. + internal static class NonCapturingTimer + { + public static Timer Create(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) + { + if (callback is null) + { + throw new ArgumentNullException(nameof(callback)); + } + + // Don't capture the current ExecutionContext and its AsyncLocals onto the timer. + var restoreFlow = false; + try + { + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + return new Timer(callback, state, dueTime, period); + } + finally + { + // Restore the current ExecutionContext. + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } + } + } + } +} diff --git a/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/TenantHttpClientFactory.cs b/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/TenantHttpClientFactory.cs new file mode 100644 index 00000000000..01631ce9bd6 --- /dev/null +++ b/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/TenantHttpClientFactory.cs @@ -0,0 +1,452 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net.Http; +using System.Threading; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +#nullable enable + +namespace Microsoft.Extensions.Http +{ + internal class TenantHttpClientFactory : IHttpClientFactory, IHttpMessageHandlerFactory, IDisposable + { + private static readonly TimerCallback _cleanupCallback = (s) => ((TenantHttpClientFactory)s!).CleanupTimer_Tick(); + private IServiceProvider? _services; + private IServiceScopeFactory? _scopeFactory; + private IOptionsMonitor? _optionsMonitor; + private IHttpMessageHandlerBuilderFilter[]? _filters; + private Func>? _entryFactory; + private readonly Lazy _logger; + + // OC: Implement IDisposable. + private bool _disposed; + + // Default time of 10s for cleanup seems reasonable. + // Quick math: + // 10 distinct named clients * expiry time >= 1s = approximate cleanup queue of 100 items + // + // This seems frequent enough. We also rely on GC occurring to actually trigger disposal. + private readonly TimeSpan DefaultCleanupInterval = TimeSpan.FromSeconds(10); + + // We use a new timer for each regular cleanup cycle, protected with a lock. Note that this scheme + // doesn't give us anything to dispose, as the timer is started/stopped as needed. + // + // There's no need for the factory itself to be disposable. If you stop using it, eventually everything will + // get reclaimed. + private Timer? _cleanupTimer; + private readonly object _cleanupTimerLock; + private readonly object _cleanupActiveLock; + + // Collection of 'active' handlers. + // + // Using lazy for synchronization to ensure that only one instance of HttpMessageHandler is created + // for each name. + // + // internal for tests + internal readonly ConcurrentDictionary> _activeHandlers; + + // Collection of 'expired' but not yet disposed handlers. + // + // Used when we're rotating handlers so that we can dispose HttpMessageHandler instances once they + // are eligible for garbage collection. + // + // internal for tests + internal readonly ConcurrentQueue _expiredHandlers; + private readonly TimerCallback _expiryCallback; + + public TenantHttpClientFactory( + IServiceProvider services, + IServiceScopeFactory scopeFactory, + IOptionsMonitor optionsMonitor, + IEnumerable filters) + { + // ThrowHelper.ThrowIfNull(services); + // ThrowHelper.ThrowIfNull(scopeFactory); + // ThrowHelper.ThrowIfNull(optionsMonitor); + // ThrowHelper.ThrowIfNull(filters); + + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(scopeFactory); + ArgumentNullException.ThrowIfNull(optionsMonitor); + ArgumentNullException.ThrowIfNull(filters); + + _services = services; + _scopeFactory = scopeFactory; + _optionsMonitor = optionsMonitor; + _filters = filters.ToArray(); + + // case-sensitive because named options is. + _activeHandlers = new ConcurrentDictionary>(StringComparer.Ordinal); + _entryFactory = (name) => + { + return new Lazy(() => + { + return CreateHandlerEntry(name); + }, LazyThreadSafetyMode.ExecutionAndPublication); + }; + + _expiredHandlers = new ConcurrentQueue(); + _expiryCallback = ExpiryTimer_Tick; + + _cleanupTimerLock = new object(); + _cleanupActiveLock = new object(); + + // We want to prevent a circular dependency between ILoggerFactory and IHttpClientFactory, in case + // any of ILoggerProvider instances use IHttpClientFactory to send logs to an external server. + // Logger will be created during the first ExpiryTimer_Tick execution. Lazy guarantees thread safety + // to prevent creation of unnecessary ILogger objects in case several handlers expired at the same time. + + // OC: Null check on '_services'. + _logger = new Lazy(() => + _services is not null + ? _services.GetRequiredService().CreateLogger() + : NullLogger.Instance, + LazyThreadSafetyMode.ExecutionAndPublication); + } + + public HttpClient CreateClient(string name) + { + // ThrowHelper.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(name); + + var handler = CreateHandler(name); + var client = new HttpClient(handler, disposeHandler: false); + + var options = _optionsMonitor!.Get(name); + for (var i = 0; i < options.HttpClientActions.Count; i++) + { + options.HttpClientActions[i](client); + } + + return client; + } + + public HttpMessageHandler CreateHandler(string name) + { + // ThrowHelper.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(name); + + var entry = _activeHandlers.GetOrAdd(name, _entryFactory!).Value; + + StartHandlerEntryTimer(entry); + + return entry.Handler; + } + + // Internal for tests + internal ActiveHandlerTrackingEntry CreateHandlerEntry(string name) + { + var services = _services; + var scope = (IServiceScope?)null; + + var options = _optionsMonitor!.Get(name); + if (!options.SuppressHandlerScope) + { + scope = _scopeFactory!.CreateScope(); + services = scope.ServiceProvider; + } + + try + { + var builder = services!.GetRequiredService(); + builder.Name = name; + + // This is similar to the initialization pattern in: + // https://github.com/aspnet/Hosting/blob/e892ed8bbdcd25a0dafc1850033398dc57f65fe1/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs#L188 + Action configure = Configure; + for (var i = _filters!.Length - 1; i >= 0; i--) + { + configure = _filters[i].Configure(configure); + } + + configure(builder); + + // Wrap the handler so we can ensure the inner handler outlives the outer handler. + var handler = new LifetimeTrackingHttpMessageHandler(builder.Build()); + + // Note that we can't start the timer here. That would introduce a very very subtle race condition + // with very short expiry times. We need to wait until we've actually handed out the handler once + // to start the timer. + // + // Otherwise it would be possible that we start the timer here, immediately expire it (very short + // timer) and then dispose it without ever creating a client. That would be bad. It's unlikely + // this would happen, but we want to be sure. + var entry = new ActiveHandlerTrackingEntry(name, handler, scope, options.HandlerLifetime); + + return entry; + + void Configure(HttpMessageHandlerBuilder b) + { + for (var i = 0; i < options.HttpMessageHandlerBuilderActions.Count; i++) + { + options.HttpMessageHandlerBuilderActions[i](b); + } + + // OC: 'LoggingBuilderActions' option doesn't exist yet. + + // Logging is added separately in the end. But for now it should be still possible to override it via filters... + // foreach (Action action in options.LoggingBuilderActions) + // { + // action(b); + // } + } + } + catch + { + // If something fails while creating the handler, dispose the services. + scope?.Dispose(); + throw; + } + } + + // Internal for tests + internal void ExpiryTimer_Tick(object? state) + { + // OC: Check if disposed. + if (_disposed) + { + return; + } + + var active = (ActiveHandlerTrackingEntry)state!; + + // The timer callback should be the only one removing from the active collection. If we can't find + // our entry in the collection, then this is a bug. + var removed = _activeHandlers.TryRemove(active.Name, out Lazy? found); + Debug.Assert(removed, "Entry not found. We should always be able to remove the entry"); + Debug.Assert(object.ReferenceEquals(active, found!.Value), "Different entry found. The entry should not have been replaced"); + + // At this point the handler is no longer 'active' and will not be handed out to any new clients. + // However we haven't dropped our strong reference to the handler, so we can't yet determine if + // there are still any other outstanding references (we know there is at least one). + // + // We use a different state object to track expired handlers. This allows any other thread that acquired + // the 'active' entry to use it without safety problems. + var expired = new ExpiredHandlerTrackingEntry(active); + _expiredHandlers.Enqueue(expired); + + Log.HandlerExpired(_logger, active.Name, active.Lifetime); + + StartCleanupTimer(); + } + + // Internal so it can be overridden in tests. + internal virtual void StartHandlerEntryTimer(ActiveHandlerTrackingEntry entry) + { + entry.StartExpiryTimer(_expiryCallback); + } + + // Internal so it can be overridden in tests. + internal virtual void StartCleanupTimer() + { + lock (_cleanupTimerLock) + { + _cleanupTimer ??= NonCapturingTimer.Create(_cleanupCallback, this, DefaultCleanupInterval, Timeout.InfiniteTimeSpan); + } + } + + // Internal so it can be overridden in tests. + internal virtual void StopCleanupTimer() + { + lock (_cleanupTimerLock) + { + _cleanupTimer?.Dispose(); + _cleanupTimer = null; + } + } + + // Internal for tests. + internal void CleanupTimer_Tick() + { + // Stop any pending timers, we'll restart the timer if there's anything left to process after cleanup. + // + // With the scheme we're using it's possible we could end up with some redundant cleanup operations. + // This is expected and fine. + // + // An alternative would be to take a lock during the whole cleanup process. This isn't ideal because it + // would result in threads executing ExpiryTimer_Tick as they would need to block on cleanup to figure out + // whether we need to start the timer. + StopCleanupTimer(); + + if (!Monitor.TryEnter(_cleanupActiveLock)) + { + // We don't want to run a concurrent cleanup cycle. This can happen if the cleanup cycle takes + // a long time for some reason. Since we're running user code inside Dispose, it's definitely + // possible. + // + // If we end up in that position, just make sure the timer gets started again. It should be cheap + // to run a 'no-op' cleanup. + // + + // OC: May also happen if called while disposing. + StartCleanupTimer(); + + return; + } + + try + { + var initialCount = _expiredHandlers.Count; + Log.CleanupCycleStart(_logger, initialCount); + + var stopwatch = ValueStopwatch.StartNew(); + + var disposedCount = 0; + for (var i = 0; i < initialCount; i++) + { + // Since we're the only one removing from _expired, TryDequeue must always succeed. + _expiredHandlers.TryDequeue(out ExpiredHandlerTrackingEntry? entry); + Debug.Assert(entry != null, "Entry was null, we should always get an entry back from TryDequeue"); + + // OC: Also check if disposed. + if (entry.CanDispose || _disposed) + { + try + { + entry.InnerHandler?.Dispose(); + entry.InnerHandler = null; + entry.Scope?.Dispose(); + entry.Scope = null; + disposedCount++; + } + catch (Exception ex) + { + Log.CleanupItemFailed(_logger, entry.Name, ex); + } + } + else + { + // If the entry is still live, put it back in the queue so we can process it + // during the next cleanup cycle. + _expiredHandlers.Enqueue(entry); + } + } + + Log.CleanupCycleEnd(_logger, stopwatch.GetElapsedTime(), disposedCount, _expiredHandlers.Count); + } + finally + { + Monitor.Exit(_cleanupActiveLock); + } + + // We didn't totally empty the cleanup queue, try again later. + if (!_expiredHandlers.IsEmpty) + { + StartCleanupTimer(); + } + } + + // OC: Implement IDisposable. + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + foreach (var entry in _activeHandlers.Values) + { + entry.Value.Dispose(); + } + + _activeHandlers.Clear(); + + _services = null; + CleanupTimer_Tick(); + + _scopeFactory = null; + _optionsMonitor = null; + _filters = null; + _entryFactory = null; + } + + private static class Log + { + public static class EventIds + { + public static readonly EventId CleanupCycleStart = new EventId(100, "CleanupCycleStart"); + public static readonly EventId CleanupCycleEnd = new EventId(101, "CleanupCycleEnd"); + public static readonly EventId CleanupItemFailed = new EventId(102, "CleanupItemFailed"); + public static readonly EventId HandlerExpired = new EventId(103, "HandlerExpired"); + } + + private static readonly Action _cleanupCycleStart = LoggerMessage.Define( + LogLevel.Debug, + EventIds.CleanupCycleStart, + "Starting HttpMessageHandler cleanup cycle with {InitialCount} items"); + + private static readonly Action _cleanupCycleEnd = LoggerMessage.Define( + LogLevel.Debug, + EventIds.CleanupCycleEnd, + "Ending HttpMessageHandler cleanup cycle after {ElapsedMilliseconds}ms - processed: {DisposedCount} items - remaining: {RemainingItems} items"); + + private static readonly Action _cleanupItemFailed = LoggerMessage.Define( + LogLevel.Error, + EventIds.CleanupItemFailed, + "HttpMessageHandler.Dispose() threw an unhandled exception for client: '{ClientName}'"); + + private static readonly Action _handlerExpired = LoggerMessage.Define( + LogLevel.Debug, + EventIds.HandlerExpired, + "HttpMessageHandler expired after {HandlerLifetime}ms for client '{ClientName}'"); + + + public static void CleanupCycleStart(Lazy loggerLazy, int initialCount) + { + if (TryGetLogger(loggerLazy, out ILogger? logger)) + { + _cleanupCycleStart(logger, initialCount, null); + } + } + + public static void CleanupCycleEnd(Lazy loggerLazy, TimeSpan duration, int disposedCount, int finalCount) + { + if (TryGetLogger(loggerLazy, out ILogger? logger)) + { + _cleanupCycleEnd(logger, duration.TotalMilliseconds, disposedCount, finalCount, null); + } + } + + public static void CleanupItemFailed(Lazy loggerLazy, string clientName, Exception exception) + { + if (TryGetLogger(loggerLazy, out ILogger? logger)) + { + _cleanupItemFailed(logger, clientName, exception); + } + } + + public static void HandlerExpired(Lazy loggerLazy, string clientName, TimeSpan lifetime) + { + if (TryGetLogger(loggerLazy, out ILogger? logger)) + { + _handlerExpired(logger, lifetime.TotalMilliseconds, clientName, null); + } + } + + private static bool TryGetLogger(Lazy loggerLazy, [NotNullWhen(true)] out ILogger? logger) + { + logger = null; + try + { + logger = loggerLazy.Value; + } + catch { } // Not throwing in logs. + + return logger is not null; + } + } + } +} diff --git a/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/ValueStopwatch.cs b/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/ValueStopwatch.cs new file mode 100644 index 00000000000..19341f36099 --- /dev/null +++ b/src/OrchardCore/OrchardCore/Modules/Overrides/HttpClient/ValueStopwatch.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; + +namespace Microsoft.Extensions.Internal; + +internal readonly struct ValueStopwatch +{ +#if !NET7_0_OR_GREATER + private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; +#endif + + private readonly long _startTimestamp; + + public readonly bool IsActive => _startTimestamp != 0; + + private ValueStopwatch(long startTimestamp) + { + _startTimestamp = startTimestamp; + } + + public static ValueStopwatch StartNew() => new ValueStopwatch(Stopwatch.GetTimestamp()); + + public TimeSpan GetElapsedTime() + { + // Start timestamp can't be zero in an initialized ValueStopwatch. It would have to be literally the first thing executed when the machine boots to be 0. + // So it being 0 is a clear indication of default(ValueStopwatch). + if (!IsActive) + { + throw new InvalidOperationException("An uninitialized, or 'default', ValueStopwatch cannot be used to get elapsed time."); + } + + var end = Stopwatch.GetTimestamp(); + +#if !NET7_0_OR_GREATER + var timestampDelta = end - _startTimestamp; + var ticks = (long)(TimestampToTicks * timestampDelta); + return new TimeSpan(ticks); +#else + return Stopwatch.GetElapsedTime(_startTimestamp, end); +#endif + } +} diff --git a/src/OrchardCore/OrchardCore/Shell/Builders/ShellContainerFactory.cs b/src/OrchardCore/OrchardCore/Shell/Builders/ShellContainerFactory.cs index ab80d928783..cf0eac88998 100644 --- a/src/OrchardCore/OrchardCore/Shell/Builders/ShellContainerFactory.cs +++ b/src/OrchardCore/OrchardCore/Shell/Builders/ShellContainerFactory.cs @@ -50,11 +50,8 @@ public async Task CreateContainerAsync(ShellSettings settings, // Execute IStartup registrations - var moduleServiceCollection = _serviceProvider.CreateChildContainer(_applicationServices); - foreach (var dependency in blueprint.Dependencies.Where(t => typeof(IStartup).IsAssignableFrom(t.Key))) { - moduleServiceCollection.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IStartup), dependency.Key)); tenantServiceCollection.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IStartup), dependency.Key)); } @@ -99,15 +96,8 @@ public async Task CreateContainerAsync(ShellSettings settings, // Add the startup class to the DI so we can instantiate it with // valid ctor arguments - moduleServiceCollection.AddSingleton(rawStartup); tenantServiceCollection.AddSingleton(rawStartup); - moduleServiceCollection.AddSingleton(sp => - { - var startupInstance = sp.GetService(rawStartup); - return new StartupBaseMock(startupInstance, configureServicesMethod, configureMethod, orderProperty, configureOrderProperty); - }); - tenantServiceCollection.AddSingleton(sp => { var startupInstance = sp.GetService(rawStartup); @@ -115,21 +105,11 @@ public async Task CreateContainerAsync(ShellSettings settings, }); } - // Make shell settings available to the modules - moduleServiceCollection.AddSingleton(settings); - moduleServiceCollection.AddSingleton(sp => - { - // Resolve it lazily as it's constructed lazily - var shellSettings = sp.GetRequiredService(); - return shellSettings.ShellConfiguration; - }); - - var moduleServiceProvider = moduleServiceCollection.BuildServiceProvider(true); - // Index all service descriptors by their feature id var featureAwareServiceCollection = new FeatureAwareServiceCollection(tenantServiceCollection); - var startups = moduleServiceProvider.GetServices(); + var shellServiceProvider = tenantServiceCollection.BuildServiceProvider(true); + var startups = shellServiceProvider.GetServices(); // IStartup instances are ordered by module dependency with an Order of 0 by default. // OrderBy performs a stable sort so order is preserved among equal Order values. @@ -147,9 +127,10 @@ public async Task CreateContainerAsync(ShellSettings settings, startup.ConfigureServices(featureAwareServiceCollection); } - await moduleServiceProvider.DisposeAsync(); + await shellServiceProvider.DisposeAsync(); - var shellServiceProvider = tenantServiceCollection.BuildServiceProvider(true); + // Rebuild the service provider from the updated collection. + shellServiceProvider = tenantServiceCollection.BuildServiceProvider(true); // Register all DIed types in ITypeFeatureProvider var typeFeatureProvider = shellServiceProvider.GetRequiredService(); diff --git a/src/OrchardCore/OrchardCore/Shell/Builders/ShellContextFactory.cs b/src/OrchardCore/OrchardCore/Shell/Builders/ShellContextFactory.cs index 4bcae836c48..f06f0d477cd 100644 --- a/src/OrchardCore/OrchardCore/Shell/Builders/ShellContextFactory.cs +++ b/src/OrchardCore/OrchardCore/Shell/Builders/ShellContextFactory.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -45,7 +46,8 @@ async Task IShellContextFactory.CreateShellContextAsync(ShellSetti if (currentDescriptor is not null) { - await describedContext.DisposeAsync(); + // Mark as using shared setting that should not be disposed. + await describedContext.WithSharedSettings().DisposeAsync(); return await CreateDescribedContextAsync(settings, currentDescriptor); } @@ -71,23 +73,32 @@ public async Task CreateDescribedContextAsync(ShellSettings settin _logger.LogDebug("Creating described context for tenant '{TenantName}'", settings.Name); } - await settings.EnsureConfigurationAsync(); - - var blueprint = await _compositionStrategy.ComposeAsync(settings, shellDescriptor); - var provider = await _shellContainerFactory.CreateContainerAsync(settings, blueprint); - - var options = provider.GetService>().Value; - foreach (var initializeAsync in options.Initializers) + // Prevent settings from being disposed when an intermediate container is disposed. + Interlocked.Increment(ref settings._shellCreating); + try { - await initializeAsync(provider); + await settings.EnsureConfigurationAsync(); + + var blueprint = await _compositionStrategy.ComposeAsync(settings, shellDescriptor); + var provider = await _shellContainerFactory.CreateContainerAsync(settings, blueprint); + + var options = provider.GetService>().Value; + foreach (var initializeAsync in options.Initializers) + { + await initializeAsync(provider); + } + + return new ShellContext + { + Settings = settings, + Blueprint = blueprint, + ServiceProvider = provider + }; } - - return new ShellContext + finally { - Settings = settings, - Blueprint = blueprint, - ServiceProvider = provider - }; + Interlocked.Decrement(ref settings._shellCreating); + } } /// diff --git a/src/OrchardCore/OrchardCore/Shell/Configuration/ShellConfigurationSources.cs b/src/OrchardCore/OrchardCore/Shell/Configuration/ShellConfigurationSources.cs index 1c9ced9799d..0bd0466c5f1 100644 --- a/src/OrchardCore/OrchardCore/Shell/Configuration/ShellConfigurationSources.cs +++ b/src/OrchardCore/OrchardCore/Shell/Configuration/ShellConfigurationSources.cs @@ -25,7 +25,7 @@ public ShellConfigurationSources(IOptions shellOptions, ILogger shellOptions) public Task AddSourcesAsync(IConfigurationBuilder builder) { - builder.AddJsonFile(_tenants, optional: true); + builder.AddTenantJsonFile(_tenants, optional: true); return Task.CompletedTask; } diff --git a/src/OrchardCore/OrchardCore/Shell/Distributed/DistributedContext.cs b/src/OrchardCore/OrchardCore/Shell/Distributed/DistributedContext.cs index 032352e33c7..500eedc872f 100644 --- a/src/OrchardCore/OrchardCore/Shell/Distributed/DistributedContext.cs +++ b/src/OrchardCore/OrchardCore/Shell/Distributed/DistributedContext.cs @@ -7,16 +7,24 @@ namespace OrchardCore.Environment.Shell.Distributed { + /// + /// Isolated context based on the default tenant settings used to resolve the . + /// internal class DistributedContext : IDisposable, IAsyncDisposable { private readonly ShellContext _context; private volatile int _count; private bool _released; + /// + /// Initializes a new . + /// public DistributedContext(ShellContext context) { Interlocked.Increment(ref _count); - _context = context; + + // By default marked as using shared settings that should not be disposed. + _context = context.WithSharedSettings(); // If the distributed feature is not enabled, the distributed cache is not set. if (context.ServiceProvider.GetService() is null) @@ -34,10 +42,28 @@ public DistributedContext(ShellContext context) DistributedCache = distributedCache; } + /// + /// Gets the inner . + /// public ShellContext Context => _context; + /// + /// Gets the resolved . + /// public IDistributedCache DistributedCache { get; } + /// + /// Marks this instance as using unshared settings that can be disposed. + /// + public DistributedContext WithoutSharedSettings() + { + _context.WithoutSharedSettings(); + return this; + } + + /// + /// Tries to acquire this instance. + /// public DistributedContext Acquire() { // Don't acquire a released context. @@ -58,18 +84,27 @@ public DistributedContext Acquire() return this; } + /// + /// Releases once this instance. + /// public void Release() { _released = true; Dispose(); } + /// + /// Releases once this instance. + /// public async Task ReleaseAsync() { _released = true; await DisposeAsync(); } + /// + /// Disposes this instance, the last owner dispose the inner . + /// public void Dispose() { // The last use disposes the shell context. @@ -79,6 +114,9 @@ public void Dispose() } } + /// + /// Disposes this instance, the last owner dispose the inner . + /// public ValueTask DisposeAsync() { // The last use disposes the shell context. diff --git a/src/OrchardCore/OrchardCore/Shell/Distributed/DistributedShellHostedService.cs b/src/OrchardCore/OrchardCore/Shell/Distributed/DistributedShellHostedService.cs index 30c49166b39..02ebbf30323 100644 --- a/src/OrchardCore/OrchardCore/Shell/Distributed/DistributedShellHostedService.cs +++ b/src/OrchardCore/OrchardCore/Shell/Distributed/DistributedShellHostedService.cs @@ -18,6 +18,8 @@ namespace OrchardCore.Environment.Shell.Distributed /// internal class DistributedShellHostedService : BackgroundService { + private const string DistributedFeatureId = "OrchardCore.Tenants.Distributed"; + private const string ShellChangedIdKey = "SHELL_CHANGED_ID"; private const string ShellCountChangedIdKey = "SHELL_COUNT_CHANGED_ID"; private const string ReleaseIdKeySuffix = "_RELEASE_ID"; @@ -39,7 +41,7 @@ internal class DistributedShellHostedService : BackgroundService private string _shellChangedId; private string _shellCountChangedId; - private ShellContext _defaultContext; + private long _defaultContextUtcTicks; private DistributedContext _context; private DateTime _busyStartTime; @@ -110,8 +112,11 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) defaultTenantSyncingSeconds = 0; // Load the settings of the default tenant that may have been setup by another instance. - var defaultSettings = await _shellSettingsManager.LoadSettingsAsync(ShellSettings.DefaultShellName); - if (defaultSettings.IsRunning()) + using var loadedDefaultSettings = (await _shellSettingsManager + .LoadSettingsAsync(ShellSettings.DefaultShellName)) + .AsDisposable(); + + if (loadedDefaultSettings.IsRunning()) { // If the default tenant has been setup by another instance, reload it locally. await _shellHost.ReloadShellContextAsync(defaultContext.Settings, eventSource: false); @@ -173,6 +178,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) // Retrieve all tenant settings that are loaded locally. var loadedSettings = _shellHost.GetAllSettings().ToList(); var tenantsToRemove = Array.Empty(); + var tenantsToCreate = Array.Empty(); // Check if at least one tenant has been created or removed. if (shellCountChangedId is not null && _shellCountChangedId != shellCountChangedId) @@ -181,12 +187,14 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) var loadedTenants = loadedSettings.Select(s => s.Name); // Retrieve all new created tenants that are not already loaded. - var tenantsToLoad = sharedTenants.Except(loadedTenants).ToArray(); + tenantsToCreate = sharedTenants.Except(loadedTenants).ToArray(); // Load all new created tenants. - foreach (var tenant in tenantsToLoad) + foreach (var tenant in tenantsToCreate) { - loadedSettings.Add(await _shellSettingsManager.LoadSettingsAsync(tenant)); + loadedSettings.Add((await _shellSettingsManager + .LoadSettingsAsync(tenant)) + .AsDisposable()); } // Retrieve all removed tenants that are not yet removed locally. @@ -200,6 +208,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) // Keep in sync all tenants by checking their specific identifiers. foreach (var settings in loadedSettings) { + // Newly loaded settings from the configuration should be disposed. + using var disposable = tenantsToCreate.Contains(settings.Name) ? settings : null; + // Wait for the min idle time after the max busy time. if (!await TryWaitAfterBusyTime(stoppingToken)) { @@ -212,7 +223,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Try to retrieve the release identifier of this tenant from the distributed cache. var releaseId = await distributedCache.GetStringAsync(ReleaseIdKey(settings.Name), CancellationToken.None); - if (releaseId is not null) + if (releaseId is not null && !tenantsToCreate.Contains(settings.Name)) { // Check if the release identifier of this tenant has changed. var identifier = _identifiers.GetOrAdd(settings.Name, name => new ShellIdentifier()); @@ -237,6 +248,12 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) // Update the local identifier. identifier.ReloadId = reloadId; + // For a new tenant also update the release identifier. + if (tenantsToCreate.Contains(settings.Name)) + { + identifier.ReleaseId = releaseId; + } + // Keep in sync this tenant by reloading it locally. await _shellHost.ReloadShellContextAsync(settings, eventSource: false); } @@ -294,13 +311,24 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) _terminated = true; + _shellHost.LoadingAsync -= LoadingAsync; + _shellHost.ReleasingAsync -= ReleasingAsync; + _shellHost.ReloadingAsync -= ReloadingAsync; + _shellHost.RemovingAsync -= RemovingAsync; + if (_context is not null) { await _context.ReleaseAsync(); } - _defaultContext = null; _context = null; + + foreach (var semaphore in _semaphores.Values) + { + semaphore.Dispose(); + } + + _semaphores.Clear(); } /// @@ -313,18 +341,30 @@ public async Task LoadingAsync() return; } + // Load a first isolated configuration before the default context is initialized. + var defaultSettings = (await _shellSettingsManager + .LoadSettingsAsync(ShellSettings.DefaultShellName)) + .AsDisposable(); + // If there is no default tenant or it is not running, nothing to do. - var defaultSettings = await _shellSettingsManager.LoadSettingsAsync(ShellSettings.DefaultShellName); if (!defaultSettings.IsRunning()) { + defaultSettings.Dispose(); return; } - // Create a local distributed context because it is not yet initialized. - var context = _context = await CreateDistributedContextAsync(defaultSettings); + // Create a distributed context based on the first loaded isolated configuration. + var context = _context = (await CreateDistributedContextAsync(defaultSettings)) + ?.WithoutSharedSettings(); + + if (context is null) + { + defaultSettings.Dispose(); + return; + } // If the required distributed features are not enabled, nothing to do. - var distributedCache = context?.DistributedCache; + var distributedCache = context.DistributedCache; if (distributedCache is null) { return; @@ -440,6 +480,13 @@ public async Task ReloadingAsync(string name) // Acquire the distributed context or create a new one if not yet built. await using var context = await AcquireOrCreateDistributedContextAsync(defaultContext); + // If the context still uses the first isolated configuration. + if (context is not null && !context.Context.SharedSettings) + { + // Reset the serial number so that a new context will be built. + context.Context.Blueprint.Descriptor.SerialNumber = 0; + } + // If the required distributed features are not enabled, nothing to do. var distributedCache = context?.DistributedCache; if (distributedCache is null) @@ -528,84 +575,8 @@ public async Task RemovingAsync(string name) } } - 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 context. - /// - private async Task CreateDistributedContextAsync(ShellContext defaultContext) - { - // Get the default tenant descriptor. - var descriptor = await GetDefaultShellDescriptorAsync(defaultContext); - - // If no descriptor. - if (descriptor is null) - { - // Nothing to create. - return null; - } - - // 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; - } - } - - /// - /// Creates a distributed context based on the default tenant settings. - /// - private async Task CreateDistributedContextAsync(ShellSettings defaultSettings) - { - try - { - return new DistributedContext(await _shellContextFactory.CreateShellContextAsync(defaultSettings)); - } - catch - { - return null; - } - } - - /// - /// 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 is null) - { - try - { - // Get the default tenant descriptor from the store. - descriptor = await _shellContextFactory.GetShellDescriptorAsync(defaultContext.Settings); - } - catch - { - return null; - } - } - - return descriptor; - } + private static string ReleaseIdKey(string name) => $"{name}{ReleaseIdKeySuffix}"; + private static string ReloadIdKey(string name) => $"{name}{ReloadIdKeySuffix}"; /// /// Gets or creates a new distributed context if the default tenant has changed. @@ -613,7 +584,7 @@ private async Task GetDefaultShellDescriptorAsync(ShellContext private async Task GetOrCreateDistributedContextAsync(ShellContext defaultContext) { // Check if the default tenant has changed. - if (_defaultContext != defaultContext) + if (_defaultContextUtcTicks != defaultContext.UtcTicks) { var previousContext = _context; @@ -621,7 +592,7 @@ private async Task GetOrCreateDistributedContextAsync(ShellC _context = await ReuseOrCreateDistributedContextAsync(defaultContext); // Cache the default context. - _defaultContext = defaultContext; + _defaultContextUtcTicks = defaultContext.UtcTicks; // If the context is not reused. if (_context != previousContext && previousContext is not null) @@ -663,8 +634,9 @@ private async Task ReuseOrCreateDistributedContextAsync(Shel return null; } - // Check if the default tenant descriptor was updated. - if (_context.Context.Blueprint.Descriptor.SerialNumber != descriptor.SerialNumber) + // Check if the default tenant descriptor or tenant configuration was updated. + if (_context.Context.Blueprint.Descriptor.SerialNumber != descriptor.SerialNumber || + !_context.Context.Settings.HasConfiguration()) { // Creates a new context based on the default settings and descriptor. return await CreateDistributedContextAsync(defaultContext.Settings, descriptor); @@ -690,6 +662,118 @@ private Task AcquireOrCreateDistributedContextAsync(ShellCon return Task.FromResult(distributedContext); } + /// + /// Creates a distributed context based on the default tenant context. + /// + private async Task CreateDistributedContextAsync(ShellContext defaultContext) + { + // Get the default tenant descriptor. + var descriptor = await GetDefaultShellDescriptorAsync(defaultContext); + + // If no descriptor. + if (descriptor is null) + { + // Nothing to create. + return null; + } + + // 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. + /// + private async Task CreateDistributedContextAsync(ShellSettings defaultSettings) + { + // Get the default tenant descriptor. + var descriptor = await GetDefaultShellDescriptorAsync(defaultSettings); + + // If no descriptor. + if (descriptor is null) + { + // Nothing to create. + return null; + } + + // Creates a new context based on the default settings and descriptor. + return await CreateDistributedContextAsync(defaultSettings, 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; + } + } + + /// + /// Gets the default tenant descriptor based on the default tenant context. + /// + private Task GetDefaultShellDescriptorAsync(ShellContext defaultContext) + { + // Check if the configuration has been disposed. + if (!defaultContext.Settings.HasConfiguration()) + { + return Task.FromResult(null); + } + + // Capture the descriptor as the blueprint may be set to null right after. + var descriptor = defaultContext.Blueprint?.Descriptor; + + // Check if the distributed feature is enabled. + if (descriptor?.Features.Any(feature => feature.Id == DistributedFeatureId) ?? false) + { + return Task.FromResult(descriptor); + } + + // No descriptor if the default context is a placeholder without blueprint. + if (descriptor is null) + { + // Get the default tenant descriptor from the store. + return GetDefaultShellDescriptorAsync(defaultContext.Settings); + } + + return Task.FromResult(null); + } + + /// + /// Gets the default tenant descriptor from the store based on the default tenant configuration. + /// + private async Task GetDefaultShellDescriptorAsync(ShellSettings defaultSettings) + { + // Check if the configuration has been disposed. + if (!defaultSettings.HasConfiguration()) + { + return null; + } + + try + { + // Get the descriptor from the store and check if the distributed feature is enabled. + var descriptor = await _shellContextFactory.GetShellDescriptorAsync(defaultSettings); + if (descriptor?.Features.Any(feature => feature.Id == DistributedFeatureId) ?? false) + { + return descriptor; + } + } + catch + { + } + + return null; + } + /// /// Gets the next idle time before retrying to read the distributed cache. /// diff --git a/src/OrchardCore/OrchardCore/Shell/RunningShellTable.cs b/src/OrchardCore/OrchardCore/Shell/RunningShellTable.cs index dab34ab1059..d2fbcd9b02d 100644 --- a/src/OrchardCore/OrchardCore/Shell/RunningShellTable.cs +++ b/src/OrchardCore/OrchardCore/Shell/RunningShellTable.cs @@ -48,7 +48,7 @@ public void Remove(ShellSettings settings) _shellsByHostAndPrefix = _shellsByHostAndPrefix.RemoveRange(allHostsAndPrefix); } - if (_default == settings) + if (settings.IsDefaultShell()) { _default = null; } diff --git a/src/OrchardCore/OrchardCore/Shell/ShellHost.cs b/src/OrchardCore/OrchardCore/Shell/ShellHost.cs index 49849d9faff..f2f6b68c354 100644 --- a/src/OrchardCore/OrchardCore/Shell/ShellHost.cs +++ b/src/OrchardCore/OrchardCore/Shell/ShellHost.cs @@ -92,11 +92,15 @@ public async Task GetOrCreateShellContextAsync(ShellSettings setti var semaphore = _shellSemaphores.GetOrAdd(settings.Name, (name) => new SemaphoreSlim(1)); await semaphore.WaitAsync(); - try { if (!_shellContexts.TryGetValue(settings.Name, out shell)) { + if (!settings.HasConfiguration()) + { + settings = await _shellSettingsManager.LoadSettingsAsync(settings.Name); + } + shell = await CreateShellContextAsync(settings); AddAndRegisterShell(shell); } @@ -182,7 +186,13 @@ public Task ChangedAsync(ShellDescriptor descriptor, ShellSettings settings) /// Whether the related is invoked. public async Task ReloadShellContextAsync(ShellSettings settings, bool eventSource = true) { - if (ReloadingAsync is not null && eventSource && !settings.IsInitializing()) + // A shell can't be reloaded while it is initializing, only released. + if (settings.IsInitializing()) + { + return; + } + + if (ReloadingAsync is not null && eventSource) { foreach (var d in ReloadingAsync.GetInvocationList()) { @@ -197,10 +207,8 @@ public async Task ReloadShellContextAsync(ShellSettings settings, bool eventSour return; } - if (!settings.IsInitializing()) - { - settings = await _shellSettingsManager.LoadSettingsAsync(settings.Name); - } + // Reload the shell settings from the configuration. + settings = await _shellSettingsManager.LoadSettingsAsync(settings.Name); var count = 0; while (count++ < ReloadShellMaxRetriesCount) @@ -208,6 +216,7 @@ public async Task ReloadShellContextAsync(ShellSettings settings, bool eventSour if (_shellContexts.TryRemove(settings.Name, out var context)) { _runningShellTable.Remove(settings); + context.Settings.AsDisposable(); await context.ReleaseAsync(); } @@ -225,20 +234,15 @@ public async Task ReloadShellContextAsync(ShellSettings settings, bool eventSour _runningShellTable.Add(settings); } - if (settings.IsInitializing()) - { - return; - } - - var currentVersionId = settings.VersionId; - - settings = await _shellSettingsManager.LoadSettingsAsync(settings.Name); - // Consistency: We may have been the last to add the shell but not with the last settings. - if (settings.VersionId == currentVersionId) + var loaded = await _shellSettingsManager.LoadSettingsAsync(settings.Name); + if (settings.VersionId == loaded.VersionId) { + loaded.AsDisposable().Dispose(); return; } + + settings = loaded; } throw new ShellHostReloadException( @@ -299,6 +303,7 @@ public async Task RemoveShellContextAsync(ShellSettings settings, bool eventSour if (_shellContexts.TryRemove(settings.Name, out var context)) { + context.Settings.AsDisposable(); await context.ReleaseAsync(); } @@ -427,7 +432,8 @@ private async Task CreateSetupContextAsync(ShellSettings defaultSe defaultSettings = _shellSettingsManager .CreateDefaultSettings() .AsDefaultShell() - .AsUninitialized(); + .AsUninitialized() + .AsDisposable(); await UpdateShellSettingsAsync(defaultSettings); } diff --git a/src/OrchardCore/OrchardCore/Shell/ShellSettingsManager.cs b/src/OrchardCore/OrchardCore/Shell/ShellSettingsManager.cs index 502c6bf4390..219215a5270 100644 --- a/src/OrchardCore/OrchardCore/Shell/ShellSettingsManager.cs +++ b/src/OrchardCore/OrchardCore/Shell/ShellSettingsManager.cs @@ -12,30 +12,33 @@ namespace OrchardCore.Environment.Shell { - public class ShellSettingsManager : IShellSettingsManager + public class ShellSettingsManager : IShellSettingsManager, IDisposable { private readonly IConfiguration _applicationConfiguration; private readonly IShellsConfigurationSources _tenantsConfigSources; private readonly IShellConfigurationSources _tenantConfigSources; - private readonly IShellsSettingsSources _settingsSources; + private readonly IShellsSettingsSources _tenantsSettingsSources; private IConfiguration _configuration; + private IConfigurationRoot _configurationRoot; + private IConfigurationRoot _tenantsSettingsRoot; private IEnumerable _configuredTenants; private readonly SemaphoreSlim _semaphore = new(1); - private Func> _tenantConfigBuilderFactory; + private Func, Task> _tenantConfigFactoryAsync; private readonly SemaphoreSlim _tenantConfigSemaphore = new(1); + private bool _disposed; public ShellSettingsManager( IConfiguration applicationConfiguration, IShellsConfigurationSources tenantsConfigSources, IShellConfigurationSources tenantConfigSources, - IShellsSettingsSources settingsSources) + IShellsSettingsSources tenantsSettingsSources) { _applicationConfiguration = applicationConfiguration; _tenantsConfigSources = tenantsConfigSources; _tenantConfigSources = tenantConfigSources; - _settingsSources = settingsSources; + _tenantsSettingsSources = tenantsSettingsSources; } public ShellSettings CreateDefaultSettings() @@ -54,25 +57,20 @@ public async Task> LoadSettingsAsync() { await EnsureConfigurationAsync(); - var tenantsSettings = (await new ConfigurationBuilder() - .AddSourcesAsync(_settingsSources)) - .Build(); - - var tenants = tenantsSettings.GetChildren().Select(section => section.Key); + var tenants = _tenantsSettingsRoot.GetChildren().Select(section => section.Key); var allTenants = _configuredTenants.Concat(tenants).Distinct().ToArray(); var allSettings = new List(); foreach (var tenant in allTenants) { - var tenantSettings = new ConfigurationBuilder() + var tenantSettingsBuilder = new ConfigurationBuilder() .AddConfiguration(_configuration) .AddConfiguration(_configuration.GetSection(tenant)) - .AddConfiguration(tenantsSettings.GetSection(tenant)) - .Build(); + .AddConfiguration(_tenantsSettingsRoot.GetSection(tenant)); - var settings = new ShellConfiguration(tenantSettings); - var configuration = new ShellConfiguration(tenant, _tenantConfigBuilderFactory); + var settings = new ShellConfiguration(tenantSettingsBuilder); + var configuration = new ShellConfiguration(tenant, _tenantConfigFactoryAsync); var shellSettings = new ShellSettings(settings, configuration) { @@ -97,11 +95,9 @@ public async Task> LoadSettingsNamesAsync() { await EnsureConfigurationAsync(); - var tenantsSettings = (await new ConfigurationBuilder() - .AddSourcesAsync(_settingsSources)) - .Build(); + _tenantsSettingsRoot.Reload(); - var tenants = tenantsSettings.GetChildren().Select(section => section.Key); + var tenants = _tenantsSettingsRoot.GetChildren().Select(section => section.Key); return _configuredTenants.Concat(tenants).Distinct().ToArray(); } finally @@ -117,18 +113,15 @@ public async Task LoadSettingsAsync(string tenant) { await EnsureConfigurationAsync(); - var tenantsSettings = (await new ConfigurationBuilder() - .AddSourcesAsync(tenant, _settingsSources)) - .Build(); + _tenantsSettingsRoot.Reload(); - var tenantSettings = new ConfigurationBuilder() + var tenantSettingsBuilder = new ConfigurationBuilder() .AddConfiguration(_configuration) .AddConfiguration(_configuration.GetSection(tenant)) - .AddConfiguration(tenantsSettings.GetSection(tenant)) - .Build(); + .AddConfiguration(_tenantsSettingsRoot.GetSection(tenant)); - var settings = new ShellConfiguration(tenantSettings); - var configuration = new ShellConfiguration(tenant, _tenantConfigBuilderFactory); + var settings = new ShellConfiguration(tenantSettingsBuilder); + var configuration = new ShellConfiguration(tenant, _tenantConfigFactoryAsync); return new ShellSettings(settings, configuration) { @@ -148,7 +141,7 @@ public async Task SaveSettingsAsync(ShellSettings settings) { await EnsureConfigurationAsync(); - if (settings == null) + if (settings is null) { throw new ArgumentNullException(nameof(settings)); } @@ -185,7 +178,7 @@ public async Task SaveSettingsAsync(ShellSettings settings) tenantSettings.Remove("Name"); - await _settingsSources.SaveAsync(settings.Name, tenantSettings.ToObject>()); + await _tenantsSettingsSources.SaveAsync(settings.Name, tenantSettings.ToObject>()); var tenantConfig = new JObject(); @@ -230,12 +223,12 @@ public async Task RemoveSettingsAsync(ShellSettings settings) { await EnsureConfigurationAsync(); - if (settings == null) + if (settings is null) { throw new ArgumentNullException(nameof(settings)); } - await _settingsSources.RemoveAsync(settings.Name); + await _tenantsSettingsSources.RemoveAsync(settings.Name); await _tenantConfigSemaphore.WaitAsync(); try @@ -255,7 +248,7 @@ public async Task RemoveSettingsAsync(ShellSettings settings) private async Task EnsureConfigurationAsync() { - if (_configuration != null) + if (_configuration is not null) { return; } @@ -263,18 +256,20 @@ private async Task EnsureConfigurationAsync() var lastProviders = (_applicationConfiguration as IConfigurationRoot)?.Providers .Where(p => p is EnvironmentVariablesConfigurationProvider || p is CommandLineConfigurationProvider) - .ToArray(); + .ToArray() + ?? Array.Empty(); var configurationBuilder = await new ConfigurationBuilder() .AddConfiguration(_applicationConfiguration) .AddSourcesAsync(_tenantsConfigSources); - if (lastProviders?.Length > 0) + if (lastProviders.Length > 0) { configurationBuilder.AddConfiguration(new ConfigurationRoot(lastProviders)); } - var configuration = configurationBuilder.Build().GetSection("OrchardCore"); + _configurationRoot = configurationBuilder.Build(); + var configuration = _configurationRoot.GetSection("OrchardCore"); _configuredTenants = configuration.GetChildren() .Where(section => Enum.TryParse(section["State"], ignoreCase: true, out _)) @@ -282,14 +277,23 @@ private async Task EnsureConfigurationAsync() .Distinct() .ToArray(); - _tenantConfigBuilderFactory = async (tenant) => + _tenantsSettingsRoot = (await new ConfigurationBuilder() + .AddSourcesAsync(_tenantsSettingsSources)) + .Build(); + + _tenantConfigFactoryAsync = async (tenant, configure) => { await _tenantConfigSemaphore.WaitAsync(); try { - var builder = new ConfigurationBuilder().AddConfiguration(_configuration); - builder.AddConfiguration(configuration.GetSection(tenant)); - return await builder.AddSourcesAsync(tenant, _tenantConfigSources); + var builder = await new ConfigurationBuilder() + .AddConfiguration(_configuration) + .AddConfiguration(_configuration.GetSection(tenant)) + .AddSourcesAsync(tenant, _tenantConfigSources); + + configure(builder); + + return builder.Build(); } finally { @@ -299,5 +303,23 @@ private async Task EnsureConfigurationAsync() _configuration = configuration; } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + (_configurationRoot as IDisposable)?.Dispose(); + (_tenantsSettingsRoot as IDisposable)?.Dispose(); + + _semaphore?.Dispose(); + _tenantConfigSemaphore?.Dispose(); + + GC.SuppressFinalize(this); + } } }