Skip to content

Commit

Permalink
Shell Pipeline in the Background (#14105)
Browse files Browse the repository at this point in the history
  • Loading branch information
jtkech authored Aug 18, 2023
1 parent 0d87a22 commit 1e350a8
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 90 deletions.
10 changes: 5 additions & 5 deletions src/OrchardCore.Cms.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,13 @@
//},
// By default background tasks are waiting for their shell to be lazily built on a first matching request.
//"OrchardCore_BackgroundService": {
// "ShellWarmup": true // Allows to eagerly build shells just before executing their first background task.
// "ShellWarmup": true // Allows to eagerly build shell containers just before executing their first background task.
//},
// Add 'AddOrchardCoreAzureKeyVault()' to the Generic Host in 'CreateHostBuilder() section'.
// "OrchardCore_KeyVault_Azure": {
// "KeyVaultName": "", // Set the name of your Azure Key Vault.
// "ReloadInterval": // Optional, timespan to wait between attempts at polling the Azure KeyVault for changes. Leave blank to disable reloading.
// },
//"OrchardCore_KeyVault_Azure": {
// "KeyVaultName": "", // Set the name of your Azure Key Vault.
// "ReloadInterval": // Optional, timespan to wait between attempts at polling the Azure KeyVault for changes. Leave blank to disable reloading.
//},
// See https://docs.orchardcore.net/en/latest/docs/reference/modules/Users/Configuration/#custom-paths
//"OrchardCore_Users": {
// "LoginPath": "Login",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ public async Task<IActionResult> Edit(string name)
Description = settings.Description,
LockTimeout = settings.LockTimeout,
LockExpiration = settings.LockExpiration,
UsePipeline = settings.UsePipeline,
};

return View(model);
Expand Down Expand Up @@ -201,6 +202,7 @@ public async Task<IActionResult> Edit(BackgroundTaskViewModel model)
settings.Description = model.Description;
settings.LockTimeout = model.LockTimeout;
settings.LockExpiration = model.LockExpiration;
settings.UsePipeline = model.UsePipeline;

await _backgroundTaskManager.UpdateAsync(model.Name, settings);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public class BackgroundTaskViewModel

public int LockExpiration { get; set; }

public bool UsePipeline { get; set; }

public bool IsAtomic => LockTimeout > 0 && LockExpiration > 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@
</div>
</div>

<div class="@Orchard.GetWrapperCssClasses()">
<div class="@Orchard.GetEndCssClasses(true)">
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UsePipeline" checked="@Model.UsePipeline" />
<label class="form-check-label" asp-for="UsePipeline">@T["Use Tenant Pipeline"]</label>
</div>
<span class="hint">@T["Wether or not the tenant pipeline should be built and then executed."]</span>
<span class="hint">@T["This to configure endpoints and then to allow route urls generation."]</span>
</div>
</div>

