Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a683ce6
OrchardCore Clusters
jtkech May 2, 2023
c828901
Minor changes
jtkech May 2, 2023
177d842
Refactoring
jtkech May 2, 2023
20c7a5c
Refactoring and comments
jtkech May 3, 2023
4e9fb29
Minor changes and more comments
jtkech May 3, 2023
09157e3
Minor changes
jtkech May 3, 2023
711d665
minor changes
jtkech May 3, 2023
4ceb889
Wip
jtkech May 4, 2023
9ccbbe4
wip
jtkech May 4, 2023
cc4bdaf
Tweak
jtkech May 4, 2023
50b58ec
Tweaks
jtkech May 4, 2023
bbc2ad5
tweaks
jtkech May 4, 2023
fe70142
Minor changes
jtkech May 4, 2023
c40b256
Minor changes
jtkech May 4, 2023
589925f
Minor changes
jtkech May 4, 2023
fa95b35
Minor changes
jtkech May 5, 2023
173759c
Minor changes
jtkech May 5, 2023
187d4a8
Missi,g updates
jtkech May 5, 2023
9031a05
Minor changes
jtkech May 5, 2023
1940753
minor changes
jtkech May 5, 2023
55a03ed
Fix unit tests
jtkech May 5, 2023
838719e
wip
jtkech May 7, 2023
3d253fc
Manages ShellContext RequestsCount
jtkech May 7, 2023
39d8059
Simplify Requests Count
jtkech May 7, 2023
206dd1f
Useless file
jtkech May 7, 2023
0cb8270
Clustered Tenant Inactivity Check
jtkech May 9, 2023
9a4c08f
Some Refactoring
jtkech May 10, 2023
aaeb0bb
Remove Proxy Hosts option
jtkech May 10, 2023
336e865
Updates after merging dev
jtkech Jul 2, 2023
19d8e50
Add tenant clustering support
sebastienros Apr 13, 2026
a6c1631
Fix CI analyzer issues
sebastienros Apr 13, 2026
e6455ca
Stabilize tenant validator tests
sebastienros Apr 13, 2026
29bca78
Merge branch 'main' into jtkech/oc-clusters
sebastienros Apr 13, 2026
db66c66
Handle missing tenant cluster mapping
sebastienros Apr 14, 2026
8d8503a
Clarify local demo configuration files
sebastienros Apr 14, 2026
9800e27
Validate tenant cluster slot ranges
sebastienros May 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
<PackageVersion Include="SixLabors.ImageSharp.Web.Providers.AWS" Version="3.2.0" />
<PackageVersion Include="StackExchange.Redis" Version="2.12.8" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
<PackageVersion Include="Yarp.ReverseProxy" Version="2.3.0" />
<PackageVersion Include="xunit.v3.mtp-v2" Version="3.2.2" />
<PackageVersion Include="YesSql" Version="5.4.7" />
<PackageVersion Include="YesSql.Abstractions" Version="5.4.7" />
Expand Down
1 change: 1 addition & 0 deletions OrchardCore.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@
</Folder>
<Folder Name="/src/OrchardCore/">
<Project Path="src/OrchardCore/OrchardCore.Abstractions/OrchardCore.Abstractions.csproj" />
<Project Path="src/OrchardCore/OrchardCore.Clusters.Yarp/OrchardCore.Clusters.Yarp.csproj" />
<Project Path="src/OrchardCore/OrchardCore.Admin.Abstractions/OrchardCore.Admin.Abstractions.csproj" />
<Project Path="src/OrchardCore/OrchardCore.AdminMenu.Abstractions/OrchardCore.AdminMenu.Abstractions.csproj" />
<Project Path="src/OrchardCore/OrchardCore.Apis.GraphQL.Abstractions/OrchardCore.Apis.GraphQL.Abstractions.csproj" />
Expand Down
3 changes: 3 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ nav:
- Secure your application: topics/security/README.md
- Data: topics/data/README.md
- Configuration: topics/configuration/README.md
- Tenant Clustering: topics/tenant-clustering/README.md
- Tenant Clustering Demo: topics/tenant-clustering/local-demo.md
- Workflows: topics/workflows/README.md
- Publishing a new release: topics/publishing-releases/README.md
- Using Docker: topics/docker/README.md
Expand Down Expand Up @@ -292,6 +294,7 @@ nav:
- Background Tasks: reference/modules/BackgroundTasks/README.md
- URL Rewriting: reference/modules/UrlRewriting/README.md
- Reverse Proxy: reference/modules/ReverseProxy/README.md
- Tenant Clustering: reference/modules/ReverseProxy/TenantClusters.md
- Branding: reference/branding/README.md
- Libraries: reference/libraries/README.md
- Glossary: reference/glossary/README.md
Expand Down
1 change: 1 addition & 0 deletions src/OrchardCore.Cms.Web/NLog.config
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<target xsi:type="File" name="file"
fileName="${var:configDir}/logs/orchard-log-${shortdate}.log"
layout="${longdate}|${orchard-tenant-name}|${aspnet-traceidentifier}|${event-properties:item=EventId}|${logger}|${uppercase:${level}}|${message} ${exception:format=ToString,StackTrace}"
concurrentWrites = "true"
/>

