using Microsoft.Extensions.Logging;
using StellaOps.Plugin.Abstractions;
using StellaOps.Plugin.Abstractions.Execution;
using StellaOps.Plugin.Abstractions.Health;
using StellaOps.Plugin.Abstractions.Lifecycle;
using StellaOps.Plugin.Registry.Models;
using System.Collections.Concurrent;
using System.Text.Json;
namespace StellaOps.Plugin.Registry;
///
/// In-memory implementation of the plugin registry for testing and development.
///
public sealed class InMemoryPluginRegistry : IPluginRegistry
{
private readonly ConcurrentDictionary _plugins = new();
private readonly ConcurrentDictionary _capabilities = new();
private readonly ConcurrentDictionary _instances = new();
private readonly ConcurrentDictionary> _healthHistory = new();
private readonly ILogger _logger;
private readonly TimeProvider _timeProvider;
///
/// Creates a new in-memory plugin registry instance.
///
public InMemoryPluginRegistry(
ILogger logger,
TimeProvider timeProvider)
{
_logger = logger;
_timeProvider = timeProvider;
}
///
public Task RegisterAsync(LoadedPlugin plugin, CancellationToken ct)
{
var now = _timeProvider.GetUtcNow();
var id = Guid.NewGuid();
// Get current health status from last check result if available
var healthStatus = plugin.LastHealthCheckResult?.Status ?? HealthStatus.Unknown;
var record = new PluginRecord
{
Id = id,
PluginId = plugin.Info.Id,
Name = plugin.Info.Name,
Version = plugin.Info.Version,
Vendor = plugin.Info.Vendor,
Description = plugin.Info.Description,
LicenseId = plugin.Info.LicenseId,
TrustLevel = plugin.TrustLevel,
Capabilities = plugin.Capabilities,
Status = plugin.State,
HealthStatus = healthStatus,
Source = "installed",
AssemblyPath = plugin.Manifest?.AssemblyPath,
EntryPoint = plugin.Manifest?.EntryPoint,
CreatedAt = now,
UpdatedAt = now,
LoadedAt = plugin.LoadedAt
};
_plugins[id] = record;
_logger.LogDebug("Registered plugin {PluginId} with ID {Id}", plugin.Info.Id, id);
return Task.FromResult(record);
}
///
public Task UpdateStatusAsync(string pluginId, PluginLifecycleState status, string? message = null, CancellationToken ct = default)
{
var record = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId);
if (record != null)
{
var updated = new PluginRecord
{
Id = record.Id,
PluginId = record.PluginId,
Name = record.Name,
Version = record.Version,
Vendor = record.Vendor,
Description = record.Description,
LicenseId = record.LicenseId,
TrustLevel = record.TrustLevel,
Capabilities = record.Capabilities,
Status = status,
StatusMessage = message,
HealthStatus = record.HealthStatus,
LastHealthCheck = record.LastHealthCheck,
HealthCheckFailures = record.HealthCheckFailures,
Source = record.Source,
AssemblyPath = record.AssemblyPath,
EntryPoint = record.EntryPoint,
Manifest = record.Manifest,
CreatedAt = record.CreatedAt,
UpdatedAt = _timeProvider.GetUtcNow(),
LoadedAt = record.LoadedAt
};
_plugins[record.Id] = updated;
}
return Task.CompletedTask;
}
///
public Task UpdateHealthAsync(string pluginId, HealthStatus status, HealthCheckResult? result = null, CancellationToken ct = default)
{
var record = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId);
if (record != null)
{
var now = _timeProvider.GetUtcNow();
var updated = new PluginRecord
{
Id = record.Id,
PluginId = record.PluginId,
Name = record.Name,
Version = record.Version,
Vendor = record.Vendor,
Description = record.Description,
LicenseId = record.LicenseId,
TrustLevel = record.TrustLevel,
Capabilities = record.Capabilities,
Status = record.Status,
StatusMessage = record.StatusMessage,
HealthStatus = status,
LastHealthCheck = now,
HealthCheckFailures = status == HealthStatus.Healthy ? 0 : record.HealthCheckFailures + 1,
Source = record.Source,
AssemblyPath = record.AssemblyPath,
EntryPoint = record.EntryPoint,
Manifest = record.Manifest,
CreatedAt = record.CreatedAt,
UpdatedAt = now,
LoadedAt = record.LoadedAt
};
_plugins[record.Id] = updated;
if (result != null)
{
RecordHealthCheckAsync(pluginId, result, ct).GetAwaiter().GetResult();
}
}
return Task.CompletedTask;
}
///
public Task UnregisterAsync(string pluginId, CancellationToken ct)
{
var record = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId);
if (record != null)
{
_plugins.TryRemove(record.Id, out _);
// Remove capabilities
var capsToRemove = _capabilities.Values.Where(c => c.PluginId == record.Id).ToList();
foreach (var cap in capsToRemove)
{
_capabilities.TryRemove(cap.Id, out _);
}
// Remove instances
var instancesToRemove = _instances.Values.Where(i => i.PluginId == record.Id).ToList();
foreach (var instance in instancesToRemove)
{
_instances.TryRemove(instance.Id, out _);
}
_logger.LogDebug("Unregistered plugin {PluginId}", pluginId);
}
return Task.CompletedTask;
}
///
public Task GetAsync(string pluginId, CancellationToken ct)
{
var record = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId);
return Task.FromResult(record);
}
///
public Task> GetAllAsync(CancellationToken ct)
{
var result = _plugins.Values.OrderBy(p => p.Name).ToList();
return Task.FromResult>(result);
}
///
public Task> GetByStatusAsync(PluginLifecycleState status, CancellationToken ct)
{
var result = _plugins.Values
.Where(p => p.Status == status)
.OrderBy(p => p.Name)
.ToList();
return Task.FromResult>(result);
}
///
public Task> GetByCapabilityAsync(PluginCapabilities capability, CancellationToken ct)
{
var result = _plugins.Values
.Where(p => (p.Capabilities & capability) != 0 && p.Status == PluginLifecycleState.Active)
.OrderBy(p => p.Name)
.ToList();
return Task.FromResult>(result);
}
///
public Task> GetByCapabilityTypeAsync(string capabilityType, string? capabilityId = null, CancellationToken ct = default)
{
var matchingCaps = _capabilities.Values
.Where(c => c.CapabilityType == capabilityType && c.IsEnabled)
.Where(c => capabilityId == null || c.CapabilityId == capabilityId);
var pluginIds = matchingCaps.Select(c => c.PluginId).ToHashSet();
var result = _plugins.Values
.Where(p => pluginIds.Contains(p.Id) && p.Status == PluginLifecycleState.Active)
.OrderBy(p => p.Name)
.ToList();
return Task.FromResult>(result);
}
///
public Task RegisterCapabilitiesAsync(Guid pluginDbId, IEnumerable capabilities, CancellationToken ct)
{
foreach (var cap in capabilities)
{
var record = cap with { PluginId = pluginDbId };
_capabilities[cap.Id] = record;
}
return Task.CompletedTask;
}
///
public Task> GetCapabilitiesAsync(string pluginId, CancellationToken ct)
{
var plugin = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId);
if (plugin == null)
{
return Task.FromResult>([]);
}
var result = _capabilities.Values
.Where(c => c.PluginId == plugin.Id)
.OrderBy(c => c.CapabilityType)
.ThenBy(c => c.CapabilityId)
.ToList();
return Task.FromResult>(result);
}
///
public Task CreateInstanceAsync(CreatePluginInstanceRequest request, CancellationToken ct)
{
var plugin = _plugins.Values.FirstOrDefault(p => p.PluginId == request.PluginId)
?? throw new InvalidOperationException($"Plugin {request.PluginId} not found");
var now = _timeProvider.GetUtcNow();
var id = Guid.NewGuid();
var record = new PluginInstanceRecord
{
Id = id,
PluginId = plugin.Id,
PluginStringId = request.PluginId,
TenantId = request.TenantId,
InstanceName = request.InstanceName,
Config = request.Config,
SecretsPath = request.SecretsPath,
ResourceLimits = request.ResourceLimits,
Enabled = true,
Status = "pending",
CreatedAt = now,
UpdatedAt = now
};
_instances[id] = record;
_logger.LogDebug("Created instance {InstanceId} for plugin {PluginId}", id, request.PluginId);
return Task.FromResult(record);
}
///
public Task GetInstanceAsync(Guid instanceId, CancellationToken ct)
{
_instances.TryGetValue(instanceId, out var record);
return Task.FromResult(record);
}
///
public Task> GetInstancesForTenantAsync(Guid tenantId, CancellationToken ct)
{
var result = _instances.Values
.Where(i => i.TenantId == tenantId)
.OrderBy(i => i.CreatedAt)
.ToList();
return Task.FromResult>(result);
}
///
public Task> GetInstancesForPluginAsync(string pluginId, CancellationToken ct)
{
var plugin = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId);
if (plugin == null)
{
return Task.FromResult>([]);
}
var result = _instances.Values
.Where(i => i.PluginId == plugin.Id)
.OrderBy(i => i.CreatedAt)
.ToList();
return Task.FromResult>(result);
}
///
public Task UpdateInstanceConfigAsync(Guid instanceId, JsonDocument config, CancellationToken ct)
{
if (_instances.TryGetValue(instanceId, out var existing))
{
var updated = existing with
{
Config = config,
UpdatedAt = _timeProvider.GetUtcNow()
};
_instances[instanceId] = updated;
}
return Task.CompletedTask;
}
///
public Task SetInstanceEnabledAsync(Guid instanceId, bool enabled, CancellationToken ct)
{
if (_instances.TryGetValue(instanceId, out var existing))
{
var updated = existing with
{
Enabled = enabled,
UpdatedAt = _timeProvider.GetUtcNow()
};
_instances[instanceId] = updated;
}
return Task.CompletedTask;
}
///
public Task DeleteInstanceAsync(Guid instanceId, CancellationToken ct)
{
_instances.TryRemove(instanceId, out _);
return Task.CompletedTask;
}
///
public Task RecordHealthCheckAsync(string pluginId, HealthCheckResult result, CancellationToken ct)
{
var plugin = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId);
if (plugin == null)
return Task.CompletedTask;
var now = _timeProvider.GetUtcNow();
var healthRecord = new PluginHealthRecord
{
Id = Guid.NewGuid(),
PluginId = plugin.Id,
PluginStringId = pluginId,
CheckedAt = now,
Status = result.Status,
ResponseTimeMs = result.Duration.HasValue ? (int)result.Duration.Value.TotalMilliseconds : null,
ErrorMessage = result.Message,
CreatedAt = now
};
_healthHistory.AddOrUpdate(
plugin.Id,
_ => [healthRecord],
(_, list) =>
{
list.Add(healthRecord);
return list;
});
return Task.CompletedTask;
}
///
public Task> GetHealthHistoryAsync(
string pluginId,
DateTimeOffset since,
int limit = 100,
CancellationToken ct = default)
{
var plugin = _plugins.Values.FirstOrDefault(p => p.PluginId == pluginId);
if (plugin == null || !_healthHistory.TryGetValue(plugin.Id, out var history))
{
return Task.FromResult>([]);
}
var result = history
.Where(h => h.CheckedAt >= since)
.OrderByDescending(h => h.CheckedAt)
.Take(limit)
.ToList();
return Task.FromResult>(result);
}
///
/// Clears all data from the registry. For testing purposes.
///
public void Clear()
{
_plugins.Clear();
_capabilities.Clear();
_instances.Clear();
_healthHistory.Clear();
}
}