<commit_before>using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using OrchardCore.Environment.Extensions;
using OrchardCore.Environment.Shell.Builders;
using OrchardCore.Environment.Shell.Descriptor.Models;
using OrchardCore.Environment.Shell.Events;
using OrchardCore.Environment.Shell.Models;
using OrchardCore.Environment.Shell.Scope;

namespace OrchardCore.Environment.Shell
{
    /// <summary>
    /// All <see cref="ShellContext"/> are pre-created when <see cref="InitializeAsync"/> is called on startup and where we first load
    /// all <see cref="ShellSettings"/> that we also need to register in the <see cref="IRunningShellTable"/> to serve incoming requests.
    /// For each <see cref="ShellContext"/> a service container and then a request pipeline are only built on the first matching request.
    /// </summary>
    public class ShellHost : IShellHost, IDisposable
    {
        private const int ReloadShellMaxRetriesCount = 9;

        private readonly IShellSettingsManager _shellSettingsManager;
        private readonly IShellContextFactory _shellContextFactory;
        private readonly IRunningShellTable _runningShellTable;
        private readonly IExtensionManager _extensionManager;
        private readonly ILogger _logger;

        private bool _initialized;
        private readonly ConcurrentDictionary<string, ShellContext> _shellContexts = new ConcurrentDictionary<string, ShellContext>();
        private readonly ConcurrentDictionary<string, ShellSettings> _shellSettings = new ConcurrentDictionary<string, ShellSettings>();
        private readonly ConcurrentDictionary<string, SemaphoreSlim> _shellSemaphores = new ConcurrentDictionary<string, SemaphoreSlim>();
        private SemaphoreSlim _initializingSemaphore = new SemaphoreSlim(1);

        public ShellHost(
            IShellSettingsManager shellSettingsManager,
            IShellContextFactory shellContextFactory,
            IRunningShellTable runningShellTable,
            IExtensionManager extensionManager,
            ILogger<ShellHost> logger)
        {
            _shellSettingsManager = shellSettingsManager;
            _shellContextFactory = shellContextFactory;
            _runningShellTable = runningShellTable;
            _extensionManager = extensionManager;
            _logger = logger;
        }

        public ShellsEvent LoadingAsync { get; set; }
        public ShellEvent ReleasingAsync { get; set; }
        public ShellEvent ReloadingAsync { get; set; }

        public async Task InitializeAsync()
        {
            if (!_initialized)
            {
                // Prevent concurrent requests from creating all shells multiple times
                await _initializingSemaphore.WaitAsync();
                try
                {
                    if (!_initialized)
                    {
                        await PreCreateAndRegisterShellsAsync();
                    }
                }
                finally
                {
                    _initialized = true;
                    _initializingSemaphore.Release();
                }
            }
        }

        public async Task<ShellContext> GetOrCreateShellContextAsync(ShellSettings settings)
        {
            ShellContext shell = null;

            while (shell == null)
            {
                if (!_shellContexts.TryGetValue(settings.Name, out shell))
                {
                    var semaphore = _shellSemaphores.GetOrAdd(settings.Name, (name) => new SemaphoreSlim(1));

                    await semaphore.WaitAsync();

                    try
                    {
                        if (!_shellContexts.TryGetValue(settings.Name, out shell))
                        {
                            shell = await CreateShellContextAsync(settings);
                            AddAndRegisterShell(shell);
                        }
                    }
                    finally
                    {
                        semaphore.Release();
                    }
                }

                if (shell.Released)
                {
                    // If the context is released, it is removed from the dictionary so that the next iteration
                    // or a new call on 'GetOrCreateShellContextAsync()' will recreate a new shell context.
                    _shellContexts.TryRemove(settings.Name, out _);
                    shell = null;
                }
            }

            return shell;
        }

        public async Task<ShellScope> GetScopeAsync(ShellSettings settings)
        {
            ShellScope scope = null;

            while (scope == null)
            {
                if (!_shellContexts.TryGetValue(settings.Name, out var shellContext))
                {
                    shellContext = await GetOrCreateShellContextAsync(settings);
                }

                // We create a scope before checking if the shell has been released.
                scope = shellContext.CreateScope();

                // If CreateScope() returned null, the shell is released. We then remove it and
                // retry with the hope to get one that won't be released before we create a scope.
                if (scope == null)
                {
                    // If the context is released, it is removed from the dictionary so that the next
                    // iteration or a new call on 'GetScopeAsync()' will recreate a new shell context.
                    _shellContexts.TryRemove(settings.Name, out _);
                }
            }

            return scope;
        }

