diff --git a/src/Umbraco.Core/FireAndForgetRunner.cs b/src/Umbraco.Core/FireAndForgetRunner.cs new file mode 100644 index 0000000000..8c466bd439 --- /dev/null +++ b/src/Umbraco.Core/FireAndForgetRunner.cs @@ -0,0 +1,41 @@ +using Microsoft.Extensions.Logging; + +namespace Umbraco.Cms.Core; + + +public class FireAndForgetRunner : IFireAndForgetRunner +{ + private readonly ILogger _logger; + + public FireAndForgetRunner(ILogger logger) => _logger = logger; + + public void RunFireAndForget(Func task) => ExecuteBackgroundTask(task); + + private Task ExecuteBackgroundTask(Func fn) + { + // it is also possible to use UnsafeQueueUserWorkItem which does not flow the execution context, + // however that seems more difficult to use for async operations. + + // Do not flow AsyncLocal to the child thread + using (ExecutionContext.SuppressFlow()) + { + // NOTE: ConfigureAwait(false) is irrelevant here, it is not needed because this is not being + // awaited. ConfigureAwait(false) is only relevant when awaiting to prevent the SynchronizationContext + // (very different from the ExecutionContext!) from running the continuation on the calling thread. + return Task.Run(LoggingWrapper(fn)); + } + } + + private Func LoggingWrapper(Func fn) => + async () => + { + try + { + await fn(); + } + catch (Exception e) + { + _logger.LogError(e, "Exception thrown in a background thread"); + } + }; +} diff --git a/src/Umbraco.Core/IFireAndForgetRunner.cs b/src/Umbraco.Core/IFireAndForgetRunner.cs new file mode 100644 index 0000000000..b28a777990 --- /dev/null +++ b/src/Umbraco.Core/IFireAndForgetRunner.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core; + +public interface IFireAndForgetRunner +{ + void RunFireAndForget(Func task); +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 1c874b2efa..154bae9cd0 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -219,6 +219,8 @@ public static partial class UmbracoBuilderExtensions // Services required to run background jobs (with out the handler) builder.Services.AddSingleton(); + + builder.Services.AddTransient(); return builder; } diff --git a/src/Umbraco.Infrastructure/Install/InstallHelper.cs b/src/Umbraco.Infrastructure/Install/InstallHelper.cs index 41e9d13ed8..1cec345110 100644 --- a/src/Umbraco.Infrastructure/Install/InstallHelper.cs +++ b/src/Umbraco.Infrastructure/Install/InstallHelper.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -9,6 +10,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -24,8 +26,35 @@ namespace Umbraco.Cms.Infrastructure.Install private readonly ICookieManager _cookieManager; private readonly IUserAgentProvider _userAgentProvider; private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; + private readonly IFireAndForgetRunner _fireAndForgetRunner; private InstallationType? _installationType; + public InstallHelper( + DatabaseBuilder databaseBuilder, + ILogger logger, + IUmbracoVersion umbracoVersion, + IOptionsMonitor connectionStrings, + IInstallationService installationService, + ICookieManager cookieManager, + IUserAgentProvider userAgentProvider, + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IFireAndForgetRunner fireAndForgetRunner) + { + _logger = logger; + _umbracoVersion = umbracoVersion; + _databaseBuilder = databaseBuilder; + _connectionStrings = connectionStrings; + _installationService = installationService; + _cookieManager = cookieManager; + _userAgentProvider = userAgentProvider; + _umbracoDatabaseFactory = umbracoDatabaseFactory; + _fireAndForgetRunner = fireAndForgetRunner; + + // We need to initialize the type already, as we can't detect later, if the connection string is added on the fly. + GetInstallationType(); + } + + [Obsolete("Please use constructor that takes an IFireAndForgetRunner instead, scheduled for removal in Umbraco 12")] public InstallHelper( DatabaseBuilder databaseBuilder, ILogger logger, @@ -35,6 +64,16 @@ namespace Umbraco.Cms.Infrastructure.Install ICookieManager cookieManager, IUserAgentProvider userAgentProvider, IUmbracoDatabaseFactory umbracoDatabaseFactory) + : this( + databaseBuilder, + logger, + umbracoVersion, + connectionStrings, + installationService, + cookieManager, + userAgentProvider, + umbracoDatabaseFactory, + StaticServiceProvider.Instance.GetRequiredService()) { _logger = logger; _umbracoVersion = umbracoVersion; @@ -87,7 +126,7 @@ namespace Umbraco.Cms.Infrastructure.Install userAgent: userAgent, dbProvider: dbProvider); - await _installationService.LogInstall(installLog); + _fireAndForgetRunner.RunFireAndForget(() => _installationService.LogInstall(installLog)); } catch (Exception ex) {