private static async Task FlushAsync(ShellScope scope, IEnumerable <IndexingTask> tasks) { var localQueue = new List <IndexingTask>(tasks); var serviceProvider = scope.ServiceProvider; var dbConnectionAccessor = serviceProvider.GetService <IDbConnectionAccessor>(); var shellSettings = serviceProvider.GetService <ShellSettings>(); var logger = serviceProvider.GetService <ILogger <IndexingTaskManager> >(); var tablePrefix = shellSettings["TablePrefix"]; if (!String.IsNullOrEmpty(tablePrefix)) { tablePrefix += '_'; } var contentItemIds = new HashSet <string>(); // Remove duplicate tasks, only keep the last one for (var i = localQueue.Count; i > 0; i--) { var task = localQueue[i - 1]; if (contentItemIds.Contains(task.ContentItemId)) { localQueue.RemoveAt(i - 1); } else { contentItemIds.Add(task.ContentItemId); } } // At this point, content items ids should be unique in localQueue var ids = localQueue.Select(x => x.ContentItemId).ToArray(); var table = $"{tablePrefix}{nameof(IndexingTask)}"; using (var connection = dbConnectionAccessor.CreateConnection()) { await connection.OpenAsync(); using (var transaction = connection.BeginTransaction()) { var dialect = SqlDialectFactory.For(transaction.Connection); try { if (logger.IsEnabled(LogLevel.Debug)) { logger.LogDebug($"Updating indexing tasks: {String.Join(", ", tasks.Select(x => x.ContentItemId))}"); } // Page delete statements to prevent the limits from IN sql statements var pageSize = 100; var deleteCmd = $"delete from {dialect.QuoteForTableName(table)} where {dialect.QuoteForColumnName("ContentItemId")} {dialect.InOperator("@Ids")};"; do { var pageOfIds = ids.Take(pageSize).ToArray(); if (pageOfIds.Any()) { await transaction.Connection.ExecuteAsync(deleteCmd, new { Ids = pageOfIds }, transaction); ids = ids.Skip(pageSize).ToArray(); } } while (ids.Any()); var insertCmd = $"insert into {dialect.QuoteForTableName(table)} ({dialect.QuoteForColumnName("CreatedUtc")}, {dialect.QuoteForColumnName("ContentItemId")}, {dialect.QuoteForColumnName("Type")}) values (@CreatedUtc, @ContentItemId, @Type);"; await transaction.Connection.ExecuteAsync(insertCmd, localQueue, transaction); transaction.Commit(); } catch (Exception e) { transaction.Rollback(); logger.LogError(e, "An error occurred while updating indexing tasks"); throw; } } } }
/// <summary> /// Adds tenant level data access services. /// </summary> public static OrchardCoreBuilder AddDataAccess(this OrchardCoreBuilder builder) { builder.ConfigureServices(services => { services.AddScoped <IDataMigrationManager, DataMigrationManager>(); services.AddScoped <IModularTenantEvents, AutomaticDataMigrations>(); // Adding supported databases services.TryAddDataProvider(name: "Sql Server", value: "SqlConnection", hasConnectionString: true, sampleConnectionString: "Server=localhost;Database=Orchard;User Id=username;Password=password", hasTablePrefix: true, isDefault: false); services.TryAddDataProvider(name: "Sqlite", value: "Sqlite", hasConnectionString: false, hasTablePrefix: false, isDefault: true); services.TryAddDataProvider(name: "MySql", value: "MySql", hasConnectionString: true, sampleConnectionString: "Server=localhost;Database=Orchard;Uid=username;Pwd=password", hasTablePrefix: true, isDefault: false); services.TryAddDataProvider(name: "Postgres", value: "Postgres", hasConnectionString: true, sampleConnectionString: "Server=localhost;Port=5432;Database=Orchard;User Id=username;Password=password", hasTablePrefix: true, isDefault: false); // Configuring data access services.AddSingleton <IStore>(sp => { var shellSettings = sp.GetService <ShellSettings>(); // Before the setup a 'DatabaseProvider' may be configured without a required 'ConnectionString'. if (shellSettings.State == TenantState.Uninitialized || shellSettings["DatabaseProvider"] == null) { return(null); } IConfiguration storeConfiguration = new YesSql.Configuration(); switch (shellSettings["DatabaseProvider"]) { case "SqlConnection": storeConfiguration .UseSqlServer(shellSettings["ConnectionString"], IsolationLevel.ReadUncommitted) .UseBlockIdGenerator(); break; case "Sqlite": var shellOptions = sp.GetService <IOptions <ShellOptions> >(); var option = shellOptions.Value; var databaseFolder = Path.Combine(option.ShellsApplicationDataPath, option.ShellsContainerName, shellSettings.Name); var databaseFile = Path.Combine(databaseFolder, "yessql.db"); Directory.CreateDirectory(databaseFolder); storeConfiguration .UseSqLite($"Data Source={databaseFile};Cache=Shared", IsolationLevel.ReadUncommitted) .UseDefaultIdGenerator(); break; case "MySql": storeConfiguration .UseMySql(shellSettings["ConnectionString"], IsolationLevel.ReadUncommitted) .UseBlockIdGenerator(); break; case "Postgres": storeConfiguration .UsePostgreSql(shellSettings["ConnectionString"], IsolationLevel.ReadUncommitted) .UseBlockIdGenerator(); break; default: throw new ArgumentException("Unknown database provider: " + shellSettings["DatabaseProvider"]); } if (!string.IsNullOrWhiteSpace(shellSettings["TablePrefix"])) { storeConfiguration = storeConfiguration.SetTablePrefix(shellSettings["TablePrefix"] + "_"); } var store = StoreFactory.CreateAsync(storeConfiguration).GetAwaiter().GetResult(); var indexes = sp.GetServices <IIndexProvider>(); store.RegisterIndexes(indexes); return(store); }); services.AddScoped(sp => { var store = sp.GetService <IStore>(); if (store == null) { return(null); } var session = store.CreateSession(); var scopedServices = sp.GetServices <IScopedIndexProvider>(); session.RegisterIndexes(scopedServices.ToArray()); ShellScope.RegisterBeforeDispose(scope => { return(session.CommitAsync()); }); return(session); }); services.AddTransient <IDbConnectionAccessor, DbConnectionAccessor>(); }); return(builder); }
internal void ReleaseInternal(ReleaseMode mode = ReleaseMode.Normal) { if (_released) { // Prevent infinite loops with circular dependencies return; } // A disabled shell still in use will be released by its last scope, as checked at the host level. if (mode == ReleaseMode.FromDependency && Settings.State == TenantState.Disabled && _refCount != 0) { return; } // When a tenant is changed and should be restarted, its shell context is replaced with a new one, // so that new request can't use it anymore. However some existing request might still be running and try to // resolve or use its services. We then call this method to count the remaining references and dispose it // when the number reached zero. ShellScope scope = null; lock (_synLock) { if (_released) { return; } if (_dependents != null) { foreach (var dependent in _dependents) { if (dependent.TryGetTarget(out var shellContext)) { shellContext.ReleaseFromDependency(); } } } if (mode != ReleaseMode.FromLastScope && ServiceProvider != null) { // Before marking the shell as released, we create a new scope that will manage the shell state, // so that we always use the same shell scope logic to check if the reference counter reached 0. scope = new ShellScope(this); } _released = true; } if (mode == ReleaseMode.FromLastScope) { return; } if (scope != null) { // Use this scope to manage the shell state as usual. scope.TerminateShellAsync().GetAwaiter().GetResult(); return; } Dispose(); }
public async Task UpdateAtomicAsync(Func <Task <TDocument> > updateAsync, Func <TDocument, Task> afterUpdateAsync = null) { if (_isDistributed) { try { _ = await _distributedCache.GetStringAsync(_options.CacheIdKey); } catch { await DocumentStore.CancelAsync(); throw new InvalidOperationException($"Can't update the '{typeof(TDocument).Name}' if not able to access the distributed cache"); } } var delegates = ShellScope.GetOrCreateFeature <UpdateDelegates>(); if (delegates.UpdateDelegateAsync == null || !delegates.UpdateDelegateAsync.GetInvocationList().Contains(updateAsync)) { delegates.UpdateDelegateAsync += () => updateAsync(); } if (afterUpdateAsync != null && (delegates.AfterUpdateDelegateAsync == null || !delegates.AfterUpdateDelegateAsync.GetInvocationList().Contains(afterUpdateAsync))) { delegates.AfterUpdateDelegateAsync += document => afterUpdateAsync(document); } DocumentStore.AfterCommitSuccess <TDocument>(async() => { (var locker, var locked) = await _distributedLock.TryAcquireLockAsync( _options.CacheKey + "_LOCK", TimeSpan.FromMilliseconds(_options.LockTimeout), TimeSpan.FromMilliseconds(_options.LockExpiration)); if (!locked) { return; } await using var acquiredLock = locker; TDocument document = null; foreach (var d in delegates.UpdateDelegateAsync.GetInvocationList()) { document = await((UpdateDelegate)d)(); } document.Identifier ??= IdGenerator.GenerateId(); await SetInternalAsync(document); if (delegates.AfterUpdateDelegateAsync != null) { foreach (var d in delegates.AfterUpdateDelegateAsync.GetInvocationList()) { await((AfterUpdateDelegate)d)(document); } } }); }
/// <summary> /// Adds a Signal (if not already present) to be sent at the end of the shell scope. /// </summary> public static void DeferredSignalToken(this ISignal signal, string key) { ShellScope.AddDeferredSignal(key); }
public async Task <(IEnumerable <IFeatureInfo>, IEnumerable <IFeatureInfo>)> UpdateFeaturesAsync(ShellDescriptor shellDescriptor, IEnumerable <IFeatureInfo> featuresToDisable, IEnumerable <IFeatureInfo> featuresToEnable, bool force) { var featureEventHandlers = ShellScope.Services.GetServices <IFeatureEventHandler>(); var enabledFeatureIds = _extensionManager.GetFeatures() .Where(f => shellDescriptor.Features.Any(sf => sf.Id == f.Id)) .Select(f => f.Id) .ToHashSet(); var installedFeatureIds = enabledFeatureIds .Concat(shellDescriptor.Installed.Select(sf => sf.Id)) .ToHashSet(); var alwaysEnabledIds = _alwaysEnabledFeatures.Select(sf => sf.Id).ToArray(); var allFeaturesToDisable = featuresToDisable .Where(f => !alwaysEnabledIds.Contains(f.Id)) .SelectMany(feature => GetFeaturesToDisable(feature, enabledFeatureIds, force)) .Distinct() .Reverse() .ToList(); foreach (var feature in allFeaturesToDisable) { enabledFeatureIds.Remove(feature.Id); if (_logger.IsEnabled(LogLevel.Information)) { _logger.LogInformation("Disabling feature '{FeatureName}'", feature.Id); } await featureEventHandlers.InvokeAsync((handler, featureInfo) => handler.DisablingAsync(featureInfo), feature, _logger); } var allFeaturesToEnable = featuresToEnable .SelectMany(feature => GetFeaturesToEnable(feature, enabledFeatureIds, force)) .Distinct() .ToList(); foreach (var feature in allFeaturesToEnable) { if (_logger.IsEnabled(LogLevel.Information)) { _logger.LogInformation("Enabling feature '{FeatureName}'", feature.Id); } await featureEventHandlers.InvokeAsync((handler, featureInfo) => handler.EnablingAsync(featureInfo), feature, _logger); } var allFeaturesToInstall = allFeaturesToEnable .Where(f => !installedFeatureIds.Contains(f.Id)) .ToList(); foreach (var feature in allFeaturesToInstall) { if (_logger.IsEnabled(LogLevel.Information)) { _logger.LogInformation("Installing feature '{FeatureName}'", feature.Id); } await featureEventHandlers.InvokeAsync((handler, featureInfo) => handler.InstallingAsync(featureInfo), feature, _logger); } if (allFeaturesToEnable.Count > 0) { enabledFeatureIds.UnionWith(allFeaturesToEnable.Select(f => f.Id)); } if (allFeaturesToDisable.Count > 0 || allFeaturesToEnable.Count > 0) { await _shellDescriptorManager.UpdateShellDescriptorAsync( shellDescriptor.SerialNumber, enabledFeatureIds.Select(id => new ShellFeature(id)).ToArray()); ShellScope.AddDeferredTask(async scope => { var featureEventHandlers = scope.ServiceProvider.GetServices <IFeatureEventHandler>(); var logger = scope.ServiceProvider.GetRequiredService <ILogger <ShellFeaturesManager> >(); foreach (var feature in allFeaturesToInstall) { if (logger.IsEnabled(LogLevel.Information)) { logger.LogInformation("Feature '{FeatureName}' was installed", feature.Id); } await featureEventHandlers.InvokeAsync((handler, featureInfo) => handler.InstalledAsync(featureInfo), feature, logger); } foreach (var feature in allFeaturesToEnable) { if (logger.IsEnabled(LogLevel.Information)) { logger.LogInformation("Feature '{FeatureName}' was enabled", feature.Id); } await featureEventHandlers.InvokeAsync((handler, featureInfo) => handler.EnabledAsync(featureInfo), feature, logger); } foreach (var feature in allFeaturesToDisable) { if (logger.IsEnabled(LogLevel.Information)) { logger.LogInformation("Feature '{FeatureName}' was disabled", feature.Id); } await featureEventHandlers.InvokeAsync((handler, featureInfo) => handler.DisabledAsync(featureInfo), feature, logger); } }); } return(allFeaturesToDisable, allFeaturesToEnable); }
public void AfterCommitSuccess <T>(DocumentStoreCommitSuccessDelegate afterCommit) => ShellScope.RegisterBeforeDispose(scope => afterCommit());
public BackgroundTaskEventContext(string name, ShellScope scope) { Name = name; Tenant = scope.ShellContext.Settings.Name; Services = scope.ServiceProvider; }