/// <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."); }
/// <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); }); } }
/// <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); }
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; }
/// <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."); }