        public async Task UpdateShellSettingsAsync(ShellSettings settings)
        {
            settings.Identifier = IdGenerator.GenerateId();
            await _shellSettingsManager.SaveSettingsAsync(settings);
            await ReloadShellContextAsync(settings);
        }

        /// <summary>
        /// A feature is enabled / disabled, the tenant needs to be released so that a new shell will be built.
        /// </summary>
        public Task ChangedAsync(ShellDescriptor descriptor, ShellSettings settings)
            => ReleaseShellContextAsync(settings);

        /// <summary>
        /// Reloads the settings and releases the shell so that a new one will be
        /// built for subsequent requests, while existing requests get flushed.
        /// </summary>
        /// <param name="settings">The <see cref="ShellSettings"/> to reload.</param>
        /// <param name="eventSource">
        /// Whether the related <see cref="ShellEvent"/> is invoked.
        /// </param>
        public async Task ReloadShellContextAsync(ShellSettings settings, bool eventSource = true)
        {
            if (ReloadingAsync != null && eventSource && settings.State != TenantState.Initializing)
            {
                await ReloadingAsync(settings.Name);
            }

            // A disabled shell still in use will be released by its last scope.
            if (!CanReleaseShell(settings))
            {
                _runningShellTable.Remove(settings);
                return;
            }

            if (settings.State != TenantState.Initializing)
            {
                settings = await _shellSettingsManager.LoadSettingsAsync(settings.Name);
            }

            var count = 0;
            while (count < ReloadShellMaxRetriesCount)
            {
                count++;

                if (_shellContexts.TryRemove(settings.Name, out var context))
                {
                    _runningShellTable.Remove(settings);
                    context.Release();
                }

                // Add a 'PlaceHolder' allowing to retrieve the settings until the shell will be rebuilt.
                if (!_shellContexts.TryAdd(settings.Name, new ShellContext.PlaceHolder { Settings = settings }))
                {
                    // Atomicity: We may have been the last to load the settings but unable to add the shell.
                    continue;
                }

                _shellSettings[settings.Name] = settings;

                if (CanRegisterShell(settings))
                {
                    _runningShellTable.Add(settings);
                }

                if (settings.State == TenantState.Initializing)
                {
                    return;
                }

                var currentIdentifier = settings.Identifier;

                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.Identifier == currentIdentifier)
                {
                    return;
                }
            }

            throw new ShellHostReloadException(
                $"Unable to reload the tenant '{settings.Name}' as too many concurrent processes are trying to do so.");
        }