<div><span asp-validation-for="LockTimeout" class="text-danger">@T["Invalid lock timeout value in the advanced tab"]</span></div>
<div><span asp-validation-for="LockExpiration" class="text-danger">@T["Invalid lock expiration value in the advanced tab"]</span></div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,42 @@ namespace OrchardCore.BackgroundTasks
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class BackgroundTaskAttribute : Attribute
{
/// <summary>
/// The display name of this background task.
/// </summary>
public string Title { get; set; }

/// <summary>
/// Wether this background task is enabled or not.
/// </summary>
public bool Enable { get; set; } = true;

/// <summary>
/// The background task schedule as a cron expression.
/// </summary>
public string Schedule { get; set; } = "*/5 * * * *";

/// <summary>
/// The description of this background task.
/// </summary>
public string Description { get; set; } = String.Empty;

/// <summary>
/// The timeout in milliseconds to acquire a lock before executing the task atomically.
/// There is no locking if equal to zero or if there is no registered distributed lock.
/// </summary>
public int LockTimeout { get; set; }

/// <summary>
/// The expiration in milliseconds of the lock acquired before executing the task atomically.
/// There is no locking if equal to zero or if there is no registered distributed lock.
/// </summary>
public int LockExpiration { get; set; }

/// <summary>
/// Wether or not the tenant pipeline should be built and then executed.
/// This to configure endpoints and then to allow route urls generation.
/// </summary>
public bool UsePipeline { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static BackgroundTaskSettings GetDefaultSettings(this IBackgroundTask tas
Description = attribute.Description,
LockTimeout = attribute.LockTimeout,
LockExpiration = attribute.LockExpiration,
UsePipeline = attribute.UsePipeline,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,53 @@ namespace OrchardCore.BackgroundTasks
{
public class BackgroundTaskSettings
{
/// <summary>
/// The name of this background task.
/// </summary>
public string Name { get; set; } = String.Empty;

/// <summary>
/// The display name of this background task.
/// </summary>
public string Title { get; set; }

/// <summary>
/// Wether this background task is enabled or not.
/// </summary>
public bool Enable { get; set; } = true;

/// <summary>
/// The background task schedule as a cron expression.
/// </summary>
public string Schedule { get; set; } = "* * * * *";

/// <summary>
/// The description of this background task.
/// </summary>
public string Description { get; set; } = String.Empty;

/// <summary>
/// The timeout in milliseconds to acquire a lock before executing the task atomically.
/// There is no locking if equal to zero or if there is no registered distributed lock.
/// </summary>
public int LockTimeout { get; set; }

/// <summary>
/// The expiration in milliseconds of the lock acquired before executing the task atomically.
/// There is no locking if equal to zero or if there is no registered distributed lock.
/// </summary>
public int LockExpiration { get; set; }

/// <summary>
/// Wether or not the tenant pipeline should be built and then executed.
/// This to configure endpoints and then to allow route urls generation.
/// </summary>
public bool UsePipeline { get; set; }

/// <summary>
/// Wether this background task is atomic or not, wether it has or not
/// both a lock timeout and a lock expiration time greater than zero.
/// </summary>
public bool IsAtomic => LockTimeout > 0 && LockExpiration > 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ private static HttpContext CreateHttpContext(ShellSettings settings)
var urlHost = settings.RequestUrlHosts.FirstOrDefault();
context.Request.Host = new HostString(urlHost ?? _localhost);

context.Request.PathBase = PathString.Empty;
if (!String.IsNullOrWhiteSpace(settings.RequestUrlPrefix))
{
context.Request.PathBase = "/" + settings.RequestUrlPrefix;
context.Request.PathBase = $"/{settings.RequestUrlPrefix}";
}

context.Request.Path = "/";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static IApplicationBuilder UseOrchardCore(this IApplicationBuilder app, A

configure?.Invoke(app);

app.UseMiddleware<ModularTenantRouterMiddleware>(app.ServerFeatures);
app.UseMiddleware<ModularTenantRouterMiddleware>();

return app;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using OrchardCore.Environment.Shell;
Expand Down Expand Up @@ -100,6 +103,22 @@ public static OrchardCoreBuilder AddBackgroundService(this OrchardCoreBuilder bu
.Configure<IConfiguration>((options, config) => config
.Bind("OrchardCore:OrchardCore_BackgroundService", options));

builder.Configure(app =>
{
app.Use((context, next) =>
{
// In the background only the endpoints middlewares need to be executed.
if (context.Items.ContainsKey("IsBackground"))
{
// Shortcut the tenant pipeline.
return Task.CompletedTask;
}
return next(context);
});
},
order: Int32.MinValue);

return builder;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Builders;
using OrchardCore.Environment.Shell.Scope;

namespace OrchardCore.Modules;

public static class ShellPipelineExtensions
{
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphores = new();

/// <summary>
/// Builds the tenant pipeline atomically.
/// </summary>
public static async Task BuildPipelineAsync(this ShellContext context)
{
var semaphore = _semaphores.GetOrAdd(context.Settings.Name, _ => new SemaphoreSlim(1));

await semaphore.WaitAsync();
try
{
if (!context.HasPipeline())
{
context.Pipeline = context.BuildPipeline();
}
}
finally
{
semaphore.Release();
}
}

/// <summary>
/// Builds the tenant pipeline.
/// </summary>
private static IShellPipeline BuildPipeline(this ShellContext context)
{
var features = context.ServiceProvider.GetService<IServer>()?.Features;
var builder = new ApplicationBuilder(context.ServiceProvider, features ?? new FeatureCollection());
var startupFilters = builder.ApplicationServices.GetService<IEnumerable<IStartupFilter>>();

Action<IApplicationBuilder> configure = builder =>
{
ConfigurePipeline(builder);
};

foreach (var filter in startupFilters.Reverse())
{
configure = filter.Configure(configure);
}

configure(builder);

var shellPipeline = new ShellRequestPipeline
{
Next = builder.Build()
};

return shellPipeline;
}

/// <summary>
/// Configures the tenant pipeline.
/// </summary>
private static void ConfigurePipeline(IApplicationBuilder builder)
{
// 'IStartup' instances are ordered by module dependencies with a 'ConfigureOrder' of 0 by default.
// 'OrderBy' performs a stable sort, so the order is preserved among equal 'ConfigureOrder' values.
var startups = builder.ApplicationServices.GetServices<IStartup>().OrderBy(s => s.ConfigureOrder);

builder.UseRouting().UseEndpoints(routes =>
{
foreach (var startup in startups)
{
startup.Configure(builder, routes, ShellScope.Services);
}
});
}
}
20 changes: 20 additions & 0 deletions src/OrchardCore/OrchardCore/Modules/ModularBackgroundService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ await shellScope.UsingAsync(async scope =>
{
try
{
// Use the base url, if defined, to override the 'Scheme', 'Host' and 'PathBase'.
_httpContextAccessor.HttpContext.SetBaseUrl((await siteService.GetSiteSettingsAsync()).BaseUrl);
}
catch (Exception ex) when (!ex.IsFatal())
Expand All @@ -158,6 +159,25 @@ await shellScope.UsingAsync(async scope =>
}
}
try
{
if (scheduler.Settings.UsePipeline)
{
if (!scope.ShellContext.HasPipeline())
{
// Build the shell pipeline to configure endpoint data sources.
await scope.ShellContext.BuildPipelineAsync();
}
// Run the pipeline to make the 'HttpContext' aware of endpoints.
await scope.ShellContext.Pipeline.Invoke(_httpContextAccessor.HttpContext);
}
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogError(ex, "Error while running in the background the pipeline of tenant '{TenantName}'.", tenant);
}
var context = new BackgroundTaskEventContext(taskName, scope);
var handlers = scope.ServiceProvider.GetServices<IBackgroundTaskEventHandler>();
Expand Down
Loading

0 comments on commit 1e350a8

Please sign in to comment.