Skip to content

Add Garnet module #123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion OrchardCoreContrib.Modules.sln
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.Themes.A
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.Liquid", "src\OrchardCoreContrib.Liquid\OrchardCoreContrib.Liquid.csproj", "{8C49A05B-5D69-4222-9245-310E412E237E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCoreContrib.Sms.Azure", "src\OrchardCoreContrib.Sms.Azure\OrchardCoreContrib.Sms.Azure.csproj", "{47BE63E8-AECC-43E5-AC48-2E2835452402}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.Sms.Azure", "src\OrchardCoreContrib.Sms.Azure\OrchardCoreContrib.Sms.Azure.csproj", "{47BE63E8-AECC-43E5-AC48-2E2835452402}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCoreContrib.Garnet", "src\OrchardCoreContrib.Garnet\OrchardCoreContrib.Garnet.csproj", "{14666792-7A70-4ACF-9FAB-CC1435213052}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -156,6 +158,10 @@ Global
{47BE63E8-AECC-43E5-AC48-2E2835452402}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47BE63E8-AECC-43E5-AC48-2E2835452402}.Release|Any CPU.ActiveCfg = Release|Any CPU
{47BE63E8-AECC-43E5-AC48-2E2835452402}.Release|Any CPU.Build.0 = Release|Any CPU
{14666792-7A70-4ACF-9FAB-CC1435213052}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{14666792-7A70-4ACF-9FAB-CC1435213052}.Debug|Any CPU.Build.0 = Debug|Any CPU
{14666792-7A70-4ACF-9FAB-CC1435213052}.Release|Any CPU.ActiveCfg = Release|Any CPU
{14666792-7A70-4ACF-9FAB-CC1435213052}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -184,6 +190,7 @@ Global
{EF11652A-B9A2-4E5E-897E-16F26C4D6946} = {C80A325F-F4C4-4C7B-A3CF-FB77CD8C9949}
{8C49A05B-5D69-4222-9245-310E412E237E} = {C80A325F-F4C4-4C7B-A3CF-FB77CD8C9949}
{47BE63E8-AECC-43E5-AC48-2E2835452402} = {C80A325F-F4C4-4C7B-A3CF-FB77CD8C9949}
{14666792-7A70-4ACF-9FAB-CC1435213052} = {C80A325F-F4C4-4C7B-A3CF-FB77CD8C9949}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {48F73B05-7D3D-4ACF-81AE-A98B2B4EFDB2}
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The `OrchardCoreContrib.Modules` repository consists of the following modules:
| [Hotmail Module](src/OrchardCoreContrib.Email.Hotmail/README.md) | `OrchardCoreContrib.Email.Hotmail` | [![NuGet](https://img.shields.io/nuget/v/OrchardCoreContrib.Email.Hotmail.svg)](https://www.nuget.org/packages/OrchardCoreContrib.Email.Hotmail) |
| [SendGrid Module](src/OrchardCoreContrib.Email.SendGrid/README.md) | `OrchardCoreContrib.Email.SendGrid` | [![NuGet](https://img.shields.io/nuget/v/OrchardCoreContrib.Email.SendGrid.svg)](https://www.nuget.org/packages/OrchardCoreContrib.Email.SendGrid) |
| [Yahoo Module](src/OrchardCoreContrib.Email.Yahoo/README.md) | `OrchardCoreContrib.Email.Yahoo` | [![NuGet](https://img.shields.io/nuget/v/OrchardCoreContrib.Email.Yahoo.svg)](https://www.nuget.org/packages/OrchardCoreContrib.Email.Yahoo) |
| [Garnet Module](src/OrchardCoreContrib.Garnet/README.md) | `OrchardCoreContrib.Garnet` | [![NuGet](https://img.shields.io/nuget/v/OrchardCoreContrib.Garnet.svg)](https://www.nuget.org/packages/OrchardCoreContrib.Garnet) |
| [Google Maps Module](src/OrchardCoreContrib.GoogleMaps/README.md) | `OrchardCoreContrib.GoogleMaps` | [![NuGet](https://img.shields.io/nuget/v/OrchardCoreContrib.GoogleMaps.svg)](https://www.nuget.org/packages/OrchardCoreContrib.GoogleMaps) |
| [Gravatar Module](src/OrchardCoreContrib.Gravatar/README.md) | `OrchardCoreContrib.Gravatar` | [![NuGet](https://img.shields.io/nuget/v/OrchardCoreContrib.Gravatar.svg)](https://www.nuget.org/packages/OrchardCoreContrib.Gravatar) |
| [Health Checks Module](src/OrchardCoreContrib.HealthChecks/README.md) | `OrchardCoreContrib.HealthChecks` | [![NuGet](https://img.shields.io/nuget/v/OrchardCoreContrib.HealthChecks.svg)](https://www.nuget.org/packages/OrchardCoreContrib.HealthChecks) |
Expand Down
17 changes: 17 additions & 0 deletions src/OrchardCoreContrib.Garnet/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace OrchardCoreContrib.Garnet;

/// <summary>
/// Contains a set of constants for the Garnet module.
/// </summary>
public class Constants
{
/// <summary>
/// Gets the configuration section name for the Garnet module.
/// </summary>
public const string ConfigurationSectionName = "OrchardCoreContrib_Garnet";

/// <summary>
/// Gets the health check name for the Garnet module.
/// </summary>
public const string HealthCheckName = "Garnet Health Check";
}
102 changes: 102 additions & 0 deletions src/OrchardCoreContrib.Garnet/GarnetClientFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using Garnet.client;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;

namespace OrchardCoreContrib.Garnet;

/// <summary>
/// Represents a factory for creating instances of <see cref="GarnetClient"/>.
/// </summary>
public class GarnetClientFactory : IGarnetClientFactory, IDisposable
{
private static readonly ConcurrentDictionary<string, Lazy<Task<GarnetClient>>> _factories = new();

private static volatile int _registered;
private static volatile int _refCount;

private readonly IHostApplicationLifetime _lifetime;
private readonly ILogger _logger;

/// <summary>
/// Creates a new instance of <see cref="GarnetClientFactory"/>.
/// </summary>
/// <param name="lifetime">The <see cref="IHostApplicationLifetime"/>.</param>
/// <param name="logger">The <see cref="ILogger{GarnetClientFactory}"/>.</param>
public GarnetClientFactory(IHostApplicationLifetime lifetime, ILogger<GarnetClientFactory> logger)
{
Interlocked.Increment(ref _refCount);

_lifetime = lifetime;

if (Interlocked.CompareExchange(ref _registered, 1, 0) == 0)
{
_lifetime.ApplicationStopped.Register(Release);
}

_logger = logger;
}

/// <inheritdoc/>
public async Task<GarnetClient> CreateAsync(GarnetOptions options)
{
var key = $"garnet://{options.Host}:{options.Port};username={options.UserName};password={options.Password}";

if (_factories.TryGetValue(key, out var value))
{
var client = await value.Value;
if (client is null)
{
_factories.Remove(key, out _);
}
}

return await _factories.GetOrAdd(key, new Lazy<Task<GarnetClient>>(async () =>
{
try
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Creating a new instance of '{name}'. A single instance per configuration should be created across tenants. Total instances prior creating is '{count}'.", nameof(GarnetClient), _factories.Count);
}

var client = new GarnetClient(options.Host, options.Port, authUsername: options.UserName, authPassword: options.Password);

await client.ConnectAsync();

return client;
}
catch (Exception e)
{
_logger.LogError(e, "Unable to connect to Garnet.");

return null;
}
})).Value;
}

/// <inheritdoc/>
public void Dispose()
{
if (Interlocked.Decrement(ref _refCount) == 0 && _lifetime.ApplicationStopped.IsCancellationRequested)
{
Release();
}
}

private static void Release()
{
if (Interlocked.CompareExchange(ref _refCount, 0, 0) == 0)
{
var factories = _factories.Values.ToArray();

_factories.Clear();

foreach (var factory in factories)
{
var client = factory.Value;
client.Dispose();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using OrchardCoreContrib.Garnet;
using OrchardCoreContrib.Garnet.HealthChecks;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Provides a set of extensions for <see cref="IHealthChecksBuilder"/> to add Garnet health checks.
/// </summary>
public static class GarnetHealthCheckExtensions
{
/// <summary>
/// Adds a Garnet health check.
/// </summary>
/// <param name="healthChecksBuilder">The <see cref="IHealthChecksBuilder"/>.</param>
public static IHealthChecksBuilder AddGarnetCheck(this IHealthChecksBuilder healthChecksBuilder)
=> healthChecksBuilder.AddCheck<GarnetHealthCheck>(Constants.HealthCheckName);
}
51 changes: 51 additions & 0 deletions src/OrchardCoreContrib.Garnet/HealthChecks/GarnetHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using OrchardCoreContrib.Garnet.Services;

namespace OrchardCoreContrib.Garnet.HealthChecks;

/// <summary>
/// Represents a health check for the Garnet service.
/// </summary>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/>.</param>
public class GarnetHealthCheck(IServiceProvider serviceProvider) : IHealthCheck
{
/// <inheritdoc/>
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var garnetService = serviceProvider.GetService<IGarnetService>();
if (garnetService == null)
{
return HealthCheckResult.Unhealthy(description: $"The service '{nameof(IGarnetService)}' isn't registered.");
}

if (garnetService.Client is null)
{
await garnetService.ConnectAsync();
}

if (garnetService.Client.IsConnected)
{
var result = await garnetService.Client.PingAsync();
if (result == "PONG")
{
return HealthCheckResult.Healthy();
}
else
{
return HealthCheckResult.Unhealthy(description: "The Garnet server couldn't be reached and might be offline or have degraded performance.");
}
}
else
{
return HealthCheckResult.Unhealthy(description: "Couldn't connect to the Garnet server.");
}
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Retrieving the status of the Garnet service failed.", ex);
}
}
}
36 changes: 36 additions & 0 deletions src/OrchardCoreContrib.Garnet/HealthChecks/Startup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OrchardCore.Modules;
using OrchardCoreContrib.HealthChecks;

namespace OrchardCoreContrib.Garnet.HealthChecks;

/// <summary>
/// Represensts a startup point to register the health checks for Garnet module.
/// </summary>
[RequireFeatures("OrchardCoreContrib.HealthChecks")]
public class Startup : StartupBase
{
/// <inheritdoc/>
public override void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();

services.AddHealthChecks()
.AddGarnetCheck();
}

/// <inheritdoc/>
public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
{
var healthChecksOptions = serviceProvider.GetService<IOptions<HealthChecksOptions>>().Value;

routes.MapHealthChecks($"{healthChecksOptions.Url}/garnet", new HealthCheckOptions
{
Predicate = r => r.Name == Constants.HealthCheckName
});
}
}
11 changes: 11 additions & 0 deletions src/OrchardCoreContrib.Garnet/Manifest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using OrchardCore.Modules.Manifest;
using ManifestConstants = OrchardCoreContrib.Modules.Manifest.ManifestConstants;

[assembly: Module(
Name = "Garnet",
Author = ManifestConstants.Author,
Website = ManifestConstants.Website,
Version = "1.0.0",
Description = "Garnet configuration support.",
Category = "Caching"
)]
35 changes: 35 additions & 0 deletions src/OrchardCoreContrib.Garnet/OrchardCoreContrib.Garnet.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<VersionPrefix>1.0.0</VersionPrefix>
<Authors>The Orchard Core Contrib Team</Authors>
<Company />
<Description>Provides Garnet features for configuration.</Description>
<PackageLicenseExpression>BSD-3-Clause</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/OrchardCoreContrib/OrchardCoreContrib.Modules/tree/main/src/OrchardCoreContrib.Garnet/README.md</PackageProjectUrl>
<RepositoryUrl>https://github.com/OrchardCoreContrib/OrchardCoreContrib.Modules</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageTags>Orchard Core, Orchard Core Contrib, Garnet, Caching</PackageTags>
<PackageReleaseNotes>https://github.com/OrchardCoreContrib/OrchardCoreContrib.Modules/releases</PackageReleaseNotes>
<PackageId>OrchardCoreContrib.Garnet</PackageId>
<PackageIcon>icon.png</PackageIcon>
<Product>Orchard Core Contrib Garnet Module</Product>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<Copyright>2019 Orchard Core Contrib</Copyright>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="OrchardCore.Module.Targets" Version="1.8.3" />
<PackageReference Include="OrchardCoreContrib.Abstractions" Version="1.3.1" />
<PackageReference Include="OrchardCoreContrib.Garnet.Abstractions" Version="1.0.0" />
<PackageReference Include="OrchardCoreContrib.HealthChecks.Abstractions" Version="1.2.1" />
</ItemGroup>

<ItemGroup>
<None Include="../../images/icon.png" Pack="true" PackagePath="icon.png" />
</ItemGroup>

</Project>
36 changes: 36 additions & 0 deletions src/OrchardCoreContrib.Garnet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Garnet Module

This module provides a set of features for Garnet service.

## Version

1.0.0

## Category

Caching

## Dependencies

This module has no dependencies.

## Features

| | |
|------------------|-----------------------------------------------|
| **Name** | Garnet (`OrchardCoreContrib.Garnet`) |
| **Description** | Allow you to configure Garnet service. |
| **Dependencies** | |

## NuGet Packages

| Name | Version |
|-----------------------------------------------------------------------------------------------|---------|
| [`OrchardCoreContrib.Garnet`](https://www.nuget.org/packages/OrchardCoreContrib.Garnet/1.0.0) | 1.0.0 |

## Get Started

1. Install the [`OrchardCoreContrib.Garnet`](https://www.nuget.org/packages/OrchardCoreContrib.Garnet/) NuGet package to your Orchard Core host project.
2. Go to the admin site
3. Select **Configuration -> Features** menu.
4. Enable the `Garnet` feature.
27 changes: 27 additions & 0 deletions src/OrchardCoreContrib.Garnet/Services/GarnetService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Garnet.client;
using Microsoft.Extensions.Options;
using OrchardCore.Modules;

namespace OrchardCoreContrib.Garnet.Services;

/// <summary>
/// Represents the Garnet service.
/// </summary>
/// <param name="factory">The <see cref="IGarnetClientFactory"/>.</param>
/// <param name="options">The <see cref="IOptions{GarnetOptions}"/>.</param>
public class GarnetService(IGarnetClientFactory factory, IOptions<GarnetOptions> options) : ModularTenantEvents, IGarnetService
{
private readonly GarnetOptions _garnetOptions = options.Value;

/// <inheritdoc/>
public GarnetClient Client { get; private set; }

/// <inheritdoc/>
public string InstancePrefix => _garnetOptions.InstancePrefix;

/// <inheritdoc/>
public async Task ConnectAsync() => Client ??= await factory.CreateAsync(_garnetOptions);

/// <inheritdoc/>
public override async Task ActivatingAsync() => await ConnectAsync();
}
Loading