        /// <summary>
        /// Releases a shell so that a new one will be built for subsequent requests.
        /// Note: Can be used to free up resources after a given time of inactivity.
        /// </summary>
        /// <param name="settings">The <see cref="ShellSettings"/> to reload.</param>
        /// <param name="eventSource">
        /// Whether the related <see cref="ShellEvent"/> is invoked.
        /// </param>
        public async Task ReleaseShellContextAsync(ShellSettings settings, bool eventSource = true)
        {
            if (ReleasingAsync != null && eventSource && settings.State != TenantState.Initializing)
            {
                await ReleasingAsync(settings.Name);
            }

            // A disabled shell still in use will be released by its last scope.
            if (!CanReleaseShell(settings))
            {
                return;
            }

            if (_shellContexts.TryRemove(settings.Name, out var context))
            {
                context.Release();
            }

            // Add a 'PlaceHolder' allowing to retrieve the settings until the shell will be rebuilt.
            if (_shellContexts.TryAdd(context.Settings.Name, new ShellContext.PlaceHolder { Settings = settings }))<commit_msg>Fixes context.Settings NRE (#7344)<commit_after>using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using OrchardCore.Environment.Extensions;
using OrchardCore.Environment.Shell.Builders;
using OrchardCore.Environment.Shell.Descriptor.Models;
using OrchardCore.Environment.Shell.Events;
using OrchardCore.Environment.Shell.Models;
using OrchardCore.Environment.Shell.Scope;

namespace OrchardCore.Environment.Shell
{
    /// <summary>
    /// All <see cref="ShellContext"/> are pre-created when <see cref="InitializeAsync"/> is called on startup and where we first load
    /// all <see cref="ShellSettings"/> that we also need to register in the <see cref="IRunningShellTable"/> to serve incoming requests.
    /// For each <see cref="ShellContext"/> a service container and then a request pipeline are only built on the first matching request.
    /// </summary>
    public class ShellHost : IShellHost, IDisposable
    {
        private const int ReloadShellMaxRetriesCount = 9;

        private readonly IShellSettingsManager _shellSettingsManager;
        private readonly IShellContextFactory _shellContextFactory;
        private readonly IRunningShellTable _runningShellTable;
        private readonly IExtensionManager _extensionManager;
        private readonly ILogger _logger;

        private bool _initialized;
        private readonly ConcurrentDictionary<string, ShellContext> _shellContexts = new ConcurrentDictionary<string, ShellContext>();
        private readonly ConcurrentDictionary<string, ShellSettings> _shellSettings = new ConcurrentDictionary<string, ShellSettings>();
        private readonly ConcurrentDictionary<string, SemaphoreSlim> _shellSemaphores = new ConcurrentDictionary<string, SemaphoreSlim>();
        private SemaphoreSlim _initializingSemaphore = new SemaphoreSlim(1);

        public ShellHost(
            IShellSettingsManager shellSettingsManager,
            IShellContextFactory shellContextFactory,
            IRunningShellTable runningShellTable,
            IExtensionManager extensionManager,
            ILogger<ShellHost> logger)
        {
            _shellSettingsManager = shellSettingsManager;
            _shellContextFactory = shellContextFactory;
            _runningShellTable = runningShellTable;
            _extensionManager = extensionManager;
            _logger = logger;
        }

        public ShellsEvent LoadingAsync { get; set; }
        public ShellEvent ReleasingAsync { get; set; }
        public ShellEvent ReloadingAsync { get; set; }

        public async Task InitializeAsync()
        {
            if (!_initialized)
            {
                // Prevent concurrent requests from creating all shells multiple times
                await _initializingSemaphore.WaitAsync();
                try
                {
                    if (!_initialized)
                    {
                        await PreCreateAndRegisterShellsAsync();
                    }
                }
                finally
                {
                    _initialized = true;
                    _initializingSemaphore.Release();
                }
            }
        }

        public async Task<ShellContext> GetOrCreateShellContextAsync(ShellSettings settings)
        {
            ShellContext shell = null;

            while (shell == null)
            {
                if (!_shellContexts.TryGetValue(settings.Name, out shell))
                {
                    var semaphore = _shellSemaphores.GetOrAdd(settings.Name, (name) => new SemaphoreSlim(1));

                    await semaphore.WaitAsync();

                    try
                    {
                        if (!_shellContexts.TryGetValue(settings.Name, out shell))
                        {
                            shell = await CreateShellContextAsync(settings);
                            AddAndRegisterShell(shell);
                        }
                    }
                    finally
                    {
                        semaphore.Release();
                    }
                }

                if (shell.Released)
                {
                    // If the context is released, it is removed from the dictionary so that the next iteration
                    // or a new call on 'GetOrCreateShellContextAsync()' will recreate a new shell context.
                    _shellContexts.TryRemove(settings.Name, out _);
                    shell = null;
                }
            }

            return shell;
        }

        public async Task<ShellScope> GetScopeAsync(ShellSettings settings)
        {
            ShellScope scope = null;

            while (scope == null)
            {
                if (!_shellContexts.TryGetValue(settings.Name, out var shellContext))
                {
                    shellContext = await GetOrCreateShellContextAsync(settings);
                }

                // We create a scope before checking if the shell has been released.
                scope = shellContext.CreateScope();

                // If CreateScope() returned null, the shell is released. We then remove it and
                // retry with the hope to get one that won't be released before we create a scope.
                if (scope == null)
                {
                    // If the context is released, it is removed from the dictionary so that the next
                    // iteration or a new call on 'GetScopeAsync()' will recreate a new shell context.
                    _shellContexts.TryRemove(settings.Name, out _);
                }
            }

            return scope;
        }

        public async Task UpdateShellSettingsAsync(ShellSettings settings)
        {
            settings.Identifier = IdGenerator.GenerateId();
            await _shellSettingsManager.SaveSettingsAsync(settings);
            await ReloadShellContextAsync(settings);
        }

        /// <summary>
        /// A feature is enabled / disabled, the tenant needs to be released so that a new shell will be built.
        /// </summary>
        public Task ChangedAsync(ShellDescriptor descriptor, ShellSettings settings)
            => ReleaseShellContextAsync(settings);

        /// <summary>
        /// Reloads the settings and releases the shell so that a new one will be
        /// built for subsequent requests, while existing requests get flushed.
        /// </summary>
        /// <param name="settings">The <see cref="ShellSettings"/> to reload.</param>
        /// <param name="eventSource">
        /// Whether the related <see cref="ShellEvent"/> is invoked.
        /// </param>
        public async Task ReloadShellContextAsync(ShellSettings settings, bool eventSource = true)
        {
            if (ReloadingAsync != null && eventSource && settings.State != TenantState.Initializing)
            {
                await ReloadingAsync(settings.Name);
            }

            // A disabled shell still in use will be released by its last scope.
            if (!CanReleaseShell(settings))
            {
                _runningShellTable.Remove(settings);
                return;
            }

            if (settings.State != TenantState.Initializing)
            {
                settings = await _shellSettingsManager.LoadSettingsAsync(settings.Name);
            }

            var count = 0;
            while (count < ReloadShellMaxRetriesCount)
            {
                count++;

                if (_shellContexts.TryRemove(settings.Name, out var context))
                {
                    _runningShellTable.Remove(settings);
                    context.Release();
                }

                // Add a 'PlaceHolder' allowing to retrieve the settings until the shell will be rebuilt.
                if (!_shellContexts.TryAdd(settings.Name, new ShellContext.PlaceHolder { Settings = settings }))
                {
                    // Atomicity: We may have been the last to load the settings but unable to add the shell.
                    continue;
                }

                _shellSettings[settings.Name] = settings;

                if (CanRegisterShell(settings))
                {
                    _runningShellTable.Add(settings);
                }

                if (settings.State == TenantState.Initializing)
                {
                    return;
                }

                var currentIdentifier = settings.Identifier;

                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.Identifier == currentIdentifier)
                {
                    return;
                }
            }

            throw new ShellHostReloadException(
                $"Unable to reload the tenant '{settings.Name}' as too many concurrent processes are trying to do so.");
        }

        /// <summary>
        /// Releases a shell so that a new one will be built for subsequent requests.
        /// Note: Can be used to free up resources after a given time of inactivity.
        /// </summary>
        /// <param name="settings">The <see cref="ShellSettings"/> to reload.</param>
        /// <param name="eventSource">
        /// Whether the related <see cref="ShellEvent"/> is invoked.
        /// </param>
        public async Task ReleaseShellContextAsync(ShellSettings settings, bool eventSource = true)
        {
            if (ReleasingAsync != null && eventSource && settings.State != TenantState.Initializing)
            {
                await ReleasingAsync(settings.Name);
            }

            // A disabled shell still in use will be released by its last scope.
            if (!CanReleaseShell(settings))
            {
                return;
            }

            if (_shellContexts.TryRemove(settings.Name, out var context))
            {
                context.Release();
            }

            // Add a 'PlaceHolder' allowing to retrieve the settings until the shell will be rebuilt.
            if (_shellContexts.TryAdd(settings.Name, new ShellContext.PlaceHolder { Settings = settings }))
{'fraction_non_alphanumeric': 0.07361695948889212, 'fraction_numerical': 0.0005808044141135473, 'mean_word_length': 1.702328014648182, 'pattern_counts': {'":': 0, '<': 73, '<?xml version=': 0, '>': 75, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4221322', 'n_tokens_mistral': 4768, 'n_tokens_neox': 4501, 'n_words': 1415}