Skip to content

Commit

Permalink
IInitializerTimeout and message to ErrorWithObject overload (#83)
Browse files Browse the repository at this point in the history
Timeout for IInitializer and overload with message for ErrorWithObject
  • Loading branch information
muphblu authored Jul 8, 2024
1 parent 0904bc9 commit fceb627
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 58 deletions.
11 changes: 5 additions & 6 deletions ATI.Services.Common/Initializers/InitializeOrderAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
using System;

namespace ATI.Services.Common.Initializers
namespace ATI.Services.Common.Initializers;

[AttributeUsage(AttributeTargets.Class)]
public class InitializeOrderAttribute : Attribute
{
[AttributeUsage(AttributeTargets.Class)]
public class InitializeOrderAttribute : Attribute
{
public InitializeOrder Order { get; set; }
}
public InitializeOrder Order { get; set; }
}
30 changes: 30 additions & 0 deletions ATI.Services.Common/Initializers/InitializeTimeoutAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;

namespace ATI.Services.Common.Initializers;

/// <summary>
/// Attribute for setting initialization timeout and behavior in case of timeout
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class InitializeTimeoutAttribute : Attribute
{
/// <summary>
/// Total initialization timeout in seconds
/// When set, enables timeout for initialization,
/// in case of Timeout and Required = true, application will not start
/// in case of Timeout and Required = false, application will start without waiting for initialization if this service
/// Default value is 10 seconds
/// </summary>
public int InitTimeoutSec { get; set; } = 10;

/// <summary>
/// Number of retries on exception
/// </summary>
public int Retry { get; set; } = 0;

/// <summary>
/// Is initialization required
/// When true, application will not start if initialization failed due to timeout or exception
/// </summary>
public bool Required { get; set; } = false;
}
43 changes: 21 additions & 22 deletions ATI.Services.Common/Initializers/Interfaces/IInitializer.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
using System.Threading.Tasks;
using ATI.Services.Common.Variables;

namespace ATI.Services.Common.Initializers.Interfaces
{
/// <summary>
/// Интерфейс который маркирует объект, как требующий инициализации на старте приложения
/// используется классом <see cref="StartupInitializer"/>
/// для задания порядка инициализации используйте аттрибут <see cref="InitializeOrderAttribute"/>
/// Имеющийся порядок на данный момент:
/// ATI.Services.Authorization.AuthorizationInitializer - InitializeOrder.First
/// <see cref="ServiceVariablesInitializer"/> - InitializeOrder.First
/// <see cref="MetricsInitializer"/> - InitializeOrder.First
/// <see cref="RedisInitializer"/> - InitializeOrder.Third
/// <see cref="TwoLevelCacheInitializer"/> - InitializeOrder.Third
/// <see cref="Caching.LocalCache.LocalCache{T}"/> - InitializeOrder.Fourth
/// ATI.Services.Consul.ConsulInitializer - InitializeOrder.Sixth
/// </summary>
public interface IInitializer
{
Task InitializeAsync();
string InitStartConsoleMessage();
string InitEndConsoleMessage();

}
namespace ATI.Services.Common.Initializers.Interfaces;

/// <summary>
/// Интерфейс который маркирует объект, как требующий инициализации на старте приложения
/// используется классом <see cref="StartupInitializer"/>
/// <para>Для управления временем и поведением инициацлизации используйте <see cref="InitializeTimeoutAttribute"/></para>
/// для задания порядка инициализации используйте аттрибут <see cref="InitializeOrderAttribute"/>
/// Имеющийся порядок на данный момент:
/// ATI.Services.Authorization.AuthorizationInitializer - InitializeOrder.First
/// <see cref="ServiceVariablesInitializer"/> - InitializeOrder.First
/// <see cref="MetricsInitializer"/> - InitializeOrder.First
/// <see cref="RedisInitializer"/> - InitializeOrder.Third
/// <see cref="TwoLevelCacheInitializer"/> - InitializeOrder.Third
/// <see cref="Caching.LocalCache.LocalCache{T}"/> - InitializeOrder.Fourth
/// ATI.Services.Consul.ConsulInitializer - InitializeOrder.Sixth
/// </summary>
public interface IInitializer
{
Task InitializeAsync();
string InitStartConsoleMessage();
string InitEndConsoleMessage();
}
103 changes: 75 additions & 28 deletions ATI.Services.Common/Initializers/StartupInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,93 @@
using System.Linq;
using System.Threading.Tasks;
using ATI.Services.Common.Initializers.Interfaces;
using ATI.Services.Common.Logging;
using JetBrains.Annotations;
using NLog;
using Polly;

namespace ATI.Services.Common.Initializers
namespace ATI.Services.Common.Initializers;

public class StartupInitializer(IServiceProvider serviceProvider)
{
public class StartupInitializer
private readonly ILogger _logger = LogManager.GetCurrentClassLogger();

[UsedImplicitly]
public async Task InitializeAsync()
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger = LogManager.GetCurrentClassLogger();
var initializers =
AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.Where(type => !type.IsInterface && typeof(IInitializer).IsAssignableFrom(type))
.Select(initializer =>
new
{
InitializerType = initializer,
Order = (initializer.GetCustomAttributes(typeof(InitializeOrderAttribute), true)
.FirstOrDefault() as InitializeOrderAttribute)?.Order ??
InitializeOrder.Last
}
)
.ToList();

public StartupInitializer(IServiceProvider serviceProvider)
foreach (var initializerInfo in initializers.OrderBy(i => i.Order))
{
_serviceProvider = serviceProvider;
}
if (serviceProvider.GetService(initializerInfo.InitializerType) is not IInitializer initializer)
continue;

[UsedImplicitly]
public async Task InitializeAsync()
{
var initializers = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.Where(type => !type.IsInterface && typeof(IInitializer).IsAssignableFrom(type))
.Select(initializer =>
new
{
InitializerType = initializer,
Order = (initializer.GetCustomAttributes(typeof(InitializeOrderAttribute), true)
.FirstOrDefault() as InitializeOrderAttribute)?.Order ?? InitializeOrder.Last
}
)
.ToList();

foreach (var initializerInfo in initializers.OrderBy(i => i.Order))
Console.WriteLine(initializer.InitStartConsoleMessage());
var initializerName = initializer.GetType().Name;

if (initializer.GetType().GetCustomAttributes(typeof(InitializeTimeoutAttribute), false).FirstOrDefault()
is InitializeTimeoutAttribute initTimeoutAttribute)
{
if (_serviceProvider.GetService(initializerInfo.InitializerType) is IInitializer initializer)
await InitWithPolicy(initTimeoutAttribute, initializerName, () => initializer.InitializeAsync());
}
else
{
try
{
Console.WriteLine(initializer.InitStartConsoleMessage());
await initializer.InitializeAsync();
Console.WriteLine(initializer.InitEndConsoleMessage());
_logger.Trace($"{initializer.GetType()} initialized");
}
catch (Exception e)
{
_logger.ErrorWithObject(e, $"Exception during initializer {initializerName}");
}
}

Console.WriteLine(initializer.InitEndConsoleMessage());
_logger.Trace($"{initializerName} initialized");
}

return;

async Task InitWithPolicy(InitializeTimeoutAttribute initTimeoutAttribute,
string initializerName,
Func<Task> init)
{
var initPolicy =
Policy.WrapAsync(Policy.TimeoutAsync(TimeSpan.FromSeconds(initTimeoutAttribute.InitTimeoutSec)),
Policy.Handle<Exception>().WaitAndRetryAsync(
initTimeoutAttribute.Retry,
i => TimeSpan.FromMilliseconds(Math.Min(200 * i, 2000)),
(exception, _, i, _) => _logger.Warn(exception, $"Retry number:{i} for initializer {initializerName}")));

var policyResult = await initPolicy.ExecuteAndCaptureAsync(init);
if(policyResult.Outcome is OutcomeType.Successful)
return;

if (initTimeoutAttribute.Required)
{
var message = $"Required initializer {initializerName} failed with {policyResult.FinalException?.Message}, application will not start.";
Console.WriteLine(message);
_logger.Error(policyResult.FinalException, message);
throw policyResult.FinalException!;
}
else
{
var message = $"Required initializer {initializerName} failed with {policyResult.FinalException?.Message}, continue in background.";
Console.WriteLine(message);
_logger.Warn(message);
}
}
}
Expand Down
9 changes: 7 additions & 2 deletions ATI.Services.Common/Logging/LoggerExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ namespace ATI.Services.Common.Logging;
[PublicAPI]
public static class LoggerExtension
{
public static void ErrorWithObject(this ILogger logger, Exception ex, string message,
params object[] logObjects)
public static void ErrorWithObject(this ILogger logger, Exception ex, string message, params object[] logObjects)
{
logger.LogWithObject(LogLevel.Error, ex, message, logObjects: logObjects);
}
Expand All @@ -23,6 +22,12 @@ public static void ErrorWithObject(this ILogger logger, Exception ex, params obj
logger.LogWithObject(LogLevel.Error, ex, logObjects: logObjects);
}

public static void ErrorWithObject(this ILogger logger, string message, params object[] logObjects)
{
logger.LogWithObject(LogLevel.Error, message: message, logObjects: logObjects);
}

[Obsolete("Use ErrorWithObject(this ILogger logger, string message, params object[] logObjects) instead")]
public static void ErrorWithObject(this ILogger logger, params object[] logObjects)
{
logger.LogWithObject(LogLevel.Error, logObjects: logObjects);
Expand Down

0 comments on commit fceb627

Please sign in to comment.