Beispiel #1
0
        /// <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>
        public async Task ReloadShellContextAsync(ShellSettings settings)
        {
            // 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;
                }

                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.");
        }
Beispiel #2
0
        /// <summary>
        /// Marks the specific tenant as released, such that a new shell is created for subsequent requests,
        /// while existing requests get flushed.
        /// </summary>
        /// <param name="settings"></param>
        public async Task ReloadShellContextAsync(ShellSettings settings)
        {
            if (settings.State == TenantState.Disabled)
            {
                // If a disabled shell is still in use it will be released and then disposed by its last scope.
                // Knowing that it is still removed from the running shell table, so that it is no more served.
                if (_shellContexts.TryGetValue(settings.Name, out var value) && value.ActiveScopes > 0)
                {
                    _runningShellTable.Remove(settings);
                    return;
                }
            }

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

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

            await GetOrCreateShellContextAsync(settings);
        }
        public async Task PaymentSuccess(string tenantId, string tenantName, BillingPeriod billingPeriod, decimal amount, PaymentMethod paymentMethod, string planName)
        {
            //TODO: Should billing info be saved in default tenant only, in the tenant's db, or both ?

            // Retrieve settings for speficified tenant.
            var settingsList = await _shellSettingsManager.LoadSettingsAsync();

            if (settingsList.Any())
            {
                var settings   = settingsList.SingleOrDefault(s => string.Equals(s.Name, tenantName, StringComparison.OrdinalIgnoreCase));
                var shellScope = await _shellHost.GetScopeAsync(settings);

                await shellScope.UsingAsync(async scope =>
                {
                    //Check if billing history exists
                    var tenantBillingRepo    = scope.ServiceProvider.GetServices <ITenantBillingHistoryRepository>().FirstOrDefault();
                    var tenantBillingHistory = await tenantBillingRepo.GetTenantBillingDetailsByNameAsync(tenantName);
                    if (tenantBillingHistory == null)
                    {
                        tenantBillingHistory = new TenantBillingDetails(tenantId, tenantName, planName);
                    }

                    if (tenantBillingHistory.IsNewPaymentMethod(paymentMethod))
                    {
                        tenantBillingHistory.AddNewPaymentMethod(paymentMethod);
                    }

                    tenantBillingHistory.AddMonthlyBill(billingPeriod, PaymentStatus.Success, amount, paymentMethod.CreditCardInfo);



                    await tenantBillingRepo.CreateAsync(tenantBillingHistory);
                });
            }
        }
Beispiel #4
0
        /// <summary>
        /// The auto setup middleware invoke.
        /// </summary>
        /// <param name="httpContext">
        /// The http context.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        public async Task InvokeAsync(HttpContext httpContext)
        {
            if (_setupOptions != null && _shellSettings.State == TenantState.Uninitialized)
            {
                // Try to acquire a lock before starting installation, it guaranties an atomic setup in multi instances environment.
                (var locker, var locked) = await _distributedLock.TryAcquireAutoSetupLockAsync(_lockOptions);

                if (!locked)
                {
                    throw new TimeoutException($"Fails to acquire an auto setup lock for the tenant: {_setupOptions.ShellName}");
                }

                await using var acquiredLock = locker;

                if (_shellSettings.State == TenantState.Uninitialized)
                {
                    var pathBase = httpContext.Request.PathBase;
                    if (!pathBase.HasValue)
                    {
                        pathBase = "/";
                    }

                    // Check if the tenant was installed by another instance.
                    var settings = await _shellSettingsManager.LoadSettingsAsync(_shellSettings.Name);

                    if (settings.State != TenantState.Uninitialized)
                    {
                        await _shellHost.ReloadShellContextAsync(_shellSettings, eventSource : false);

                        httpContext.Response.Redirect(pathBase);

                        return;
                    }

                    var setupService = httpContext.RequestServices.GetRequiredService <ISetupService>();
                    if (await SetupTenantAsync(setupService, _setupOptions, _shellSettings))
                    {
                        if (_setupOptions.IsDefault)
                        {
                            // Create the rest of the shells for further on demand setup.
                            foreach (var setupOptions in _options.Tenants)
                            {
                                if (_setupOptions != setupOptions)
                                {
                                    await CreateTenantSettingsAsync(setupOptions);
                                }
                            }
                        }

                        httpContext.Response.Redirect(pathBase);

                        return;
                    }
                }
            }

            await _next.Invoke(httpContext);
        }
