Skip to content

Commit

Permalink
Support MODULE LOADCS command (microsoft#463)
Browse files Browse the repository at this point in the history
Adds support for MODULE LOADCS command.

This initial version supports .net modules.
Adds ability to register a module with its name and version number.
As part of initialization, a module can register commands and transactions that it implements.
  • Loading branch information
yrajas authored Jun 21, 2024
1 parent 1be8646 commit 4a64897
Show file tree
Hide file tree
Showing 15 changed files with 1,033 additions and 446 deletions.
11 changes: 11 additions & 0 deletions Garnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BDN.benchmark", "benchmark\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommandInfoUpdater", "playground\CommandInfoUpdater\CommandInfoUpdater.csproj", "{9BE474A2-1547-43AC-B4F2-FB48A01FA995}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleModule", "playground\SampleModule\SampleModule.csproj", "{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -269,6 +271,14 @@ Global
{9BE474A2-1547-43AC-B4F2-FB48A01FA995}.Release|Any CPU.Build.0 = Release|Any CPU
{9BE474A2-1547-43AC-B4F2-FB48A01FA995}.Release|x64.ActiveCfg = Release|Any CPU
{9BE474A2-1547-43AC-B4F2-FB48A01FA995}.Release|x64.Build.0 = Release|Any CPU
{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Debug|x64.ActiveCfg = Debug|Any CPU
{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Debug|x64.Build.0 = Debug|Any CPU
{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Release|Any CPU.Build.0 = Release|Any CPU
{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Release|x64.ActiveCfg = Release|Any CPU
{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -297,6 +307,7 @@ Global
{5BEDAC1F-6458-4EBA-8174-EC06B07F2132} = {69A71E2C-00E3-42F3-854E-BE157A24834E}
{9F6E4734-6341-4A9C-A7FF-636A39D8BEAD} = {346A5A53-51E4-4A75-B7E6-491D950382CE}
{9BE474A2-1547-43AC-B4F2-FB48A01FA995} = {69A71E2C-00E3-42F3-854E-BE157A24834E}
{A8CA619E-8F13-4EF8-943F-2D5E3FEBFB3F} = {69A71E2C-00E3-42F3-854E-BE157A24834E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2C02C405-4798-41CA-AF98-61EDFEF6772E}
Expand Down
206 changes: 206 additions & 0 deletions libs/server/Module/ModuleRegistrar.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Logging;

namespace Garnet.server.Module
{
/// <summary>
/// Abstract base class that all Garnet modules must inherit from.
/// The module must have a parameterless constructor.
/// NOTE: This is a feature that is under development.
/// If taking a dependency on this, please be prepared for breaking changes.
/// </summary>
public abstract class ModuleBase
{
/// <summary>
/// Called when the module is loaded.
/// </summary>
/// <param name="context">Module load context</param>
/// <param name="args">Module load args</param>
public abstract void OnLoad(ModuleLoadContext context, string[] args);
}

/// <summary>
/// Represents the status of a module action.
/// </summary>
public enum ModuleActionStatus
{
Success,
Failure,
AlreadyLoaded,
AlreadyExists,
InvalidRegistrationInfo,
}

/// <summary>
/// Provides context for a loading module to invoke initialization and registration methods.
/// </summary>
public class ModuleLoadContext
{
public readonly ILogger Logger;
internal string Name;
internal uint Version;
internal bool Initialized;

private readonly ModuleRegistrar moduleRegistrar;
private readonly CustomCommandManager customCommandManager;

internal ModuleLoadContext(ModuleRegistrar moduleRegistrar, CustomCommandManager customCommandManager, ILogger logger)
{
Debug.Assert(moduleRegistrar != null);
Debug.Assert(customCommandManager != null);

Initialized = false;
this.moduleRegistrar = moduleRegistrar;
this.customCommandManager = customCommandManager;
Logger = logger;
}

/// <summary>
/// Initializes the module load context
/// </summary>
/// <param name="name">Module name</param>
/// <param name="version">Module version</param>
/// <returns>Initialization status</returns>
public ModuleActionStatus Initialize(string name, uint version)
{
if (string.IsNullOrEmpty(name))
return ModuleActionStatus.InvalidRegistrationInfo;

if (Initialized)
return ModuleActionStatus.AlreadyLoaded;

Name = name;
Version = version;

if (!moduleRegistrar.TryAdd(this))
return ModuleActionStatus.AlreadyExists;

Initialized = true;
return ModuleActionStatus.Success;
}

/// <summary>
/// Registers a raw string custom command
/// </summary>
/// <param name="name">Command name</param>
/// <param name="numParams">Number of parameters</param>
/// <param name="type">Command type</param>
/// <param name="customFunctions">Custom raw string function implementation</param>
/// <param name="commandInfo">Command info</param>
/// <param name="expirationTicks">Expiration ticks for the key</param>
/// <returns>Registration status</returns>
public ModuleActionStatus RegisterCommand(string name, CustomRawStringFunctions customFunctions, CommandType type = CommandType.ReadModifyWrite, int numParams = int.MaxValue, RespCommandsInfo commandInfo = null, long expirationTicks = 0)
{
if (string.IsNullOrEmpty(name) || customFunctions == null)
return ModuleActionStatus.InvalidRegistrationInfo;

customCommandManager.Register(name, numParams, type, customFunctions, commandInfo, expirationTicks);

return ModuleActionStatus.Success;
}

/// <summary>
/// Registers a custom transaction
/// </summary>
/// <param name="name">Transaction name</param>
/// <param name="numParams">Number of parameters</param>
/// <param name="proc">Transaction procedure implemenation</param>
/// <param name="commandInfo">Command info</param>
/// <returns>Registration status</returns>
public ModuleActionStatus RegisterTransaction(string name, Func<CustomTransactionProcedure> proc, int numParams = int.MaxValue, RespCommandsInfo commandInfo = null)
{
if (string.IsNullOrEmpty(name) || proc == null)
return ModuleActionStatus.InvalidRegistrationInfo;

customCommandManager.Register(name, numParams, proc, commandInfo);

return ModuleActionStatus.Success;
}
}

internal sealed class ModuleRegistrar
{
private static readonly Lazy<ModuleRegistrar> lazy = new Lazy<ModuleRegistrar>(() => new ModuleRegistrar());

internal static ModuleRegistrar Instance { get { return lazy.Value; } }

private ModuleRegistrar()
{
modules = new();
}

private readonly ConcurrentDictionary<string, ModuleLoadContext> modules;

internal bool LoadModule(CustomCommandManager customCommandManager, Assembly loadedAssembly, string[] moduleArgs, ILogger logger, out ReadOnlySpan<byte> errorMessage)
{
errorMessage = default;

// Get types that implement IModule from loaded assemblies
var loadedTypes = loadedAssembly
.GetTypes()
.Where(t => t.IsSubclassOf(typeof(ModuleBase)) && !t.IsAbstract)
.ToArray();

if (loadedTypes.Length == 0)
{
errorMessage = CmdStrings.RESP_ERR_MODULE_NO_INTERFACE;
return false;
}

if (loadedTypes.Length > 1)
{
errorMessage = CmdStrings.RESP_ERR_MODULE_MULTIPLE_INTERFACES;
return false;
}

// Check that type has empty constructor
var moduleType = loadedTypes[0];
if (moduleType.GetConstructor(Type.EmptyTypes) == null)
{
errorMessage = CmdStrings.RESP_ERR_GENERIC_INSTANTIATING_CLASS;
return false;
}

var instance = (ModuleBase)Activator.CreateInstance(moduleType);
if (instance == null)
{
errorMessage = CmdStrings.RESP_ERR_GENERIC_INSTANTIATING_CLASS;
return false;
}

var moduleLoadContext = new ModuleLoadContext(this, customCommandManager, logger);
try
{
instance.OnLoad(moduleLoadContext, moduleArgs);
}
catch (Exception ex)
{
logger?.LogError(ex, "Error loading module");
errorMessage = CmdStrings.RESP_ERR_MODULE_ONLOAD;
return false;
}

if (!moduleLoadContext.Initialized)
{
errorMessage = CmdStrings.RESP_ERR_MODULE_ONLOAD;
logger?.LogError("Module {0} failed to initialize", moduleLoadContext.Name);
return false;
}

logger?.LogInformation("Module {0} version {1} loaded", moduleLoadContext.Name, moduleLoadContext.Version);
return true;
}

internal bool TryAdd(ModuleLoadContext moduleLoadContext)
{
return modules.TryAdd(moduleLoadContext.Name, moduleLoadContext);
}
}
}
Loading

0 comments on commit 4a64897

Please sign in to comment.