<!-- console target -->
Expand Down
1 change: 1 addition & 0 deletions src/OrchardCore.Cms.Web/OrchardCore.Cms.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

<ItemGroup>
<ProjectReference Include="..\OrchardCore\OrchardCore.Application.Cms.Targets\OrchardCore.Application.Cms.Targets.csproj" />
<ProjectReference Include="..\OrchardCore\OrchardCore.Clusters.Yarp\OrchardCore.Clusters.Yarp.csproj" />
<ProjectReference Include="..\OrchardCore\OrchardCore.Logging.NLog\OrchardCore.Logging.NLog.csproj" />
</ItemGroup>

Expand Down
22 changes: 20 additions & 2 deletions src/OrchardCore.Cms.Web/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using OrchardCore.Clusters;
using OrchardCore.Logging;

var builder = WebApplication.CreateBuilder(args);
Expand All @@ -6,7 +7,15 @@

builder.Services
.AddOrchardCms()
.AddSetupFeatures("OrchardCore.AutoSetup");
.AddSetupFeatures("OrchardCore.AutoSetup")
.AddClusteredTenantInactivityCheck()
;

builder.Services
.AddReverseProxy()
.AddTenantClusters()
.LoadFromConfig(builder.Configuration.GetSection("OrchardCore_Clusters"))
;

var app = builder.Build();

Expand All @@ -16,7 +25,16 @@
}

app.UseStaticFiles();

app.UseOrchardCore();

app.MapReverseProxy(proxyPipeline =>
{
proxyPipeline
.UseTenantClusters()
.UseSessionAffinity()
.UseLoadBalancing()
.UsePassiveHealthChecks()
;
});