Beispiel #5
0
        public async Task <ActionResult> IndexPOST(CustomSetupViewModel model)
        {
            if (!await IsValidRequest(model.Secret))
            {
                return(BadRequest(S["Error with tenant setup link. Please contact support to issue a new link"]));
            }
            if (!IsModelValid(model))
            {
                return(View(model));
            }

            var setupContext = await CreateSetupContext(model.SiteName, model.SiteTimeZone);

            await _setupService.SetupAsync(setupContext);

            // Check if a component in the Setup failed
            if (setupContext.Errors.Any())
            {
                foreach (var error in setupContext.Errors)
                {
                    ModelState.AddModelError(error.Key, error.Value);
                }
                return(View(model));
            }

            var shellSetting = await _shellSettingsManager.LoadSettingsAsync(_shellSettings.Name);

            await(await _shellHost.GetScopeAsync(shellSetting)).UsingAsync(async scope =>
            {
                void reportError(string key, string message)
                {
                    setupContext.Errors[key] = message;
                }

                // Invoke modules to react to the custom setup event
                var customsetupEventHandlers = scope.ServiceProvider.GetServices <ICustomTenantSetupEventHandler>();
                var logger = scope.ServiceProvider.GetRequiredService <ILogger <CustomSetupController> >();
                await customsetupEventHandlers.InvokeAsync(x => x.Setup(model.Email, model.Password, "CourseAdmin", reportError), logger);
            });

            return(Redirect($"~/{_adminOptions.AdminUrlPrefix}"));
        }
        /// <summary>
        /// Keep in sync tenants by sharing shell identifiers through an <see cref="IDistributedCache"/>.
        /// </summary>
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            stoppingToken.Register(() =>
            {
                _logger.LogInformation("'{ServiceName}' is stopping.", nameof(DistributedShellHostedService));
            });

            // Init the idle time.
            var idleTime = MinIdleTime;

            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    // Wait for the current idle time on each loop.
                    if (!await TryWaitAsync(idleTime, stoppingToken))
                    {
                        break;
                    }

                    // If there is no default tenant or it is not running, nothing to do.
                    if (!_shellHost.TryGetShellContext(ShellHelper.DefaultShellName, out var defaultContext) ||
                        defaultContext.Settings.State != TenantState.Running)
                    {
                        continue;
                    }

                    // Get or create a new distributed context if the default tenant has changed.
                    var context = await GetOrCreateDistributedContextAsync(defaultContext);

                    // If the required distributed features are not enabled, nothing to do.
                    var distributedCache = context?.DistributedCache;
                    if (distributedCache == null)
                    {
                        continue;
                    }

                    // Try to retrieve the tenant changed global identifier from the distributed cache.
                    string shellChangedId;
                    try
                    {
                        shellChangedId = await distributedCache.GetStringAsync(ShellChangedIdKey);
                    }
                    catch (Exception ex) when(!ex.IsFatal())
                    {
                        // Get the next idle time before retrying to read the distributed cache.
                        idleTime = NextIdleTimeBeforeRetry(idleTime, ex);
                        continue;
                    }

                    // Reset the idle time.
                    idleTime = MinIdleTime;

                    // Check if at least one tenant has changed.
                    if (shellChangedId == null || _shellChangedId == shellChangedId)
                    {
                        continue;
                    }

                    // Try to retrieve the tenant created global identifier from the distributed cache.
                    string shellCreatedId;
                    try
                    {
                        shellCreatedId = await distributedCache.GetStringAsync(ShellCreatedIdKey);
                    }
                    catch (Exception ex) when(!ex.IsFatal())
                    {
                        _logger.LogError(ex, "Unable to read the distributed cache before checking if a tenant has been created.");
                        continue;
                    }

                    // Retrieve all tenant settings that are already loaded.
                    var allSettings = _shellHost.GetAllSettings().ToList();

                    // Check if at least one tenant has been created.
                    if (shellCreatedId != null && _shellCreatedId != shellCreatedId)
                    {
                        // Retrieve all new created tenants that are not already loaded.
                        var names = (await _shellSettingsManager.LoadSettingsNamesAsync())
                                    .Except(allSettings.Select(s => s.Name))
                                    .ToArray();

                        // Load and enlist the settings of all new created tenant.
                        foreach (var name in names)
                        {
                            allSettings.Add(await _shellSettingsManager.LoadSettingsAsync(name));
                        }
                    }

                    // Init the busy start time.
                    var _busyStartTime = DateTime.UtcNow;
                    var syncingSuccess = true;

                    // Keep in sync all tenants by checking their specific identifiers.
                    foreach (var settings in allSettings)
                    {
                        // Wait for the min idle time after the max busy time.
                        if (!await TryWaitAfterBusyTime(stoppingToken))
                        {
                            break;
                        }

                        var semaphore = _semaphores.GetOrAdd(settings.Name, name => new SemaphoreSlim(1));
                        await semaphore.WaitAsync();

                        try
                        {
                            // Try to retrieve the release identifier of this tenant from the distributed cache.
                            var releaseId = await distributedCache.GetStringAsync(ReleaseIdKey(settings.Name));

                            if (releaseId != null)
                            {
                                // Check if the release identifier of this tenant has changed.
                                var identifier = _identifiers.GetOrAdd(settings.Name, name => new ShellIdentifier());
                                if (identifier.ReleaseId != releaseId)
                                {
                                    // Upate the local identifier.
                                    identifier.ReleaseId = releaseId;

                                    // Keep in sync this tenant by releasing it locally.
                                    await _shellHost.ReleaseShellContextAsync(settings, eventSource : false);
                                }
                            }

                            // Try to retrieve the reload identifier of this tenant from the distributed cache.
                            var reloadId = await distributedCache.GetStringAsync(ReloadIdKey(settings.Name));

                            if (reloadId != null)
                            {
                                // Check if the reload identifier of this tenant has changed.
                                var identifier = _identifiers.GetOrAdd(settings.Name, name => new ShellIdentifier());
                                if (identifier.ReloadId != reloadId)
                                {
                                    // Upate the local identifier.
                                    identifier.ReloadId = reloadId;

                                    // Keep in sync this tenant by reloading it locally.
                                    await _shellHost.ReloadShellContextAsync(settings, eventSource : false);
                                }
                            }
                        }
                        catch (Exception ex) when(!ex.IsFatal())
                        {
                            syncingSuccess = false;
                            _logger.LogError(ex, "Unable to read the distributed cache while syncing the tenant '{TenantName}'.", settings.Name);
                            break;
                        }
                        finally
                        {
                            semaphore.Release();
                        }
                    }

                    // Keep in sync the tenant global identifiers.
                    if (syncingSuccess)
                    {
                        _shellChangedId = shellChangedId;
                        _shellCreatedId = shellCreatedId;
                    }
                }
                catch (Exception ex) when(!ex.IsFatal())
                {
                    _logger.LogError(ex, "Error while executing '{ServiceName}'", nameof(DistributedShellHostedService));
                }
            }

            _terminated = true;
            _context?.Release();
            _defaultContext = null;
            _context        = null;
        }
Beispiel #7
0
        /// <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)
            {
                foreach (var d in ReloadingAsync.GetInvocationList())
                {
                    await((ShellEvent)d)(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 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)
                {
                    return;
                }
            }

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