Skip to content

Commit

Permalink
Add IAsyncComponent to allow async initialize/terminate (#16536)
Browse files Browse the repository at this point in the history
* Add IAsyncComponent

* Rewrite to use IAsyncComposer

* Add AsyncComponentBase and RuntimeAsyncComponentBase

* Remove manual disposing of components on restart
  • Loading branch information
ronaldbarendse authored Sep 23, 2024
1 parent 2270db6 commit cf6137d
Show file tree
Hide file tree
Showing 13 changed files with 221 additions and 100 deletions.
55 changes: 55 additions & 0 deletions src/Umbraco.Core/Composing/AsyncComponentBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
namespace Umbraco.Cms.Core.Composing;

/// <inheritdoc />
/// <remarks>
/// By default, the component will not execute if Umbraco is restarting.
/// </remarks>
public abstract class AsyncComponentBase : IAsyncComponent
{
/// <inheritdoc />
public async Task InitializeAsync(bool isRestarting, CancellationToken cancellationToken)
{
if (CanExecute(isRestarting))
{
await InitializeAsync(cancellationToken).ConfigureAwait(false);
}
}

/// <inheritdoc />
public async Task TerminateAsync(bool isRestarting, CancellationToken cancellationToken)
{
if (CanExecute(isRestarting))
{
await TerminateAsync(cancellationToken).ConfigureAwait(false);
}
}

/// <summary>
/// Determines whether the component can execute.
/// </summary>
/// <param name="isRestarting">If set to <c>true</c> indicates Umbraco is restarting.</param>
/// <returns>
/// <c>true</c> if the component can execute; otherwise, <c>false</c>.
/// </returns>
protected virtual bool CanExecute(bool isRestarting)
=> isRestarting is false;

/// <summary>
/// Initializes the component.
/// </summary>
/// <param name="cancellationToken">The cancellation token. Cancellation indicates that the start process has been aborted.</param>
/// <returns>
/// A <see cref="Task" /> representing the asynchronous operation.
/// </returns>
protected abstract Task InitializeAsync(CancellationToken cancellationToken);

/// <summary>
/// Terminates the component.
/// </summary>
/// <param name="cancellationToken">The cancellation token. Cancellation indicates that the shutdown process should no longer be graceful.</param>
/// <returns>
/// A <see cref="Task" /> representing the asynchronous operation.
/// </returns>
protected virtual Task TerminateAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
}
47 changes: 24 additions & 23 deletions src/Umbraco.Core/Composing/ComponentCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,59 +5,60 @@
namespace Umbraco.Cms.Core.Composing;

/// <summary>
/// Represents the collection of <see cref="IComponent" /> implementations.
/// Represents the collection of <see cref="IAsyncComponent" /> implementations.
/// </summary>
public class ComponentCollection : BuilderCollectionBase<IComponent>
public class ComponentCollection : BuilderCollectionBase<IAsyncComponent>
{
private const int LogThresholdMilliseconds = 100;
private readonly ILogger<ComponentCollection> _logger;

private readonly IProfilingLogger _profilingLogger;
private readonly ILogger<ComponentCollection> _logger;

public ComponentCollection(Func<IEnumerable<IComponent>> items, IProfilingLogger profilingLogger, ILogger<ComponentCollection> logger)
public ComponentCollection(Func<IEnumerable<IAsyncComponent>> items, IProfilingLogger profilingLogger, ILogger<ComponentCollection> logger)
: base(items)
{
_profilingLogger = profilingLogger;
_logger = logger;
}

public void Initialize()
public async Task InitializeAsync(bool isRestarting, CancellationToken cancellationToken)
{
using (!_profilingLogger.IsEnabled(Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration<ComponentCollection>(
$"Initializing. (log components when >{LogThresholdMilliseconds}ms)", "Initialized."))
using (_profilingLogger.IsEnabled(Logging.LogLevel.Debug) is false
? null
: _profilingLogger.DebugDuration<ComponentCollection>($"Initializing. (log components when >{LogThresholdMilliseconds}ms)", "Initialized."))
{
foreach (IComponent component in this)
foreach (IAsyncComponent component in this)
{
Type componentType = component.GetType();
using (!_profilingLogger.IsEnabled(Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration<ComponentCollection>(
$"Initializing {componentType.FullName}.",
$"Initialized {componentType.FullName}.",
thresholdMilliseconds: LogThresholdMilliseconds))

using (_profilingLogger.IsEnabled(Logging.LogLevel.Debug) is false
? null :
_profilingLogger.DebugDuration<ComponentCollection>($"Initializing {componentType.FullName}.", $"Initialized {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
{
component.Initialize();
await component.InitializeAsync(isRestarting, cancellationToken);
}
}
}
}

public void Terminate()
public async Task TerminateAsync(bool isRestarting, CancellationToken cancellationToken)
{
using (!_profilingLogger.IsEnabled(Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration<ComponentCollection>(
$"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated."))
using (!_profilingLogger.IsEnabled(Logging.LogLevel.Debug)
? null
: _profilingLogger.DebugDuration<ComponentCollection>($"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated."))
{
// terminate components in reverse order
foreach (IComponent component in this.Reverse())
foreach (IAsyncComponent component in this.Reverse())
{
Type componentType = component.GetType();
using (!_profilingLogger.IsEnabled(Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration<ComponentCollection>(
$"Terminating {componentType.FullName}.",
$"Terminated {componentType.FullName}.",
thresholdMilliseconds: LogThresholdMilliseconds))

using (_profilingLogger.IsEnabled(Logging.LogLevel.Debug) is false
? null
: _profilingLogger.DebugDuration<ComponentCollection>($"Terminating {componentType.FullName}.", $"Terminated {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
{
try
{
component.Terminate();
component.DisposeIfDisposable();
await component.TerminateAsync(isRestarting, cancellationToken);
}
catch (Exception ex)
{
Expand Down
22 changes: 10 additions & 12 deletions src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,33 @@
namespace Umbraco.Cms.Core.Composing;

/// <summary>
/// Builds a <see cref="ComponentCollection" />.
/// Builds a <see cref="ComponentCollection" />.
/// </summary>
public class
ComponentCollectionBuilder : OrderedCollectionBuilderBase<ComponentCollectionBuilder, ComponentCollection,
IComponent>
public class ComponentCollectionBuilder : OrderedCollectionBuilderBase<ComponentCollectionBuilder, ComponentCollection, IAsyncComponent>
{
private const int LogThresholdMilliseconds = 100;

protected override ComponentCollectionBuilder This => this;

protected override IEnumerable<IComponent> CreateItems(IServiceProvider factory)
protected override IEnumerable<IAsyncComponent> CreateItems(IServiceProvider factory)
{
IProfilingLogger logger = factory.GetRequiredService<IProfilingLogger>();

using (!logger.IsEnabled(Logging.LogLevel.Debug) ? null : logger.DebugDuration<ComponentCollectionBuilder>(
$"Creating components. (log when >{LogThresholdMilliseconds}ms)", "Created."))
using (logger.IsEnabled(Logging.LogLevel.Debug) is false
? null
: logger.DebugDuration<ComponentCollectionBuilder>($"Creating components. (log when >{LogThresholdMilliseconds}ms)", "Created."))
{
return base.CreateItems(factory);
}
}

protected override IComponent CreateItem(IServiceProvider factory, Type itemType)
protected override IAsyncComponent CreateItem(IServiceProvider factory, Type itemType)
{
IProfilingLogger logger = factory.GetRequiredService<IProfilingLogger>();

using (!logger.IsEnabled(Logging.LogLevel.Debug) ? null : logger.DebugDuration<ComponentCollectionBuilder>(
$"Creating {itemType.FullName}.",
$"Created {itemType.FullName}.",
thresholdMilliseconds: LogThresholdMilliseconds))
using (logger.IsEnabled(Logging.LogLevel.Debug) is false
? null :
logger.DebugDuration<ComponentCollectionBuilder>($"Creating {itemType.FullName}.", $"Created {itemType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
{
return base.CreateItem(factory, itemType);
}
Expand Down
23 changes: 14 additions & 9 deletions src/Umbraco.Core/Composing/ComponentComposer.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;

namespace Umbraco.Cms.Core.Composing;

/// <summary>
/// Provides a base class for composers which compose a component.
/// Provides a composer that appends a component.
/// </summary>
/// <typeparam name="TComponent">The type of the component</typeparam>
/// <typeparam name="TComponent">The type of the component.</typeparam>
/// <remarks>
/// Thanks to this class, a component that does not compose anything can be registered with one line:
/// <code>
/// <![CDATA[
/// public class MyComponentComposer : ComponentComposer<MyComponent> { }
/// ]]>
/// </code>
/// </remarks>
public abstract class ComponentComposer<TComponent> : IComposer
where TComponent : IComponent
where TComponent : IAsyncComponent
{
/// <inheritdoc />
public virtual void Compose(IUmbracoBuilder builder) => builder.Components().Append<TComponent>();

// note: thanks to this class, a component that does not compose anything can be
// registered with one line:
// public class MyComponentComposer : ComponentComposer<MyComponent> { }
public virtual void Compose(IUmbracoBuilder builder)
=> builder.Components().Append<TComponent>();
}
35 changes: 35 additions & 0 deletions src/Umbraco.Core/Composing/IAsyncComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace Umbraco.Cms.Core.Composing;

/// <summary>
/// Represents a component.
/// </summary>
/// <remarks>
/// <para>
/// Components are created by DI and therefore must have a public constructor.
/// </para>
/// <para>
/// All components are terminated in reverse order when Umbraco terminates, and disposable components are disposed.
/// </para>
/// </remarks>
public interface IAsyncComponent
{
/// <summary>
/// Initializes the component.
/// </summary>
/// <param name="isRestarting">If set to <c>true</c> indicates Umbraco is restarting.</param>
/// <param name="cancellationToken">The cancellation token. Cancellation indicates that the start process has been aborted.</param>
/// <returns>
/// A <see cref="Task" /> representing the asynchronous operation.
/// </returns>
Task InitializeAsync(bool isRestarting, CancellationToken cancellationToken);

/// <summary>
/// Terminates the component.
/// </summary>
/// <param name="isRestarting">If set to <c>true</c> indicates Umbraco is restarting.</param>
/// <param name="cancellationToken">The cancellation token. Cancellation indicates that the shutdown process should no longer be graceful.</param>
/// <returns>
/// A <see cref="Task" /> representing the asynchronous operation.
/// </returns>
Task TerminateAsync(bool isRestarting, CancellationToken cancellationToken);
}
38 changes: 21 additions & 17 deletions src/Umbraco.Core/Composing/IComponent.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
namespace Umbraco.Cms.Core.Composing;

/// <summary>
/// Represents a component.
/// </summary>
/// <remarks>
/// <para>Components are created by DI and therefore must have a public constructor.</para>
/// <para>
/// All components are terminated in reverse order when Umbraco terminates, and
/// disposable components are disposed.
/// </para>
/// <para>
/// The Dispose method may be invoked more than once, and components
/// should ensure they support this.
/// </para>
/// </remarks>
public interface IComponent
/// <inheritdoc />
[Obsolete("Use IAsyncComponent instead. This interface will be removed in a future version.")]
public interface IComponent : IAsyncComponent
{
/// <summary>
/// Initializes the component.
/// Initializes the component.
/// </summary>
void Initialize();

/// <summary>
/// Terminates the component.
/// Terminates the component.
/// </summary>
void Terminate();

/// <inheritdoc />
Task IAsyncComponent.InitializeAsync(bool isRestarting, CancellationToken cancellationToken)
{
Initialize();

return Task.CompletedTask;
}

/// <inheritdoc />
Task IAsyncComponent.TerminateAsync(bool isRestarting, CancellationToken cancellationToken)
{
Terminate();

return Task.CompletedTask;
}
}
23 changes: 23 additions & 0 deletions src/Umbraco.Core/Composing/RuntimeAsyncComponentBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Umbraco.Cms.Core.Services;

namespace Umbraco.Cms.Core.Composing;

/// <inheritdoc />
/// <remarks>
/// By default, the component will not execute if Umbraco is restarting or the runtime level is not <see cref="RuntimeLevel.Run" />.
/// </remarks>
public abstract class RuntimeAsyncComponentBase : AsyncComponentBase
{
private readonly IRuntimeState _runtimeState;

/// <summary>
/// Initializes a new instance of the <see cref="RuntimeAsyncComponentBase" /> class.
/// </summary>
/// <param name="runtimeState">State of the Umbraco runtime.</param>
protected RuntimeAsyncComponentBase(IRuntimeState runtimeState)
=> _runtimeState = runtimeState;

/// <inheritdoc />
protected override bool CanExecute(bool isRestarting)
=> base.CanExecute(isRestarting) && _runtimeState.Level == RuntimeLevel.Run;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static partial class UmbracoBuilderExtensions
/// The builder.
/// </returns>
public static IUmbracoBuilder AddComponent<T>(this IUmbracoBuilder builder)
where T : IComponent
where T : IAsyncComponent
{
builder.Components().Append<T>();

Expand Down
3 changes: 2 additions & 1 deletion src/Umbraco.Core/Extensions/ObjectExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ public static class ObjectExtensions
public static IEnumerable<T> AsEnumerableOfOne<T>(this T input) => Enumerable.Repeat(input, 1);

/// <summary>
/// Disposes the object if it implements <see cref="IDisposable" />.
/// </summary>
/// <param name="input"></param>
/// <param name="input">The object.</param>
public static void DisposeIfDisposable(this object input)
{
if (input is IDisposable disposable)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
namespace Umbraco.Cms.Core.Notifications;

/// <summary>
/// Notification that occurs at the very end of the Umbraco boot process (after all <see cref="Composing.IComponent" />s are
/// initialized).
/// Notification that occurs at the very end of the Umbraco boot process (after all components are initialized).
/// </summary>
public class UmbracoApplicationStartingNotification : IUmbracoApplicationLifetimeNotification
{
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoApplicationStartingNotification" /> class.
/// </summary>
/// <seealso cref="IUmbracoApplicationLifetimeNotification" />
public class UmbracoApplicationStartingNotification : IUmbracoApplicationLifetimeNotification
/// <param name="runtimeLevel">The runtime level</param>
/// <param name="isRestarting">Indicates whether Umbraco is restarting.</param>
public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel, bool isRestarting)
{
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoApplicationStartingNotification" /> class.
/// </summary>
/// <param name="runtimeLevel">The runtime level</param>
/// <param name="isRestarting">Indicates whether Umbraco is restarting.</param>
public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel, bool isRestarting)
{
RuntimeLevel = runtimeLevel;
IsRestarting = isRestarting;
}
RuntimeLevel = runtimeLevel;
IsRestarting = isRestarting;
}

/// <summary>
/// Gets the runtime level.
/// Gets the runtime level.
/// </summary>
/// <value>
/// The runtime level.
/// The runtime level.
/// </value>
public RuntimeLevel RuntimeLevel { get; }

Expand Down
Loading

0 comments on commit cf6137d

Please sign in to comment.