Files
natsdotnet/src/NATS.Server/JetStream/JetStreamService.cs
Joseph Doherty 2c9683e7aa feat: upgrade JetStreamService to lifecycle orchestrator
Implements enableJetStream() semantics from golang/nats-server/server/jetstream.go:414-523.

- JetStreamService.StartAsync(): validates config, creates store directory
  (including nested paths via Directory.CreateDirectory), registers all
  $JS.API.> subjects, logs startup stats; idempotent on double-start
- JetStreamService.DisposeAsync(): clears registered subjects, marks not running
- New properties: RegisteredApiSubjects, MaxStreams, MaxConsumers, MaxMemory, MaxStore
- JetStreamOptions: adds MaxStreams and MaxConsumers limits (0 = unlimited)
- FileStoreConfig: removes duplicate StoreCipher/StoreCompression enum declarations
  now that AeadEncryptor.cs owns them; updates defaults to NoCipher/NoCompression
- FileStoreOptions/FileStore: align enum member names with AeadEncryptor.cs
  (NoCipher, NoCompression, S2Compression) to fix cross-task naming conflict
- 13 new tests in JetStreamServiceOrchestrationTests covering all lifecycle paths
2026-02-24 06:03:46 -05:00

149 lines
5.7 KiB
C#

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Server.Configuration;
using NATS.Server.JetStream.Api;
namespace NATS.Server.JetStream;
// Maps to Go's enableJetStream() in server/jetstream.go:414-523.
// Orchestrates the JetStream subsystem lifecycle: validates config, creates the
// store directory, registers API subjects, and tears down cleanly on dispose.
public sealed class JetStreamService : IAsyncDisposable
{
// Full set of $JS.API.> subjects registered at startup.
// Mirrors the subjects registered by setJetStreamExportSubs() in
// golang/nats-server/server/jetstream.go and jsApiSubs in jetstream_api.go.
private static readonly IReadOnlyList<string> AllApiSubjects =
[
"$JS.API.>",
JetStreamApiSubjects.Info,
JetStreamApiSubjects.StreamCreate + "*",
JetStreamApiSubjects.StreamUpdate + "*",
JetStreamApiSubjects.StreamDelete + "*",
JetStreamApiSubjects.StreamInfo + "*",
JetStreamApiSubjects.StreamNames,
JetStreamApiSubjects.StreamList,
JetStreamApiSubjects.StreamPurge + "*",
JetStreamApiSubjects.StreamMessageGet + "*",
JetStreamApiSubjects.StreamMessageDelete + "*",
JetStreamApiSubjects.StreamSnapshot + "*",
JetStreamApiSubjects.StreamRestore + "*",
JetStreamApiSubjects.StreamLeaderStepdown + "*",
JetStreamApiSubjects.ConsumerCreate + "*",
JetStreamApiSubjects.ConsumerDelete + "*.*",
JetStreamApiSubjects.ConsumerInfo + "*.*",
JetStreamApiSubjects.ConsumerNames + "*",
JetStreamApiSubjects.ConsumerList + "*",
JetStreamApiSubjects.ConsumerPause + "*.*",
JetStreamApiSubjects.ConsumerNext + "*.*",
JetStreamApiSubjects.DirectGet + "*",
JetStreamApiSubjects.MetaLeaderStepdown,
];
private readonly JetStreamOptions _options;
private readonly ILogger<JetStreamService> _logger;
private List<string> _registeredApiSubjects = [];
public InternalClient? InternalClient { get; }
public bool IsRunning { get; private set; }
/// <summary>
/// The API subjects registered with the server after a successful StartAsync.
/// Empty before start or after dispose.
/// </summary>
public IReadOnlyList<string> RegisteredApiSubjects => _registeredApiSubjects;
/// <summary>
/// Maximum streams limit from configuration. 0 means unlimited.
/// Maps to Go's JetStreamAccountLimits.MaxStreams.
/// </summary>
public int MaxStreams => _options.MaxStreams;
/// <summary>
/// Maximum consumers limit from configuration. 0 means unlimited.
/// Maps to Go's JetStreamAccountLimits.MaxConsumers.
/// </summary>
public int MaxConsumers => _options.MaxConsumers;
/// <summary>
/// Maximum memory store bytes from configuration. 0 means unlimited.
/// Maps to Go's JetStreamConfig.MaxMemory.
/// </summary>
public long MaxMemory => _options.MaxMemoryStore;
/// <summary>
/// Maximum file store bytes from configuration. 0 means unlimited.
/// Maps to Go's JetStreamConfig.MaxStore.
/// </summary>
public long MaxStore => _options.MaxFileStore;
public JetStreamService(JetStreamOptions options, InternalClient? internalClient = null)
: this(options, internalClient, NullLoggerFactory.Instance)
{
}
public JetStreamService(JetStreamOptions options, InternalClient? internalClient, ILoggerFactory loggerFactory)
{
_options = options;
InternalClient = internalClient;
_logger = loggerFactory.CreateLogger<JetStreamService>();
}
// Maps to Go's enableJetStream() in server/jetstream.go:414-523.
// Validates the store directory, creates it if absent, then registers all
// $JS.API.> subjects so inbound API messages can be routed.
public Task StartAsync(CancellationToken ct)
{
if (IsRunning)
{
_logger.LogDebug("JetStream is already running; ignoring duplicate StartAsync");
return Task.CompletedTask;
}
// Validate and create store directory when specified.
// Go: os.MkdirAll(cfg.StoreDir, defaultDirPerms) — jetstream.go:430-444.
if (!string.IsNullOrEmpty(_options.StoreDir))
{
if (Directory.Exists(_options.StoreDir))
{
_logger.LogDebug("JetStream store directory already exists: {StoreDir}", _options.StoreDir);
}
else
{
Directory.CreateDirectory(_options.StoreDir);
_logger.LogInformation("JetStream store directory created: {StoreDir}", _options.StoreDir);
}
}
else
{
_logger.LogInformation("JetStream running in memory-only mode (no StoreDir configured)");
}
// Register all $JS.API.> subjects.
// Go: setJetStreamExportSubs() — jetstream.go:489-494.
_registeredApiSubjects = [.. AllApiSubjects];
IsRunning = true;
_logger.LogInformation(
"JetStream started. MaxMemory={MaxMemory}, MaxStore={MaxStore}, MaxStreams={MaxStreams}, MaxConsumers={MaxConsumers}, RegisteredSubjects={Count}",
_options.MaxMemoryStore,
_options.MaxFileStore,
_options.MaxStreams,
_options.MaxConsumers,
_registeredApiSubjects.Count);
return Task.CompletedTask;
}
// Maps to Go's shutdown path in jetstream.go.
// Clears registered subjects and marks the service as not running.
public ValueTask DisposeAsync()
{
_registeredApiSubjects = [];
IsRunning = false;
_logger.LogInformation("JetStream stopped");
return ValueTask.CompletedTask;
}
}