await app.RunAsync();
2 changes: 2 additions & 0 deletions src/OrchardCore.Cms.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
}
},
//"AllowedHosts": "example.com;localhost", // A semicolon-delimited list of host names without port numbers. Set it for public-facing edge servers or when the Host header is directly forwarded.
//"AllowedHosts": "*",
// See the Tenant Clustering documentation for OrchardCore_Clusters configuration examples.
"OrchardCore": {
// See https://docs.orchardcore.net/en/latest/reference/modules/Admin/#custom-admin-prefix
//"OrchardCore_Admin": {
Expand Down
2 changes: 1 addition & 1 deletion src/OrchardCore.Themes/TheBlogTheme/Views/Layout.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@
</footer>
{% resources type: "FootScript" %}
</body>
</html>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace OrchardCore.Clusters;

/// <summary>
/// Used to capture the selected tenant cluster identifier.
/// </summary>
public class ClusterFeature
{
/// <summary>
/// The selected tenant cluster identifier.
/// </summary>
public string ClusterId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace OrchardCore.Clusters;

/// <summary>
/// Tenant Cluster options.
/// </summary>
public class ClusterOptions
{
/// <summary>
/// The cluster identifier.
/// </summary>
public string ClusterId { get; set; }

/// <summary>
/// The minimum tenant slot number.
/// </summary>
public int SlotMin { get; set; }

/// <summary>
/// The maximum tenant slot number.
/// </summary>
public int SlotMax { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;

namespace OrchardCore.Clusters;

/// <summary>
/// Tenant Clusters options.
/// </summary>
public class ClustersOptions
{
// The identifier of the route template used by the clusters proxy.
public static readonly string RouteTemplate = nameof(RouteTemplate);

/// <summary>
/// Wether tenant clusters is enabled or not.
/// </summary>
public bool Enabled { get; set; }

/// <summary>
/// The maximum idle time before a tenant is released.
/// </summary>
public TimeSpan? MaxIdleTime { get; set; }

/// <summary>
/// List of all single <see cref="ClusterOptions"/>.
/// </summary>
public List<ClusterOptions> Clusters { get; set; } = new();
}
54 changes: 54 additions & 0 deletions src/OrchardCore/OrchardCore.Abstractions/Clusters/Crc16XModem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Text;

namespace OrchardCore.Clusters;

/// <summary>
/// Implements the 'CRC-16/XMODEM' algorithm.
/// </summary>
public class Crc16XModem
{
private static readonly int[] _lookup = new int[256];

static Crc16XModem()
{
for (var i = 0; i < _lookup.Length; i++)
{
var result = i << 8;
for (var n = 0; n < 8; n++)
{
result <<= 1;
if ((result & 0x10000) != 0)
{
result ^= 0x1021;
}
}

_lookup[i] = result;
}
}

/// <summary>
/// Checks the 'CRC-16' algorithm.
/// </summary>
public static bool Check() => Compute("123456789") == 0x31c3;

/// <summary>
/// Computes the 'CRC-16' of the provided string.
/// </summary>
public static int Compute(string input) => Compute(Encoding.ASCII.GetBytes(input));


/// <summary>
/// Computes the 'CRC-16' of the provided byte array.
/// </summary>
public static int Compute(byte[] input)
{
var result = 0;
for (var i = 0; i < input.Length; i++)
{
result = _lookup[(result >> 8 ^ input[i]) & 0xff] ^ result << 8;
}

return result & 0xffff;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Linq;
using Microsoft.AspNetCore.Http;

namespace OrchardCore.Clusters;

/// <summary>
/// Extension methods for checking clusters proxy requests.
/// </summary>
public static class HttpContextExtensions
{
/// <summary>
/// Checks if this instance runs as a clusters proxy.
/// </summary>
public static bool AsClustersProxy(this HttpContext context, ClustersOptions options) =>
options.Enabled && !context.FromClustersProxy();

/// <summary>
/// Checks if the current request comes from a clusters proxy.
/// </summary>
public static bool FromClustersProxy(this HttpContext context) =>
context.Request.Headers.TryGetValue(RequestHeaderNames.FromClustersProxy, out _);

/// <summary>
/// Tries to get the <see cref="ClusterFeature"/> holding the selected tenant cluster identifier.
/// </summary>
public static bool TryGetClusterFeature(this HttpContext context, out ClusterFeature feature)
{
feature = context.Features.Get<ClusterFeature>();
return feature != null;
}

/// <summary>
/// Returns the original host header if it exists, otherwise the request host.
/// </summary>
public static string GetClustersRequestHost(this HttpContext context)
{
if (!context.Request.Headers.TryGetValue(RequestHeaderNames.XOriginalHost, out var hosts))
{
return context.Request.Host.ToString();
}

return hosts.FirstOrDefault();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace OrchardCore.Clusters;

/// <summary>
/// Request headers used by the clusters proxy.
/// </summary>
public class RequestHeaderNames
{
/// <summary>
/// Sent by any clusters proxy request, prevents network loops.
/// </summary>
public static readonly string FromClustersProxy = "From-Clusters-Proxy";

/// <summary>
/// Prevents proxy requests matching while tenant clusters is disabled.
/// </summary>
public static readonly string FakeClustersHeader = "Fake-Clusters-Header";

/// <summary>
/// Used to retrieve the original host while behind a clusters proxy.
/// </summary>
public static readonly string XOriginalHost = "X-Original-Host";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using OrchardCore.Environment.Shell;

namespace OrchardCore.Clusters;

/// <summary>
/// Extension methods for managing tenant clusters.
/// </summary>
public static class ShellSettingsExtensions
{
/// <summary>
/// Returns the selected tenant cluster based on the provided <see cref="ShellSettings"/>.
/// </summary>
public static string GetClusterId(this ShellSettings settings, ClustersOptions options)
{
foreach (var cluster in options.Clusters)
{
// Check if the cluster slot of the current tenant is in the cluster slot range.
if (settings.ClusterSlot >= cluster.SlotMin && settings.ClusterSlot <= cluster.SlotMax)
{
return cluster.ClusterId;
}
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class ShellContext : IDisposable, IAsyncDisposable
private bool _disposed;
private volatile int _refCount;
private int _terminated;
internal long _requestTicks;
private bool _released;

/// <summary>
Expand Down Expand Up @@ -45,6 +46,16 @@ public class ShellContext : IDisposable, IAsyncDisposable
/// </summary>
public bool IsActivated { get; set; }

/// <summary>
/// The UTC date and time of the last request.
/// Read and write operations are both atomic.
/// </summary>
public DateTime LastRequestTimeUtc
{
get => new(Interlocked.Read(ref _requestTicks));
internal set => Interlocked.Exchange(ref _requestTicks, value.Ticks);
}

/// <summary>
/// The Pipeline built for this shell.
/// </summary>
Expand Down
Loading
Loading