Skip to content

Conversation

@j-fuxa
Copy link
Contributor

@j-fuxa j-fuxa commented Aug 21, 2025

Removed: Deprecated options
Changed: Docker samples (environment variables)
Changed: Telemetry (allow extra config data, normalize keys to lowercase)

@coderabbitai
Copy link

coderabbitai bot commented Aug 21, 2025

Walkthrough

Renamed compose/env variables; renamed endpoint APIs and updated tests to replication/node terminology; expanded ClusterVNodeOptions (many new option groups and certificate-store loading); TelemetryService reads IConfiguration and publishes config-driven telemetry; minor auth diagnostics and DriveStats/logging changes.

Changes

Cohort / File(s) Summary
Compose & shared env
docker-compose.yml, samples/server/docker-compose.yaml, shared.env
Renamed environment variables: EVENTSTORE_INT_IPEVENTSTORE_REPLICATION_IP, EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_ASEVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS, EVENTSTORE_HTTP_PORTEVENTSTORE_NODE_PORT, EVENTSTORE_INT_TCP_PORTEVENTSTORE_REPLICATION_PORT.
Mini node helpers
src/EventStore.Core.Tests/Helpers/MiniClusterNode.cs, src/EventStore.Core.Tests/Helpers/MiniNode.cs
Removed explicit DisableInternalTcpTls assignment; replaced fluent endpoint helpers with WithReplicationEndpointOn, WithExternalTcpOn, WithNodeEndpointOn and switched to AdvertiseNodeAs for node advertisement.
Cluster/endpoint public API & extensions
src/EventStore.Core/Configuration/ClusterVNodeOptions.cs, src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs
Major refactor: file‑scoped namespace, ClusterVNodeOptions expanded into many option groups (InterfaceOptions, DatabaseOptions, CertificateOptions, GrpcOptions, ProjectionOptions, etc.), added computed QuorumSize, new/renamed endpoint helpers (WithReplicationEndpointOn, WithExternalTcpOn, WithNodeEndpointOn, AdvertiseNodeAs, AdvertiseInternalHostAs), enhanced certificate-store loading, and improved unknown-option detection.
Options framework & config tests
src/EventStore.Core/Configuration/ClusterVNodeOptions.Framework.cs, src/EventStore.Core/XUnit.Tests/Configuration/ClusterVNodeOptionsTests.cs
Added array-style option aggregation in config loading (handles indexed keys like gossip seeds); added tests for gossip-seed array and NODE_IP via env/config.
XUnit tests — endpoint/TLS changes
src/EventStore.Core.XUnit.Tests/Configuration/.../*.cs
Updated tests to use replication/node endpoint APIs (WithReplicationEndpointOn, WithNodeEndpointOn, AdvertiseNodeAs), removed/adjusted TLS-related assertions/tests where API semantics changed.
Telemetry service & callers
src/EventStore.Core/Telemetry/TelemetryService.cs, src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs, src/EventStore.Core/ClusterVNode.cs
TelemetryService constructor now accepts IConfiguration; plugin telemetry keys are lowercased (first letter), service emits config-sourced telemetry (reads EventStore:Telemetry), tests and ClusterVNode updated to pass configuration.
Authentication diagnostics
src/EventStore.Core/Authentication/PassthroughAuthentication/PassthroughAuthenticationProvider.cs
PassthroughAuthenticationProvider now passes diagnosticsName: "PassthroughAuthentication" to base AuthenticationProviderBase constructor.
Drive stats & diagnostics
src/EventStore.SystemRuntime/Diagnostics/DriveStats.cs
Converted DriveStats from static to instance class, added Serilog logger, added DriveData record struct with UsedBytes/Usage, added exception handling and safer path/drive resolution.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant VNode as ClusterVNode
  participant Telemetry as TelemetryService
  participant Cfg as IConfiguration
  participant Pub as IPublisher
  participant Sink as ITelemetrySink

  VNode->>Telemetry: new TelemetryService(manager, options, configuration, publisher, sink, checkpoint, nodeId)
  note right of Telemetry #F3F7EA: stores IConfiguration for extra telemetry

  Pub-->>Telemetry: PluginTelemetry(evt)
  Telemetry->>Telemetry: Normalize keys (lower-first-letter)
  Telemetry->>Sink: Publish envelope { root: LowerFirstLetter(evt.Source), data }

  Telemetry->>Cfg: Read "EventStore:Telemetry"
  Cfg-->>Telemetry: Key/Value map
  Telemetry->>Telemetry: Normalize keys
  Telemetry->>Sink: Publish envelope { root: "telemetry", data }
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • yordis

Poem

I thump my paws: ports renamed, new names to sow,
Config seeds sprout where indexed arrays go.
Certificates found in stores I sniff and cheer,
Telemetry sings config-sourced tunes to hear.
Hop-hop, a carrot commit — I'm glad you're near. 🥕🐇

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch docker_telemetry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@j-fuxa j-fuxa marked this pull request as ready for review August 23, 2025 23:03
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (16)
src/EventStore.Core/Authentication/PassthroughAuthentication/PassthroughAuthenticationProvider.cs (1)

15-15: Avoid per-call allocation for supported schemes (minor)

["Insecure"] creates a new array on each call. If this is on a hot path, return a cached static instead.

Apply within this line:

-public override IReadOnlyList<string> GetSupportedAuthenticationSchemes() => ["Insecure"];
+public override IReadOnlyList<string> GetSupportedAuthenticationSchemes() => Schemes;

And add this class-scoped field (outside the selected line range):

private static readonly string[] Schemes = ["Insecure"];
src/EventStore.Core/Telemetry/TelemetryService.cs (2)

269-276: Emit “telemetry” only when there’s content; also normalize safely

Two tweaks:

  • If EventStore:Telemetry is empty, skip emitting an empty “telemetry” object.
  • Same collision risk as above; avoid ToDictionary to prevent exceptions on case-only duplicates.
- var extraTelemetry = _configuration.GetSection("EventStore:Telemetry").Get<Dictionary<string, string>>() ??
-                      [];
- var payload =
-   JsonSerializer.SerializeToNode(extraTelemetry.ToDictionary(kvp => LowerFirstLetter(kvp.Key),
-     kvp => kvp.Value));
- message.Envelope.ReplyWith(new TelemetryMessage.Response(
-   "telemetry", payload));
+ var configured = _configuration.GetSection("EventStore:Telemetry")
+   .Get<Dictionary<string, string>>() ?? new();
+ if (configured.Count > 0) {
+   var normalized = new Dictionary<string, string>(StringComparer.Ordinal);
+   foreach (var kvp in configured)
+     normalized[LowerFirstLetter(kvp.Key)] = kvp.Value;
+   var payload = JsonSerializer.SerializeToNode(normalized);
+   message.Envelope.ReplyWith(new TelemetryMessage.Response("telemetry", payload));
+ }

269-276: Security/PII: guard against sensitive keys in EventStore:Telemetry

Config-driven telemetry is powerful, but it can leak secrets if users set keys like "password/secret/token/apiKey". Consider masking known-sensitive suffixes/patterns before publishing.

I can add a small filter like: if (key.Contains("token", OrdinalIgnoreCase) || key.Contains("secret", ...)) value = "****".

Do you want me to wire this in with a unit test?

src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs (3)

75-95: Use UTC time for deterministic tests

Prefer DateTime.UtcNow to avoid timezone-dependent behavior and daylight-saving edge cases in future assertions or logs.

Apply this diff:

-           timeStamp: DateTime.Now,
+           timeStamp: DateTime.UtcNow,

137-144: Avoid brittle JSON string comparisons; assert on semantic JSON values

Comparing JsonObject.ToString() to a multi-line literal is formatting-sensitive. Assert on the actual JSON value(s) instead.

Apply this diff:

-       Assert.Equal("""
-                    {
-                      "baz": "qux"
-                    }
-                    """,
-           _sink.Data["fakeComponent"].ToString());
+       Assert.Equal("qux", _sink.Data["fakeComponent"]!["baz"]!.GetValue<string>());

147-154: Same here: assert on telemetry value rather than full JSON text

This avoids failures due to whitespace, ordering, or serializer differences.

Apply this diff:

-       Assert.Equal("""
-                    {
-                      "cloudIdentifier": "abc"
-                    }
-                    """,
-           _sink.Data["telemetry"].ToString());
+       Assert.Equal("abc", _sink.Data["telemetry"]!["cloudIdentifier"]!.GetValue<string>());
src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (3)

32-33: Avoid side effects in options constructor

Calling FileStreamExtensions.ConfigureFlush in the parameterless constructor couples object creation to a global side effect using default values, and won’t reflect later user-provided overrides (e.g., when UnsafeDisableFlushToDisk is set via configuration after construction).

Move this to the end of FromConfiguration(...) after binding, so the configured value is applied exactly once per load:

-public ClusterVNodeOptions() => FileStreamExtensions.ConfigureFlush(Database.UnsafeDisableFlushToDisk);
+public ClusterVNodeOptions() {}

And at the end of FromConfiguration (just before returning):

-       return options;
+       FileStreamExtensions.ConfigureFlush(options.Database.UnsafeDisableFlushToDisk);
+       return options;

60-93: Apply flush configuration after binding

Follow-up to the previous comment: ensure the flush behavior respects configured values by applying it here, post-bind.

Apply this diff at the end of the method:

 		var options = new ClusterVNodeOptions
 		{
 			// ... existing bindings ...
 			LoadedOptions = GetLoadedOptions(configurationRoot)
 		};
 
-		return options;
+		FileStreamExtensions.ConfigureFlush(options.Database.UnsafeDisableFlushToDisk);
+		return options;

563-567: Use central Opts defaults to avoid duplication

These defaults are already defined in Opts. Reuse them to keep a single source of truth.

Apply this diff:

- public int ConnectionPendingSendBytesThreshold { get; init; } = 10 * 1_024 * 1_024;
+ public int ConnectionPendingSendBytesThreshold { get; init; } = Opts.ConnectionPendingSendBytesThresholdDefault;
@@
- public int ConnectionQueueSizeThreshold { get; init; } = 50_000;
+ public int ConnectionQueueSizeThreshold { get; init; } = Opts.ConnectionQueueSizeThresholdDefault;
src/EventStore.Core.Tests/Helpers/MiniNode.cs (1)

181-182: AdvertiseNodeAs gating is sensible

Only applying advertisement when a host is provided keeps behavior backward compatible. Consider also guarding for whitespace-only strings, though not critical for tests.

src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (4)

138-141: Clarify AdvertiseExternalHostAs semantics (host-only) and relation to AdvertiseNodeAs

This method sets NodeHostAdvertiseAs only. That’s consistent with later using AdvertiseNodeAs to set the HTTP port. Please make this explicit in the XML docs to avoid confusion when teams expect this to also set the port.

Apply this doc-only diff:

 /// <summary>
-/// Sets up the External Host that would be advertised
+/// Sets the external host (host only) that will be advertised to clients/gossip.
+/// Note: This does not set the HTTP port; use <see cref="AdvertiseNodeAs"/> to set both host and port.
 /// </summary>

159-171: Fill in missing XML docs for AdvertiseNodeAs

The summary/param docs are empty. This API becomes the primary way to set the advertised HTTP endpoint (host+port) and deserves explicit documentation.

Apply this doc-only diff:

 /// <summary>
-/// 
+/// Sets the advertised node HTTP endpoint (host + port) used by clients and gossip.
 /// </summary>
-/// <param name="options">The <see cref="ClusterVNodeOptions"/></param>
-/// <param name="endPoint">The advertised host</param>
+/// <param name="options">The <see cref="ClusterVNodeOptions"/> to update.</param>
+/// <param name="endPoint">The endpoint whose host and port will be advertised.</param>
 /// <returns>A <see cref="ClusterVNodeOptions"/> with the options set</returns>

255-273: Pre-validate TrustedRootCertificatesPath to improve error signal

When using the filesystem path, pre-check for directory existence to fail fast with a clearer message before attempting enumeration.

Apply this diff:

- if (string.IsNullOrEmpty(options.Certificate.TrustedRootCertificatesPath))
+ if (string.IsNullOrEmpty(options.Certificate.TrustedRootCertificatesPath))
 {
   throw new InvalidConfigurationException(
     $"{nameof(options.Certificate.TrustedRootCertificatesPath)} must be specified unless insecure mode (--insecure) is set.");
 }
 
 Log.Information("Loading trusted root certificates.");
- foreach (var (fileName, cert) in CertificateUtils
+ var rootsPath = options.Certificate.TrustedRootCertificatesPath;
+ if (!System.IO.Directory.Exists(rootsPath))
+   throw new InvalidConfigurationException(
+     $"TrustedRootCertificatesPath does not exist: {rootsPath}");
+
+ foreach (var (fileName, cert) in CertificateUtils
-         .LoadAllCertificates(options.Certificate.TrustedRootCertificatesPath))
+         .LoadAllCertificates(rootsPath))
 {
   trustedRootCerts.Add(cert);
   Log.Information("Loading trusted root certificate file: {file}", fileName);
 }
 
 if (trustedRootCerts.Count == 0)
   throw new InvalidConfigurationException(
-    $"No trusted root certificate files were loaded from the specified path: {options.Certificate.TrustedRootCertificatesPath}");
+    $"No trusted root certificate files were loaded from the specified path: {rootsPath}");
 return trustedRootCerts;

41-44: Minor: avoid constructing a new ClusterVNodeOptions to get the default DB path

RunInMemory sets Db to new ClusterVNodeOptions().Database.Db. If there’s a canonical default value, consider centralizing it (e.g., a static readonly on DatabaseOptions) to avoid allocating and to make the default explicit.

src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_single_node_and_custom_settings.cs (2)

60-63: API rename usage LGTM; add a test to assert external TCP mapping explicitly

The migration to WithNodeEndpointOn/WithExternalTcpOn/WithReplicationEndpointOn looks correct. To catch the potential NodeIp overwrite discussed in the core extensions, add an explicit assertion for the external TCP endpoint and a scenario where HTTP and external TCP use different IPs.

Apply this test augmentation (safe if NodeInfo.ExternalSecureTcp exists):

   [Test]
   public void should_set_internal_tcp_endpoint()
   {
     Assert.AreEqual(_internalTcp, _node.NodeInfo.InternalSecureTcp);
   }
+
+  [Test]
+  public void should_set_external_tcp_endpoint()
+  {
+    Assert.AreEqual(_externalTcp, _node.NodeInfo.ExternalSecureTcp);
+  }

Optionally, add a divergence scenario to ensure WithExternalTcpOn does not overwrite the HTTP endpoint IP:

// New test fixture in the same file
[TestFixture(typeof(LogFormat.V2), typeof(string))]
[TestFixture(typeof(LogFormat.V3), typeof(uint))]
public class with_diverging_node_and_external_ips<TLogFormat, TStreamId> : SingleNodeScenario<TLogFormat, TStreamId> {
  private readonly IPEndPoint _httpEndPoint = new(IPAddress.Parse("127.1.0.1"), 1113);
  private readonly IPEndPoint _externalTcp  = new(IPAddress.Parse("127.2.0.2"), 1115);
  private readonly IPEndPoint _internalTcp  = new(IPAddress.Parse("127.0.0.1"), 1114);

  protected override ClusterVNodeOptions WithOptions(ClusterVNodeOptions options) =>
    options.WithNodeEndpointOn(_httpEndPoint)
           .WithExternalTcpOn(_externalTcp)
           .WithReplicationEndpointOn(_internalTcp);

  [Test] public void http_endpoint_should_remain_configured() =>
    Assert.AreEqual(_httpEndPoint, _node.NodeInfo.HttpEndPoint);

  [Test] public void external_tcp_should_match_provided_endpoint() =>
    Assert.AreEqual(_externalTcp, _node.NodeInfo.ExternalSecureTcp);
}

157-163: LGTM: Consolidated advertise API usage

Combining AdvertiseInternalHostAs (internal host+port) with AdvertiseNodeAs (HTTP host+port) is clear and mirrors the new split between replication and node endpoints.

Consider adding a companion test to confirm call ordering does not matter (i.e., calling AdvertiseNodeAs before AdvertiseExternalHostAs yields the same outcome). This will protect against future regressions in builder composition.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e669110 and 12a0c46.

📒 Files selected for processing (15)
  • docker-compose.yml (3 hunks)
  • samples/server/docker-compose.yaml (1 hunks)
  • shared.env (1 hunks)
  • src/EventStore.Core.Tests/Helpers/MiniClusterNode.cs (0 hunks)
  • src/EventStore.Core.Tests/Helpers/MiniNode.cs (2 hunks)
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_cluster_node_and_custom_settings.cs (4 hunks)
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_default_settings.cs (0 hunks)
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_secure_tcp.cs (3 hunks)
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_single_node_and_custom_settings.cs (2 hunks)
  • src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs (9 hunks)
  • src/EventStore.Core/Authentication/PassthroughAuthentication/PassthroughAuthenticationProvider.cs (1 hunks)
  • src/EventStore.Core/ClusterVNode.cs (1 hunks)
  • src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (1 hunks)
  • src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (1 hunks)
  • src/EventStore.Core/Telemetry/TelemetryService.cs (7 hunks)
💤 Files with no reviewable changes (2)
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_default_settings.cs
  • src/EventStore.Core.Tests/Helpers/MiniClusterNode.cs
🧰 Additional context used
🧬 Code graph analysis (5)
src/EventStore.Core/Authentication/PassthroughAuthentication/PassthroughAuthenticationProvider.cs (1)
src/EventStore.Core/Services/Transport/Http/Authentication/PassthroughHttpAuthenticationProvider.cs (1)
  • PassthroughAuthenticationProvider (17-23)
src/EventStore.Core/Telemetry/TelemetryService.cs (2)
src/EventStore.Core/Messaging/IEnvelope.cs (1)
  • ReplyWith (3-3)
src/EventStore.Core/Telemetry/TelemetryMessage.cs (2)
  • Response (26-34)
  • Response (36-37)
src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs (1)
src/EventStore.SystemRuntime/RuntimeInformation.cs (1)
  • RuntimeInformation (15-50)
src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (7)
src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (16)
  • ClusterVNodeOptions (16-19)
  • ClusterVNodeOptions (21-23)
  • ClusterVNodeOptions (25-34)
  • ClusterVNodeOptions (41-44)
  • ClusterVNodeOptions (52-55)
  • ClusterVNodeOptions (62-67)
  • ClusterVNodeOptions (76-82)
  • ClusterVNodeOptions (90-97)
  • ClusterVNodeOptions (105-107)
  • ClusterVNodeOptions (115-120)
  • ClusterVNodeOptions (128-130)
  • ClusterVNodeOptions (138-140)
  • ClusterVNodeOptions (148-157)
  • ClusterVNodeOptions (164-171)
  • X509Certificate2 (178-216)
  • X509Certificate2Collection (227-273)
src/EventStore.Core/Configuration/EventStoreConfiguration.cs (3)
  • IConfigurationRoot (13-54)
  • IConfigurationRoot (56-56)
  • Path (58-72)
src/EventStore.Core/Index/PTable.cs (1)
  • PTableVersions (22-35)
src/EventStore.Core/Configuration/TypeConverters.cs (3)
  • EndPoint (19-31)
  • GossipSeedConverter (34-57)
  • IPAddressConverter (59-67)
src/EventStore.Core/TransactionLog/Chunks/TFConsts.cs (1)
  • TFConsts (4-17)
src/EventStore.Core/Util/Opts.cs (1)
  • Opts (7-33)
src/EventStore.Core/Configuration/Sources/EventStoreConfigurationKeys.cs (4)
  • EventStoreConfigurationKeys (13-94)
  • EventStoreConfigurationKeys (26-40)
  • Normalize (43-72)
  • StripConfigurationPrefix (92-93)
src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (7)
src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_cluster_node_and_custom_settings.cs (9)
  • ClusterVNodeOptions (19-26)
  • ClusterVNodeOptions (87-88)
  • ClusterVNodeOptions (109-110)
  • ClusterVNodeOptions (129-137)
  • ClusterVNodeOptions (157-164)
  • ClusterVNodeOptions (185-192)
  • ClusterVNodeOptions (213-222)
  • ClusterVNodeOptions (246-250)
  • ClusterVNodeOptions (265-268)
src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_secure_tcp.cs (2)
  • ClusterVNodeOptions (21-33)
  • ClusterVNodeOptions (67-73)
src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_single_node_and_custom_settings.cs (3)
  • ClusterVNodeOptions (20-20)
  • ClusterVNodeOptions (39-40)
  • ClusterVNodeOptions (57-63)
src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (2)
  • ClusterVNodeOptions (32-32)
  • ClusterVNodeOptions (60-93)
src/EventStore.Common/Utils/EndpointExtensions.cs (2)
  • GetHost (23-31)
  • GetPort (43-50)
src/EventStore.Core/Certificates/CertificateUtils.cs (1)
  • CertificateUtils (13-315)
src/EventStore.Common/Utils/StringExtensions.cs (1)
  • IsNotEmptyString (14-17)
🔇 Additional comments (22)
src/EventStore.Core/Authentication/PassthroughAuthentication/PassthroughAuthenticationProvider.cs (2)

9-11: Telemetry diagnostics name added — good improvement

Passing diagnosticsName: "PassthroughAuthentication" to the base constructor improves observability and aligns with the PR’s telemetry objectives. No functional concerns here.


9-11: C# 12 Primary Constructor Support Confirmed

The repo’s src/Directory.Build.props sets

  • <TargetFramework>net8.0</TargetFramework>
  • <LangVersion>12.0</LangVersion>

This guarantees C# 12 support across all projects, so the primary‐constructor syntax in PassthroughAuthenticationProvider is valid and no changes are required.

shared.env (1)

7-7: LGTM: AtomPub toggle preserved

Keeping EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=true is consistent with the new naming and expected defaults for samples. No concerns here.

samples/server/docker-compose.yaml (1)

10-14: Confirmed: EVENTSTORE_NODE_PORT is supported in 24.2.0-jammy

I verified that the “NodePort” setting is exposed via the environment variable EVENTSTORE_NODE_PORT in the v24.2 Networking documentation, with a default of 2113 and the same 2113:2113 port mapping in Docker Compose (developers.eventstore.com). The previous HttpPort parameter (and its env var EVENTSTORE_HTTP_PORT) was deprecated in version 23.10.0 when the rename to NodePort/EVENTSTORE_NODE_PORT was introduced (developers.eventstore.com). No changes are required for the 24.2.0-jammy tag.

docker-compose.yml (2)

48-61: Same consistency holds for node2; LGTM

The replication IP and advertised node port are aligned with node2’s host mapping (2112). No issues.


70-82: Same consistency holds for node3; LGTM

Replication IP and advertised node port align with 2113 mapping. Looks good.

src/EventStore.Core/Telemetry/TelemetryService.cs (2)

42-71: Injecting IConfiguration into TelemetryService is the right move — call sites verified

All usages have been updated to include the new IConfiguration parameter:

• ClusterVNode.cs (line 1469)
• TelemetryServiceTests.cs (line 49)

No further changes required.


248-265: Telemetry payload grouping change verified in code/tests

I searched across C#, JSON, and MD files for patterns like "plugins": or standalone plugins in sinks, tests, and docs and found no internal consumers or tests assuming a top-level "plugins" grouping for telemetry messages. Internally, TelemetryService now emits each plugin’s snapshot as

TelemetryMessage.Response(lowerFirst(evt.Source), payload)

instead of nesting under "plugins". Please manually verify that any external telemetry consumers (sinks, dashboards, alerting rules) and all related documentation have been updated to handle this new top-level key format.

src/EventStore.Core/ClusterVNode.cs (1)

1469-1477: LGTM: TelemetryService now receives IConfiguration

The additional configuration parameter is correctly threaded through. The lifetime is managed by registering telemetryService as a singleton for disposal. No issues spotted.

src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs (1)

49-55: Constructor update to pass IConfiguration looks good

Injecting IConfiguration and asserting cloudIdentifier via in-memory config is correct and keeps the test focused on TelemetryService behavior. Nice.

src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (1)

511-515: Clarify parameter name in GrpcOptions.FromConfiguration

I wasn’t able to find any invocations of GrpcOptions.FromConfiguration(...) in the current codebase, so it’s unclear whether callers are passing the root IConfiguration or the specific GRPC section. To make the intent crystal-clear and reduce the risk of misuse, rename the parameter to emphasize that it must be an IConfigurationSection:

--- a/src/EventStore.Core/Configuration/ClusterVNodeOptions.cs
+++ b/src/EventStore.Core/Configuration/ClusterVNodeOptions.cs
@@ -511,7 +511,7 @@
-   internal static GrpcOptions FromConfiguration(IConfiguration configurationRoot) => new()
+   internal static GrpcOptions FromConfiguration(IConfiguration section) => new()
    {
-       KeepAliveInterval = configurationRoot.GetValue<int>(nameof(KeepAliveInterval)),
-       KeepAliveTimeout = configurationRoot.GetValue<int>(nameof(KeepAliveTimeout))
+       KeepAliveInterval = section.GetValue<int>(nameof(KeepAliveInterval)),
+       KeepAliveTimeout = section.GetValue<int>(nameof(KeepAliveTimeout))
    };

Please verify:

  • That all existing callers invoke this method with configurationRoot.GetSection("EventStore:Grpc") (or similar), not with the full root.
  • There are no remaining direct passes of the root IConfiguration into this method.
src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_cluster_node_and_custom_settings.cs (5)

135-137: API rename adoption looks correct

Swapping WithInternalTcpOn/WithHttpOn for WithReplicationEndpointOn/WithNodeEndpointOn and using AdvertiseNodeAs aligns with the new naming. Assertions match the expected gossip advertise endpoints.


161-164: Binding Any for replication endpoint is fine

Using IPAddress.Any here is consistent with selecting a non-loopback advertise host in the assertions.


189-192: Node endpoint rename is applied correctly

WithNodeEndpointOn replaces the previous HTTP builder; the subsequent assertion on HttpEndPoint reflects the configured port.


218-222: Mixed Any/Loopback + explicit AdvertiseNodeAs is coherent

The combination of Any for replication and explicit AdvertiseNodeAs for HTTP is consistent. Tests verify the resulting advertised endpoints; looks good.


247-250: New DefaultOpsPassword is exercised

Good to see environment-only defaults covered in tests along with DefaultAdminPassword.

src/EventStore.Core.Tests/Helpers/MiniNode.cs (1)

166-169: Endpoint rename usage LGTM

Switching to WithReplicationEndpointOn/WithExternalTcpOn/WithNodeEndpointOn matches the new API and maintains the same semantics for test setup.

src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_secure_tcp.cs (3)

24-25: TLS scenarios updated to new endpoint APIs

Using WithReplicationEndpointOn/WithExternalTcpOn with CertificateFile mirrors the previous secure-TCP intent. The internal secure endpoint assertion remains valid.


70-73: Secure(...) + new endpoints works as intended

Certificates are supplied via Secure(...) and the replication/external endpoints are set via the new APIs. Assertions are consistent.


104-106: Negative case still validates certificate requirement

Expecting an exception without certificates while configuring endpoints is the right guardrail test.

src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (2)

148-157: LGTM: Internal advertise maps host+port correctly

AdvertiseInternalHostAs configures ReplicationHostAdvertiseAs and ReplicationTcpPortAdvertiseAs together. This is consistent and avoids the split-host/port confusion.


25-34: LGTM: Builder API changes read clean and preserve semantics

Reload, pluggable component aggregation, cluster sizing, in-memory/on-disk toggles, Insecure/Secure paths, gossip seeds, replication and node endpoint setters look cohesive and predictable. Nice use of records/with-expressions.

Also applies to: 41-44, 52-55, 62-68, 76-83, 90-97, 115-121, 128-131

Comment on lines +3 to +4
EVENTSTORE_REPLICATION_PORT=1112
EVENTSTORE_NODE_PORT=2113
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Env var rename: replication/http → replication/node; verify image support

The renames to EVENTSTORE_REPLICATION_PORT and EVENTSTORE_NODE_PORT look aligned with the codebase-wide shift to replication/node semantics. Please double-check that the Docker images used in this repo (and in your deployment pipelines) recognize these env vars to avoid silent misconfigurations.

Run this from the repo root to spot usages and ensure these are the canonical names in the codebase:

If you're relying on the prebuilt docker image tag in samples (eventstore/eventstore:24.2.0-jammy), confirm that tag supports these new names in its docs/changelog.


🏁 Script executed:

#!/bin/bash
# Verify env var names are recognized by the server's option binding
rg -nP '\bEVENTSTORE_(REPLICATION|NODE)_(PORT|IP)\b|AdvertiseNodePortToClientAs|NodePortAdvertiseAs|ReplicationPort\b'

# Also list any leftover deprecated names to catch stragglers
rg -nP '\bEVENTSTORE_(INT(_TCP)?|HTTP)_'

Length of output: 4161


Verify env var usage and update deprecated samples

The codebase correctly recognizes EVENTSTORE_REPLICATION_PORT and EVENTSTORE_NODE_PORT, but there are still deprecated variables in one of the sample manifests that need updating to prevent misconfiguration:

• In samples/server/docker-compose-cluster.yaml, replace all occurrences of

EVENTSTORE_INT_IP=…

with

EVENTSTORE_REPLICATION_IP=…

(lines 25, 57, and 82).

Additionally, please confirm that the Docker image tag you’re using (e.g. eventstore/eventstore:24.2.0-jammy) supports the new environment variable names—EVENTSTORE_REPLICATION_PORT and EVENTSTORE_NODE_PORT—in its documentation or changelog to avoid silent mismatches.

🤖 Prompt for AI Agents
In shared.env around lines 3-4, the sample manifests still use the deprecated
EVENTSTORE_INT_IP variable; open samples/server/docker-compose-cluster.yaml and
replace all occurrences of EVENTSTORE_INT_IP with EVENTSTORE_REPLICATION_IP at
lines 25, 57, and 82, update any related comments to reflect the new name, and
verify the docker image tag used (e.g., eventstore/eventstore:24.2.0-jammy)
supports EVENTSTORE_REPLICATION_PORT and EVENTSTORE_NODE_PORT by checking the
image documentation/changelog and, if needed, bump or change the image tag to
one that documents support for the new environment variable names.

Comment on lines +199 to 201
Assert.Equal(Int32.Parse(_sink.Data["database"]["epochNumber"].ToString()),
_electionsDoneMessage.ProposalNumber);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: comparing epochNumber to proposalNumber

The test compares the sink’s database.epochNumber to ElectionsDone.ProposalNumber. These are not the same concept. The epoch number should be checked against the leader’s epoch number captured in mem1.

Apply this diff:

-       Assert.Equal(Int32.Parse(_sink.Data["database"]["epochNumber"].ToString()),
-           _electionsDoneMessage.ProposalNumber);
+       Assert.Equal(Int32.Parse(_sink.Data["database"]["epochNumber"].ToString()),
+           mem1.EpochNumber);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Assert.Equal(Int32.Parse(_sink.Data["database"]["epochNumber"].ToString()),
_electionsDoneMessage.ProposalNumber);
Assert.Equal(
Int32.Parse(_sink.Data["database"]["epochNumber"].ToString()),
mem1.EpochNumber);
🤖 Prompt for AI Agents
In src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs around
lines 199-201, the test incorrectly asserts that database.epochNumber equals
_electionsDoneMessage.ProposalNumber; instead assert that the sink’s
database.epochNumber equals the leader’s epoch number captured in mem1 (e.g.
mem1.Leader.EpochNumber or the variable that holds mem1’s leader epoch). Replace
the assertion to parse the sink value as before but compare it to mem1’s leader
epoch field rather than _electionsDoneMessage.ProposalNumber, keeping types
consistent (parse to int) and preserving existing test structure.

Comment on lines +105 to 108
public static ClusterVNodeOptions WithExternalTcpOn(
this ClusterVNodeOptions options, IPEndPoint endPoint) =>
options with { Interface = options.Interface with { NodeIp = endPoint.Address, } };

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Potential endpoint conflation: WithExternalTcpOn() writes to NodeIp

WithExternalTcpOn sets Interface.NodeIp to the endpoint's address. If a caller configures different IPs for the node HTTP endpoint and the external TCP endpoint, this will overwrite the node IP set by WithNodeEndpointOn, likely producing an incorrect HTTP binding/advertise state. At minimum, the XML docs should clarify this behavior; ideally, the method should target a dedicated external-TCP field (if one exists) and avoid touching NodeIp.

Please verify how Interface maps to NodeInfo/transport bindings and whether a distinct ExternalTcp IP/port exists in Interface. To locate usages and assess impact:

If External TCP should not affect NodeIp, update this method to configure the dedicated external TCP fields (or deprecate it in favor of the node/replication APIs) and add a guard to prevent overwriting NodeIp when it has already been explicitly set by WithNodeEndpointOn.


🏁 Script executed:

#!/bin/bash
# Show all call sites for endpoint configuration helpers
rg -nPU -C3 '\.(WithNodeEndpointOn|WithExternalTcpOn|WithReplicationEndpointOn)\s*\(' --type=cs

# Inspect where NodeIp influences bindings/advertise
rg -nP -C3 --type=cs '\bNodeIp\b|\bNodePort\b|\bNodeHostAdvertiseAs\b|\bNodePortAdvertiseAs\b'

# Check resolved endpoints on the node
rg -nP -C3 --type=cs '\bHttpEndPoint\b|\bExternalSecureTcp\b|\bInternalSecureTcp\b'

Length of output: 268647


Separate External TCP IP from NodeIp to Prevent Binding Conflation

The WithExternalTcpOn helper currently reuses Interface.NodeIp—the same field used by WithNodeEndpointOn for HTTP—leading to unintended overwrites when clients specify different IPs for HTTP vs. external TCP. We need a dedicated ExternalTcpIp (and optionally ExternalTcpPort) on the interface, update the helper and the node‐startup logic to use it, and preserve backwards compatibility by falling back to NodeIp when unset.

• Add new optional property to ClusterVNodeOptions.Interface

// in src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (partial record)
  [Description("The IP Address for the external TCP endpoint.")]
  public IPAddress? ExternalTcpIp { get; init; } = null;

• Update WithExternalTcpOn to set the new field rather than NodeIp

 public static ClusterVNodeOptions WithExternalTcpOn(
     this ClusterVNodeOptions options, IPEndPoint endPoint) =>
-      options with { Interface = options.Interface with { NodeIp = endPoint.Address, } };
+      options with { Interface = options.Interface with { ExternalTcpIp = endPoint.Address, } };

• Modify node startup in ClusterVNode.cs to use ExternalTcpIp when present

 // before constructing external TCP endpoints
-  var extTcp = disableExternalTcpTls && enableExternalTcp
-      ? new IPEndPoint(options.Interface.NodeIp, nodeTcpOptions.NodeTcpPort)
+  var extIp = options.Interface.ExternalTcpIp ?? options.Interface.NodeIp;
+  var extTcp = disableExternalTcpTls && enableExternalTcp
+      ? new IPEndPoint(extIp, nodeTcpOptions.NodeTcpPort)
     : null;
 var extSecTcp = !disableExternalTcpTls && enableExternalTcp
-      ? new IPEndPoint(options.Interface.NodeIp, nodeTcpOptions.NodeTcpPort)
+      ? new IPEndPoint(extIp, nodeTcpOptions.NodeTcpPort)
     : null;

• Add XML doc comments to clarify new behavior on both the extension method and the option field
• Update any failing tests or add coverage to ensure fallback behaviour remains unchanged when ExternalTcpIp is unset

🤖 Prompt for AI Agents
In src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs around
lines 105-108, the WithExternalTcpOn helper currently writes into
Interface.NodeIp and thus can overwrite HTTP node IPs; add a new optional
IPAddress? ExternalTcpIp (and optionally ExternalTcpPort) to the
ClusterVNodeOptions.Interface record (with XML doc comments and Description
attribute) in src/EventStore.Core/Configuration/ClusterVNodeOptions.cs, change
WithExternalTcpOn to set Interface.ExternalTcpIp (and port if added) instead of
NodeIp, update ClusterVNode.cs node-startup to prefer Interface.ExternalTcpIp
when present and fall back to Interface.NodeIp when ExternalTcpIp is null, add
XML docs to the extension and option field, and update or add tests to verify
that ExternalTcpIp takes precedence and that behavior falls back to NodeIp when
ExternalTcpIp is unset.

Comment on lines +256 to 259
var payload = JsonSerializer.SerializeToNode(
evt.Data.ToDictionary(kvp => LowerFirstLetter(kvp.Key), kvp => kvp.Value));
message.Envelope.ReplyWith(new TelemetryMessage.Response(LowerFirstLetter(evt.Source), payload));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid ToDictionary collisions and exceptions on key normalization

evt.Data.ToDictionary(...) will throw if two keys differ only by the first letter’s case (e.g., "Cloud" and "cloud") after LowerFirstLetter. Prefer last-write-wins into a normal Dictionary to eliminate the exception and make behavior predictable.

Apply:

- var payload = JsonSerializer.SerializeToNode(
-   evt.Data.ToDictionary(kvp => LowerFirstLetter(kvp.Key), kvp => kvp.Value));
- message.Envelope.ReplyWith(new TelemetryMessage.Response(LowerFirstLetter(evt.Source), payload));
+ var normalized = new Dictionary<string, object?>(StringComparer.Ordinal);
+ foreach (var kvp in evt.Data)
+   normalized[LowerFirstLetter(kvp.Key)] = kvp.Value;
+ var payload = JsonSerializer.SerializeToNode(normalized);
+ message.Envelope.ReplyWith(new TelemetryMessage.Response(LowerFirstLetter(evt.Source), payload));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var payload = JsonSerializer.SerializeToNode(
evt.Data.ToDictionary(kvp => LowerFirstLetter(kvp.Key), kvp => kvp.Value));
message.Envelope.ReplyWith(new TelemetryMessage.Response(LowerFirstLetter(evt.Source), payload));
}
var normalized = new Dictionary<string, object?>(StringComparer.Ordinal);
foreach (var kvp in evt.Data)
normalized[LowerFirstLetter(kvp.Key)] = kvp.Value;
var payload = JsonSerializer.SerializeToNode(normalized);
message.Envelope.ReplyWith(new TelemetryMessage.Response(LowerFirstLetter(evt.Source), payload));
🤖 Prompt for AI Agents
In src/EventStore.Core/Telemetry/TelemetryService.cs around lines 256 to 259,
replace the ToDictionary call (which throws on key collisions when keys differ
only by first-letter case) with building a regular Dictionary<string, object>
and assign entries using the normalized key (LowerFirstLetter) so later values
overwrite earlier ones (last-write-wins); then pass that Dictionary to
JsonSerializer.SerializeToNode and keep the rest of the message creation
unchanged.

Comment on lines +279 to 285
private static string LowerFirstLetter(string x)
{
if (string.IsNullOrEmpty(x) || char.IsLower(x[0]))
return x;

return $"{char.ToLower(x[0])}{x[1..]}";
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use culture-invariant lowercasing for stable key normalization

char.ToLower is culture-sensitive; use ToLowerInvariant to avoid locale-dependent output (e.g., Turkish-i).

- private static string LowerFirstLetter(string x)
+ private static string LowerFirstLetter(string x)
  {
    if (string.IsNullOrEmpty(x) || char.IsLower(x[0]))
      return x;

-   return $"{char.ToLower(x[0])}{x[1..]}";
+   return $"{char.ToLowerInvariant(x[0])}{x[1..]}";
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static string LowerFirstLetter(string x)
{
if (string.IsNullOrEmpty(x) || char.IsLower(x[0]))
return x;
return $"{char.ToLower(x[0])}{x[1..]}";
}
private static string LowerFirstLetter(string x)
{
if (string.IsNullOrEmpty(x) || char.IsLower(x[0]))
return x;
return $"{char.ToLowerInvariant(x[0])}{x[1..]}";
}
🤖 Prompt for AI Agents
In src/EventStore.Core/Telemetry/TelemetryService.cs around lines 279 to 285,
the helper LowerFirstLetter uses char.ToLower which is culture-sensitive;
replace the call with char.ToLowerInvariant(x[0]) so the first character is
lowercased in a culture-invariant way (keeping the rest of the logic intact) to
ensure stable key normalization across locales.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/EventStore.Core.XUnit.Tests/Configuration/ClusterVNodeOptionsTests.cs (1)

101-111: Avoid leaking process-wide env vars from tests

Environment.SetEnvironmentVariable("EVENTSTORE__CLUSTER_SIZE", "3") is not cleaned up and may affect subsequent tests in the process. Wrap in try/finally and clear the variable.

- Environment.SetEnvironmentVariable("EVENTSTORE__CLUSTER_SIZE", "3");
-
- var configuration = new ConfigurationBuilder()
+ const string varName = "EVENTSTORE__CLUSTER_SIZE";
+ Environment.SetEnvironmentVariable(varName, "3");
+ try
+ {
+   var configuration = new ConfigurationBuilder()
      .AddEnvironmentVariables()
      .AddEventStoreEnvironmentVariables()
      .Build();
 
- var options = ClusterVNodeOptions.FromConfiguration(configuration);
- options.Unknown.Options.Should().BeEmpty();
+   var options = ClusterVNodeOptions.FromConfiguration(configuration);
+   options.Unknown.Options.Should().BeEmpty();
+ }
+ finally
+ {
+   Environment.SetEnvironmentVariable(varName, null);
+ }
♻️ Duplicate comments (4)
src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs (1)

199-200: Bug: comparing epochNumber to proposalNumber

The test compares the sink's database.epochNumber to ElectionsDone.ProposalNumber. These are not the same concept. The epoch number should be checked against the leader's epoch number captured in mem1.

Apply this diff:

-       Assert.Equal(Int32.Parse(_sink.Data["database"]["epochNumber"].ToString()),
-           _electionsDoneMessage.ProposalNumber);
+       Assert.Equal(Int32.Parse(_sink.Data["database"]["epochNumber"].ToString()),
+           mem1.EpochNumber);
src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (3)

105-108: WithExternalTcpOn overwrites NodeIp; deprecate to prevent endpoint conflation

This helper sets Interface.NodeIp and can undo a previous WithNodeEndpointOn call, conflating external TCP with HTTP bindings. Either repoint it to a dedicated ExternalTcpIp (and optional port), or mark it obsolete in favor of WithNodeEndpointOn/WithReplicationEndpointOn.

Minimal, non-breaking mitigation via Obsolete attribute:

 /// <summary>
 /// Sets the external tcp endpoint to the specified value
 /// </summary>
+ [Obsolete("Use WithNodeEndpointOn (HTTP) and WithReplicationEndpointOn (internal TCP) instead. This method overwrites NodeIp.")]
 public static ClusterVNodeOptions WithExternalTcpOn(
   this ClusterVNodeOptions options, IPEndPoint endPoint) =>
   options with { Interface = options.Interface with { NodeIp = endPoint.Address, } };

Preferred long-term fix: introduce Interface.ExternalTcpIp (and port), update this helper to set it, and alter startup to use ExternalTcpIp ?? NodeIp. Happy to draft the full patch if you want.


176-216: Guard against partial store configuration when loading node certificate and improve diagnostics

If CertificateStoreLocation is set but CertificateStoreName is empty, GetCertificateStoreName will throw a generic parsing error. Fail fast with a clear configuration message and log the chosen store.

- if (!string.IsNullOrWhiteSpace(options.CertificateStore.CertificateStoreLocation))
- {
-   var location =
-     CertificateUtils.GetCertificateStoreLocation(options.CertificateStore.CertificateStoreLocation);
-   var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore.CertificateStoreName);
-   return (CertificateUtils.LoadFromStore(location, name, options.CertificateStore.CertificateSubjectName,
-     options.CertificateStore.CertificateThumbprint), null);
- }
+ if (!string.IsNullOrWhiteSpace(options.CertificateStore.CertificateStoreLocation))
+ {
+   if (string.IsNullOrWhiteSpace(options.CertificateStore.CertificateStoreName))
+     throw new InvalidConfigurationException(
+       "CertificateStoreName must be specified when CertificateStoreLocation is set.");
+
+   var location =
+     CertificateUtils.GetCertificateStoreLocation(options.CertificateStore.CertificateStoreLocation);
+   var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore.CertificateStoreName);
+   Log.Information("Loading node certificate from store: {location}/{name} subject={subject} thumbprint={thumbprint}",
+     location, name, options.CertificateStore.CertificateSubjectName, options.CertificateStore.CertificateThumbprint);
+   return (CertificateUtils.LoadFromStore(location, name, options.CertificateStore.CertificateSubjectName,
+     options.CertificateStore.CertificateThumbprint), null);
+ }
 
- if (!string.IsNullOrWhiteSpace(options.CertificateStore.CertificateStoreName))
- {
-   var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore.CertificateStoreName);
-   return (
-     CertificateUtils.LoadFromStore(name, options.CertificateStore.CertificateSubjectName,
-       options.CertificateStore.CertificateThumbprint), null);
- }
+ if (!string.IsNullOrWhiteSpace(options.CertificateStore.CertificateStoreName))
+ {
+   var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore.CertificateStoreName);
+   Log.Information("Loading node certificate from default store location: {name} subject={subject} thumbprint={thumbprint}",
+     name, options.CertificateStore.CertificateSubjectName, options.CertificateStore.CertificateThumbprint);
+   return (
+     CertificateUtils.LoadFromStore(name, options.CertificateStore.CertificateSubjectName,
+       options.CertificateStore.CertificateThumbprint), null);
+ }

227-273: Apply the same partial-config guard to trusted roots

Match the guard/logging pattern for TrustedRootCertificateStoreLocation/Name to avoid obscure exceptions and improve operability.

- if (!string.IsNullOrWhiteSpace(options.CertificateStore.TrustedRootCertificateStoreLocation))
- {
-   var location =
-     CertificateUtils.GetCertificateStoreLocation(options.CertificateStore
-       .TrustedRootCertificateStoreLocation);
-   var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore
-     .TrustedRootCertificateStoreName);
-   trustedRootCerts.Add(CertificateUtils.LoadFromStore(location, name,
-     options.CertificateStore.TrustedRootCertificateSubjectName,
-     options.CertificateStore.TrustedRootCertificateThumbprint));
-   return trustedRootCerts;
- }
+ if (!string.IsNullOrWhiteSpace(options.CertificateStore.TrustedRootCertificateStoreLocation))
+ {
+   if (string.IsNullOrWhiteSpace(options.CertificateStore.TrustedRootCertificateStoreName))
+     throw new InvalidConfigurationException(
+       "TrustedRootCertificateStoreName must be specified when TrustedRootCertificateStoreLocation is set.");
+
+   var location =
+     CertificateUtils.GetCertificateStoreLocation(options.CertificateStore
+       .TrustedRootCertificateStoreLocation);
+   var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore
+     .TrustedRootCertificateStoreName);
+   Log.Information("Loading trusted roots from store: {location}/{name} subject={subject} thumbprint={thumbprint}",
+     location, name, options.CertificateStore.TrustedRootCertificateSubjectName,
+     options.CertificateStore.TrustedRootCertificateThumbprint);
+   trustedRootCerts.Add(CertificateUtils.LoadFromStore(location, name,
+     options.CertificateStore.TrustedRootCertificateSubjectName,
+     options.CertificateStore.TrustedRootCertificateThumbprint));
+   return trustedRootCerts;
+ }
 
- if (!string.IsNullOrWhiteSpace(options.CertificateStore.TrustedRootCertificateStoreName))
- {
-   var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore
-     .TrustedRootCertificateStoreName);
-   trustedRootCerts.Add(CertificateUtils.LoadFromStore(name,
-     options.CertificateStore.TrustedRootCertificateSubjectName,
-     options.CertificateStore.TrustedRootCertificateThumbprint));
-   return trustedRootCerts;
- }
+ if (!string.IsNullOrWhiteSpace(options.CertificateStore.TrustedRootCertificateStoreName))
+ {
+   var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore
+     .TrustedRootCertificateStoreName);
+   Log.Information("Loading trusted roots from default store location: {name} subject={subject} thumbprint={thumbprint}",
+     name, options.CertificateStore.TrustedRootCertificateSubjectName,
+     options.CertificateStore.TrustedRootCertificateThumbprint);
+   trustedRootCerts.Add(CertificateUtils.LoadFromStore(name,
+     options.CertificateStore.TrustedRootCertificateSubjectName,
+     options.CertificateStore.TrustedRootCertificateThumbprint));
+   return trustedRootCerts;
+ }
🧹 Nitpick comments (11)
src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (7)

32-33: Constructor side-effect is acceptable but consider documenting ConfigureFlush behavior

Calling FileStreamExtensions.ConfigureFlush(Database.UnsafeDisableFlushToDisk) in the constructor is fine, but it globally affects flush behavior each time an instance is created. A short XML doc comment clarifying this side effect will prevent surprises during tests or tooling that instantiate options frequently.


256-264: Trusted roots default path: consider explicit null for “unset”

TrustedRootCertificatesPath defaulting to Locations.DefaultTrustedRootCertificateDirectory is convenient, but it makes it harder to detect “unset vs. defaulted”. If you need to distinguish those cases later, consider defaulting to null and setting the default in binding. Not blocking.


376-378: Use shared constant for HashCollisionReadLimit

Keep one source of truth by referencing Opts.HashCollisionReadLimitDefault.

- public int HashCollisionReadLimit { get; init; } = 100;
+ public int HashCollisionReadLimit { get; init; } = Opts.HashCollisionReadLimitDefault;

457-466: WriteStatsToDb accessor mutates StatsStorage in init; acceptable but surprising

The init accessor side-effectfully sets StatsStorage. It’s valid, but unexpected for an init-only property. A short doc comment or moving the coupling to FromConfiguration would make intent clearer. Not blocking.


563-568: Reuse Opts defaults for queue/bytes thresholds

Avoid duplicated literals; use the constants already defined in Opts to prevent drift.

- public int ConnectionPendingSendBytesThreshold { get; init; } = 10 * 1_024 * 1_024;
+ public int ConnectionPendingSendBytesThreshold { get; init; } = Opts.ConnectionPendingSendBytesThresholdDefault;- public int ConnectionQueueSizeThreshold { get; init; } = 50_000;
+ public int ConnectionQueueSizeThreshold { get; init; } = Opts.ConnectionQueueSizeThresholdDefault;

592-609: Projection defaults: consider centralizing in Opts

ProjectionsQueryExpiry and FaultOutOfOrderProjections have counterparts in Opts. Use the constants to keep defaults consistent across code paths and docs.

- public int ProjectionsQueryExpiry { get; init; } = 5;
+ public int ProjectionsQueryExpiry { get; init; } = Opts.ProjectionsQueryExpiryDefault;

- public bool FaultOutOfOrderProjections { get; init; } = false;
+ public bool FaultOutOfOrderProjections { get; init; } = Opts.FaultOutOfOrderProjectionsDefault;

620-684: Unknown options detection: make top-level filtering case-insensitive

When filtering out nested unknown keys, StartsWith uses culture-sensitive defaults and the HashSet for sections is case-sensitive. Use OrdinalIgnoreCase consistently to avoid missing sections where casing differs across providers.

- static HashSet<string> FindUnknownSections(IEnumerable<string> keys)
+ static HashSet<string> FindUnknownSections(IEnumerable<string> keys)
  {
-   var hashSet = new HashSet<string>();
+   var hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
    foreach (var key in keys.Where(key => key.Split(":").Length > 2))
      hashSet.Add(key[..key.LastIndexOf(':')]);
    return hashSet;
  }
…
- return unknownKeys
-   .Where(key => !unknownSections.Any(key.StartsWith));
+ return unknownKeys
+   .Where(key => !unknownSections.Any(section => key.StartsWith(section, StringComparison.OrdinalIgnoreCase)));
src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (1)

41-44: Avoid constructing a new ClusterVNodeOptions just to read the default DB path

new ClusterVNodeOptions() has side effects (see ConfigureFlush). Prefer the constant source for the default path.

 public static ClusterVNodeOptions RunInMemory(this ClusterVNodeOptions options) => options with
 {
-  Database = options.Database with { MemDb = true, Db = new ClusterVNodeOptions().Database.Db }
+  Database = options.Database with { MemDb = true, Db = Locations.DefaultDataDirectory }
 };
src/EventStore.Core/Configuration/ClusterVNodeOptions.Framework.cs (1)

206-215: DefaultValue formatting: consider consistent casing across platforms

Booleans are lower-case on non-Windows and default casing on Windows. For consistent CLI/docs output, consider normalizing to lower-case across platforms. Not blocking.

src/EventStore.Core.XUnit.Tests/Configuration/ClusterVNodeOptionsTests.cs (2)

210-220: NodeIp binding via env var: LGTM

Good to pin explicit IPv4. Consider an IPv6 case in a follow-up to validate converter behavior with bracketed addresses.


153-171: GossipSeed via single env var: consider using AddEventStoreEnvironmentVariables only

You already use AddEventStoreEnvironmentVariables to inject a scoped var; sticking to that avoids touching real process env and keeps tests hermetic. Not blocking.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 12a0c46 and 0a22075.

📒 Files selected for processing (17)
  • docker-compose.yml (3 hunks)
  • samples/server/docker-compose.yaml (1 hunks)
  • shared.env (1 hunks)
  • src/EventStore.Core.Tests/Helpers/MiniClusterNode.cs (0 hunks)
  • src/EventStore.Core.Tests/Helpers/MiniNode.cs (2 hunks)
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_cluster_node_and_custom_settings.cs (4 hunks)
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_default_settings.cs (0 hunks)
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_secure_tcp.cs (3 hunks)
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_single_node_and_custom_settings.cs (2 hunks)
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterVNodeOptionsTests.cs (3 hunks)
  • src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs (9 hunks)
  • src/EventStore.Core/Authentication/PassthroughAuthentication/PassthroughAuthenticationProvider.cs (1 hunks)
  • src/EventStore.Core/ClusterVNode.cs (1 hunks)
  • src/EventStore.Core/Configuration/ClusterVNodeOptions.Framework.cs (1 hunks)
  • src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (1 hunks)
  • src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (1 hunks)
  • src/EventStore.Core/Telemetry/TelemetryService.cs (7 hunks)
💤 Files with no reviewable changes (2)
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_default_settings.cs
  • src/EventStore.Core.Tests/Helpers/MiniClusterNode.cs
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/EventStore.Core/Authentication/PassthroughAuthentication/PassthroughAuthenticationProvider.cs
  • samples/server/docker-compose.yaml
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_secure_tcp.cs
  • docker-compose.yml
  • src/EventStore.Core.Tests/Helpers/MiniNode.cs
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_single_node_and_custom_settings.cs
  • src/EventStore.Core/Telemetry/TelemetryService.cs
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_cluster_node_and_custom_settings.cs
  • src/EventStore.Core/ClusterVNode.cs
  • shared.env
🧰 Additional context used
🧬 Code graph analysis (2)
src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (4)
src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (16)
  • ClusterVNodeOptions (16-19)
  • ClusterVNodeOptions (21-23)
  • ClusterVNodeOptions (25-34)
  • ClusterVNodeOptions (41-44)
  • ClusterVNodeOptions (52-55)
  • ClusterVNodeOptions (62-67)
  • ClusterVNodeOptions (76-82)
  • ClusterVNodeOptions (90-97)
  • ClusterVNodeOptions (105-107)
  • ClusterVNodeOptions (115-120)
  • ClusterVNodeOptions (128-130)
  • ClusterVNodeOptions (138-140)
  • ClusterVNodeOptions (148-157)
  • ClusterVNodeOptions (164-171)
  • X509Certificate2 (178-216)
  • X509Certificate2Collection (227-273)
src/EventStore.Core/Configuration/TypeConverters.cs (4)
  • EndPoint (19-31)
  • GossipSeedConverter (34-57)
  • GossipEndPointConverter (10-32)
  • IPAddressConverter (59-67)
src/EventStore.Core/Util/Opts.cs (1)
  • Opts (7-33)
src/EventStore.Core/Configuration/Sources/EventStoreConfigurationKeys.cs (3)
  • EventStoreConfigurationKeys (13-94)
  • EventStoreConfigurationKeys (26-40)
  • Normalize (43-72)
src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (4)
src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (2)
  • ClusterVNodeOptions (32-32)
  • ClusterVNodeOptions (60-94)
src/EventStore.Common/Utils/EndpointExtensions.cs (2)
  • GetHost (23-31)
  • GetPort (43-50)
src/EventStore.Core/Certificates/CertificateUtils.cs (1)
  • CertificateUtils (13-315)
src/EventStore.Common/Utils/StringExtensions.cs (1)
  • IsNotEmptyString (14-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build / ubuntu-24.04
  • GitHub Check: Build / noble
  • GitHub Check: Build / ubuntu-24.04-arm
  • GitHub Check: Build / bookworm-slim
  • GitHub Check: Docker Compose Smoke Test
🔇 Additional comments (22)
src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs (10)

2-2: LGTM!

The additional imports for System.Collections.Generic and Microsoft.Extensions.Configuration are appropriate for the new configuration-based telemetry setup.

Also applies to: 18-18


52-54: LGTM!

The configuration setup correctly provides the CloudIdentifier via in-memory configuration, which aligns with the updated TelemetryService constructor that now accepts IConfiguration.


76-95: LGTM!

The use of named parameters in the MemberInfo.ForVNode call improves code readability and maintainability.


122-122: LGTM!

The inline JsonObject initialization is more concise and readable.


137-143: LGTM!

The test correctly verifies the updated telemetry payload structure with the "fakeComponent" section containing "baz": "qux".


147-153: LGTM!

The test correctly verifies that the CloudIdentifier from configuration is included in the telemetry data.


185-185: LGTM!

Consistent use of inline JsonObject initialization improves readability.


203-204: LGTM!

These assertions correctly compare the database epoch number with the leader's epoch number from the respective messages.

Also applies to: 207-208


213-213: LGTM!

The updated class name "FakeComponent" (instead of "fakeComponent") follows proper C# naming conventions for class names.


217-220: LGTM!

The updated telemetry data with "enabled" and "Baz" keys aligns with the test expectations and the new payload structure.

src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (6)

96-106: Environment-only defaults for credentials look good

DefaultUserOptions correctly marks passwords as Sensitive and EnvironmentOnly. No issues.


212-230: Auth defaults consistent with prior behavior

AuthorizationType/AuthenticationType default to "internal" and deprecation note on first-level HTTP auth is clear. Looks good.


243-251: Add private-key password support: nice improvement

Support for CertificatePrivateKeyPassword (PKCS #8 encrypted keys) closes a common ops gap. LGTM.


350-351: QuorumSize calculation is correct

Integer math covers both single-node and multi-node clusters as expected.


481-496: Good additions for tunables (existence filter, scavenge backend sizing)

Defaults wired to Opts constants provide clarity and a single source of truth.


512-517: GrpcOptions.FromConfiguration may not read from the intended section

FromConfiguration uses raw property names against the provided IConfiguration rather than a section scoped to Grpc. If callers pass the root “EventStore” section, values won’t resolve from “EventStore:Grpc:...”. Please verify the call sites and consider binding from configuration.GetSection("Grpc").
Would you like a repo scan to find usages and confirm the section semantics?

src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (2)

115-121: Replication endpoint helper reads clearly and avoids conflation

Setting ReplicationIp and ReplicationPort together is the right call. LGTM.


128-131: HTTP/node endpoint helper: LGTM

Explicitly configuring NodeIp and NodePort keeps semantics clear.

src/EventStore.Core/Configuration/ClusterVNodeOptions.Framework.cs (3)

20-21: GetComponentName composition is clear

Component naming based on NodeIp/NodePort reads well.


96-112: Array-style option loading: pragmatic and effective

Handling indexed arrays when the parent key lacks a scalar value addresses GossipSeed cleanly. The comment reflects current scope; the logic is generic enough for other array options if added later. LGTM.


129-141: Provider source display name resolution looks solid

Recursive handling of SectionProvider helps produce accurate provenance.

src/EventStore.Core.XUnit.Tests/Configuration/ClusterVNodeOptionsTests.cs (1)

173-190: Array-based GossipSeed test: LGTM

Coverage for indexed configuration loading is valuable and matches the implemented behavior.

Changed docker samples (environment variables)
Changed Telemetry (allow extra config data, normalize keys to lowercase)
Fix support for gossip seeds provided as an array
Calculate in linux disk stats correctly for mount point instead of root

Signed-off-by: JOSE FUXA <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (4)
src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs (1)

199-201: Bug: comparing epochNumber to proposalNumber (duplicate of earlier review)

database.epochNumber should be compared to the leader’s epoch (mem1.EpochNumber), not ElectionsDone.ProposalNumber.

-       Assert.Equal(Int32.Parse(_sink.Data["database"]["epochNumber"].ToString()),
-           _electionsDoneMessage.ProposalNumber);
+       Assert.Equal(Int32.Parse(_sink.Data["database"]["epochNumber"].ToString()),
+           mem1.EpochNumber);
src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (3)

105-108: External TCP helper overwrites Node HTTP IP and ignores port

Setting Interface.NodeIp inside WithExternalTcpOn couples the external TCP binding to the HTTP/node binding and drops the provided port. This will overwrite a previously set node HTTP IP (e.g., via WithNodeEndpointOn) and can yield incorrect HTTP binds/advertise results; the port is also ignored.

Apply a dedicated ExternalTcp* option instead of Node*. Minimal fix in this file:

 public static ClusterVNodeOptions WithExternalTcpOn(
     this ClusterVNodeOptions options, IPEndPoint endPoint) =>
-    options with { Interface = options.Interface with { NodeIp = endPoint.Address, } };
+    options with {
+      Interface = options.Interface with {
+        ExternalTcpIp = endPoint.Address,
+        ExternalTcpPort = endPoint.Port
+      }
+    };

Outside this file, add the corresponding properties and prefer them when constructing external TCP endpoints; fall back to NodeIp/NodeTcpPort if unset. I can provide a full patch if helpful.

Run this quick scan to find call sites that combine HTTP and External TCP helpers (risk of overwrites):

#!/bin/bash
rg -nPU -C2 --type=cs '(?s)\.WithNodeEndpointOn\(.+?\).*?\.WithExternalTcpOn\('
rg -nPU -C2 --type=cs '(?s)\.WithExternalTcpOn\(.+?\).*?\.WithNodeEndpointOn\('
rg -nP --type=cs '\bNodeIp\b|\bExternalTcpIp\b|\bExternalTcpPort\b|\bNodePort\b'

188-204: Guard against partial store config and log chosen store when loading node certificate

If CertificateStoreLocation is set but CertificateStoreName is empty/whitespace, GetCertificateStoreName will throw with a parsing error. Fail fast with a clear configuration message and log the selected store for operability.

 if (!string.IsNullOrWhiteSpace(options.CertificateStore.CertificateStoreLocation))
 {
+  if (string.IsNullOrWhiteSpace(options.CertificateStore.CertificateStoreName))
+    throw new InvalidConfigurationException(
+      "CertificateStoreName must be specified when CertificateStoreLocation is set.");
+
   var location =
     CertificateUtils.GetCertificateStoreLocation(options.CertificateStore.CertificateStoreLocation);
   var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore.CertificateStoreName);
+  Log.Information("Loading node certificate from store: {location}/{name} subject={subject} thumbprint={thumbprint}",
+    location, name, options.CertificateStore.CertificateSubjectName, options.CertificateStore.CertificateThumbprint);
   return (CertificateUtils.LoadFromStore(location, name, options.CertificateStore.CertificateSubjectName,
     options.CertificateStore.CertificateThumbprint), null);
 }
 
 if (!string.IsNullOrWhiteSpace(options.CertificateStore.CertificateStoreName))
 {
   var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore.CertificateStoreName);
+  Log.Information("Loading node certificate from default store location: {name} subject={subject} thumbprint={thumbprint}",
+    name, options.CertificateStore.CertificateSubjectName, options.CertificateStore.CertificateThumbprint);
   return (
     CertificateUtils.LoadFromStore(name, options.CertificateStore.CertificateSubjectName,
       options.CertificateStore.CertificateThumbprint), null);
 }

232-253: Same guard/logging for trusted roots loaded from store

Mirror the node-certificate fix to avoid ambiguous exceptions and improve diagnostics.

 if (!string.IsNullOrWhiteSpace(options.CertificateStore.TrustedRootCertificateStoreLocation))
 {
+  if (string.IsNullOrWhiteSpace(options.CertificateStore.TrustedRootCertificateStoreName))
+    throw new InvalidConfigurationException(
+      "TrustedRootCertificateStoreName must be specified when TrustedRootCertificateStoreLocation is set.");
+
   var location =
     CertificateUtils.GetCertificateStoreLocation(options.CertificateStore
       .TrustedRootCertificateStoreLocation);
   var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore
     .TrustedRootCertificateStoreName);
+  Log.Information("Loading trusted roots from store: {location}/{name} subject={subject} thumbprint={thumbprint}",
+    location, name, options.CertificateStore.TrustedRootCertificateSubjectName,
+    options.CertificateStore.TrustedRootCertificateThumbprint);
   trustedRootCerts.Add(CertificateUtils.LoadFromStore(location, name,
     options.CertificateStore.TrustedRootCertificateSubjectName,
     options.CertificateStore.TrustedRootCertificateThumbprint));
   return trustedRootCerts;
 }
 
 if (!string.IsNullOrWhiteSpace(options.CertificateStore.TrustedRootCertificateStoreName))
 {
   var name = CertificateUtils.GetCertificateStoreName(options.CertificateStore
     .TrustedRootCertificateStoreName);
+  Log.Information("Loading trusted roots from default store location: {name} subject={subject} thumbprint={thumbprint}",
+    name, options.CertificateStore.TrustedRootCertificateSubjectName,
+    options.CertificateStore.TrustedRootCertificateThumbprint);
   trustedRootCerts.Add(CertificateUtils.LoadFromStore(name,
     options.CertificateStore.TrustedRootCertificateSubjectName,
     options.CertificateStore.TrustedRootCertificateThumbprint));
   return trustedRootCerts;
 }
🧹 Nitpick comments (20)
src/EventStore.SystemRuntime/Diagnostics/DriveStats.cs (5)

7-7: Make DriveStats static (no instance state).

The type was changed to non-static but only exposes static members. Marking it static clarifies intent and avoids accidental instantiation.

-public class DriveStats
+public static class DriveStats

9-9: Avoid exposing a public logger in the API surface.

Publishing a public static ILogger leaks implementation details and encourages external code to couple to your logging context. Prefer internal/private.

- public static readonly ILogger Log = Serilog.Log.ForContext<DriveStats>();
+ internal static readonly ILogger Log = Serilog.Log.ForContext<DriveStats>();

17-22: Use OS-appropriate, ordinal comparisons for file-system paths.

InvariantCultureIgnoreCase can misbehave with certain locales and is unnecessary for path matching. Use ordinal ignore-case on Windows and ordinal on Unix-like systems.

-			var diskName = "";
+			var diskName = "";
+			var comparison = Path.DirectorySeparatorChar == '\\'
+				? StringComparison.OrdinalIgnoreCase
+				: StringComparison.Ordinal;
 
 			foreach (var candidate in DriveInfo.GetDrives())
 			{
-				if (target.StartsWith(candidate.Name, StringComparison.InvariantCultureIgnoreCase) &&
-				    candidate.Name.StartsWith(diskName, StringComparison.InvariantCultureIgnoreCase))
+				if (target.StartsWith(candidate.Name, comparison) &&
+				    candidate.Name.StartsWith(diskName, comparison))
 				{
 					diskName = candidate.Name;
 				}
 			}

15-30: Handle non-ready drives explicitly (reduces exception noise).

Accessing TotalSize/AvailableFreeSpace on a non-ready drive can throw. You already catch exceptions; short-circuiting improves signal and avoids stack traces in common transient states.

 			var info = new DriveInfo(Path.GetFullPath(path));
 			var target = info.Name;
 			var diskName = "";
 
+			if (!info.IsReady)
+			{
+				Log.Warning("Drive {Drive} not ready when retrieving stats for {Path}", info.Name, path);
+				return new DriveData("Unknown", 0, 0);
+			}
+
 			foreach (var candidate in DriveInfo.GetDrives())

50-55: Prefer expression-bodied computed properties to avoid redundant backing fields.

Keeps DriveData minimal and derives values from the primaries without extra storage.

-	public long UsedBytes { get; } = TotalBytes - AvailableBytes;
+	public long UsedBytes => TotalBytes - AvailableBytes;
@@
-	public int Usage { get; } = (int)(TotalBytes != 0 ? (TotalBytes - AvailableBytes) * 100 / TotalBytes : 0);
+	public int Usage => TotalBytes != 0
+		? (int)((TotalBytes - AvailableBytes) * 100 / TotalBytes)
+		: 0;
src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs (4)

137-144: Make JSON assertions resilient (avoid pretty-print brittleness for fakeComponent)

Comparing JsonObject.ToString() to a hand-indented multi-line literal is brittle wrt whitespace/ordering. Assert on fields or compute the expected JSON using JsonObject to keep formatting consistent.

Suggested change:

-       Assert.NotNull(_sink.Data["fakeComponent"]);
-       Assert.Equal("""
-                    {
-                      "baz": "qux"
-                    }
-                    """,
-            _sink.Data["fakeComponent"].ToString());
+       Assert.NotNull(_sink.Data["fakeComponent"]);
+       Assert.Equal("qux", _sink.Data["fakeComponent"]!["baz"]!.ToString());

147-154: Same brittleness for telemetry.cloudIdentifier

Assert the value directly to avoid formatting issues.

-       Assert.NotNull(_sink.Data["telemetry"]);
-       Assert.Equal("""
-                    {
-                      "cloudIdentifier": "abc"
-                    }
-                    """,
-            _sink.Data["telemetry"].ToString());
+       Assert.NotNull(_sink.Data["telemetry"]);
+       Assert.Equal("abc", _sink.Data["telemetry"]!["cloudIdentifier"]!.ToString());

145-145: Swap expected/actual for clearer assertion messages

xUnit’s Assert.Equal takes expected first. Swap for readability in failures.

-       Assert.Equal(_sink.Data["environment"]!["os"]!.ToString(), RuntimeInformation.OSDescription);
+       Assert.Equal(RuntimeInformation.OSDescription, _sink.Data["environment"]!["os"]!.ToString());

38-41: Avoid disposing a Task; prefer synchronous wait pattern in ctor

using (...) on a Task is unnecessary. If you can’t await in a ctor, prefer GetAwaiter().GetResult() or move open into InitializeAsync().

-       using (var task = _db.Open(false).AsTask())
-       {
-           task.Wait();
-       }
+       _db.Open(false).GetAwaiter().GetResult();

If desired, I can move TFChunkDb.Open into InitializeAsync to avoid sync-over-async in the ctor.

src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (9)

64-69: Register TypeConverters only once to avoid repeated global mutation

TypeDescriptor.AddAttributes mutates global state and is called on every FromConfiguration invocation. Guard it so registration happens once.

Within the record, add a static flag:

+private static volatile bool s_typeConvertersRegistered;

Guard registrations:

-		TypeDescriptor.AddAttributes(typeof(EndPoint[]), new TypeConverterAttribute(typeof(GossipSeedConverter)));
-		TypeDescriptor.AddAttributes(typeof(EndPoint), new TypeConverterAttribute(typeof(GossipEndPointConverter)));
-		TypeDescriptor.AddAttributes(typeof(IPAddress), new TypeConverterAttribute(typeof(IPAddressConverter)));
+		if (!s_typeConvertersRegistered) {
+			TypeDescriptor.AddAttributes(typeof(EndPoint[]), new TypeConverterAttribute(typeof(GossipSeedConverter)));
+			TypeDescriptor.AddAttributes(typeof(EndPoint), new TypeConverterAttribute(typeof(GossipEndPointConverter)));
+			TypeDescriptor.AddAttributes(typeof(IPAddress), new TypeConverterAttribute(typeof(IPAddressConverter)));
+			s_typeConvertersRegistered = true;
+		}

159-162: Use Opts.SkipIndexScanOnReadsDefault to avoid duplicated defaults

This default already exists in Opts. Referencing it here keeps a single source of truth.

-		public bool SkipIndexScanOnReads { get; init; } = false;
+		public bool SkipIndexScanOnReads { get; init; } = Opts.SkipIndexScanOnReadsDefault;

376-379: Use Opts.HashCollisionReadLimitDefault for consistency

Avoid hardcoding defaults that already exist in Opts.

-		public int HashCollisionReadLimit { get; init; } = 100;
+		public int HashCollisionReadLimit { get; init; } = Opts.HashCollisionReadLimitDefault;

563-568: Align interface threshold defaults with Opts constants

Keeps defaults centralized and consistent.

-		public int ConnectionPendingSendBytesThreshold { get; init; } = 10 * 1_024 * 1_024;
+		public int ConnectionPendingSendBytesThreshold { get; init; } = Opts.ConnectionPendingSendBytesThresholdDefault;

-		public int ConnectionQueueSizeThreshold { get; init; } = 50_000;
+		public int ConnectionQueueSizeThreshold { get; init; } = Opts.ConnectionQueueSizeThresholdDefault;

602-605: Prefer Opts.ProjectionsQueryExpiryDefault

Same rationale: single source of truth.

-		public int ProjectionsQueryExpiry { get; init; } = 5;
+		public int ProjectionsQueryExpiry { get; init; } = Opts.ProjectionsQueryExpiryDefault;

457-466: Avoid type/property name shadowing in WriteStatsToDb logic

Using the enum type name StatsStorage and the property StatsStorage side-by-side is confusing. Fully-qualify the enum or rename the property to reduce ambiguity. Also note that binding both WriteStatsToDb and StatsStorage could conflict; the last-applied binding wins.

Option A (minimal): fully-qualify the enum in this block.

-			get => (StatsStorage.Stream & StatsStorage) != 0;
+			get => (EventStore.Core.Services.Monitoring.StatsStorage.Stream & StatsStorage) != 0;
 			init => StatsStorage =
-				value
-					? StatsStorage.StreamAndFile
-					: StatsStorage.File; // TODO SS: not sure if we should do this here
+				value
+					? EventStore.Core.Services.Monitoring.StatsStorage.StreamAndFile
+					: EventStore.Core.Services.Monitoring.StatsStorage.File; // TODO SS: not sure if we should do this here

Option B (clearer): rename the property StatsStorage to StatsStorageMode in this record to avoid shadowing (larger change).

Also applies to: 475-476


610-617: Prefer init-only setters for options to preserve immutability

Most options use init; these two use set. Unless you intend to mutate after construction, make them init for consistency.

-		public int ProjectionCompilationTimeout { get; set; } = 500;
+		public int ProjectionCompilationTimeout { get; init; } = 500;

-		public int ProjectionExecutionTimeout { get; set; } = DefaultProjectionExecutionTimeout;
+		public int ProjectionExecutionTimeout { get; init; } = DefaultProjectionExecutionTimeout;

620-684: Improve unknown-key suggestions by comparing normalized keys

Levenshtein distance on raw keys (mix of env, cli, and section formats) can degrade suggestions. Compute distance using normalized keys, but report stripped/original for UX.

-			static (string UnknownKey, string SuggestedKey) CreateUnknownOptionResult(IEnumerable<string> knownKeys,
-				string unknownKey, int distanceThreshold = 5)
+			static (string UnknownKey, string SuggestedKey) CreateUnknownOptionResult(IEnumerable<string> knownKeys,
+				string unknownKey, int distanceThreshold = 5)
 			{
-				var suggestion = knownKeys
-					.Select(key => (AllowedKey: key, Distance: Levenshtein.GetDistance(unknownKey, key)))
+				var normalizedUnknown = EventStoreConfigurationKeys.Normalize(unknownKey);
+				var suggestion = knownKeys
+					.Select(key => (AllowedKey: key, Distance: Levenshtein.GetDistance(normalizedUnknown, key)))
 					.MinBy(x => x.Distance);
 
 				return (
 					UnknownKey: EventStoreConfigurationKeys.StripConfigurationPrefix(unknownKey),
 					SuggestedKey: suggestion.Distance > distanceThreshold
 						? ""
 						: EventStoreConfigurationKeys.StripConfigurationPrefix(suggestion.AllowedKey)
 				);
 			}

512-517: Remove the unused GrpcOptions.FromConfiguration

A global search for any calls to GrpcOptions.FromConfiguration(…) returned no references outside its declaration in ClusterVNodeOptions.cs:512–517, confirming it isn’t used elsewhere in the codebase. Binding of the gRPC settings already occurs via

Grpc = configuration.BindOptions<GrpcOptions>()

in ClusterVNodeOptions.FromConfiguration(IConfigurationRoot) (see ClusterVNodeOptions.cs:85) and in the Extension methods for reloading.

Suggested refactor (optional):

  • Delete the internal static GrpcOptions FromConfiguration(IConfiguration configurationRoot) method to remove dead code.
  • OR, if you plan to expose a standalone factory, rename the parameter to configurationSection, switch the implementation to:
    internal static GrpcOptions FromConfiguration(IConfiguration configurationSection) {
        var opts = new GrpcOptions();
        configurationSection.Bind(opts);
        return opts;
    }
    and document that callers must pass the specific “Grpc” section.
src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (2)

21-24: Spelling: “Plugable” vs “Pluggable” leaks into API surface

The term is commonly spelled “Pluggable.” If the interface/type names are under our control, consider renaming to avoid long‑term API debt.

Would you like me to draft a follow-up PR with type/member renames and Obsolete shims?


159-164: Missing/empty XML summary for AdvertiseNodeAs

Please clarify intent and precedence vs. AdvertiseExternalHostAs. Example:

-  /// <summary>
-  /// </summary>
+  /// <summary>
+  /// Sets the advertised HTTP (node) host and port clients should use to reach the admin/UI/HTTP APIs.
+  /// Takes precedence over inferred values from bindings. Does not affect external TCP advertise fields.
+  /// </summary>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0a22075 and 8eced24.

📒 Files selected for processing (18)
  • docker-compose.yml (3 hunks)
  • samples/server/docker-compose.yaml (1 hunks)
  • shared.env (1 hunks)
  • src/EventStore.Core.Tests/Helpers/MiniClusterNode.cs (0 hunks)
  • src/EventStore.Core.Tests/Helpers/MiniNode.cs (2 hunks)
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_cluster_node_and_custom_settings.cs (4 hunks)
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_default_settings.cs (0 hunks)
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_secure_tcp.cs (3 hunks)
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_single_node_and_custom_settings.cs (2 hunks)
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterVNodeOptionsTests.cs (3 hunks)
  • src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs (9 hunks)
  • src/EventStore.Core/Authentication/PassthroughAuthentication/PassthroughAuthenticationProvider.cs (1 hunks)
  • src/EventStore.Core/ClusterVNode.cs (1 hunks)
  • src/EventStore.Core/Configuration/ClusterVNodeOptions.Framework.cs (1 hunks)
  • src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (1 hunks)
  • src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (1 hunks)
  • src/EventStore.Core/Telemetry/TelemetryService.cs (7 hunks)
  • src/EventStore.SystemRuntime/Diagnostics/DriveStats.cs (1 hunks)
💤 Files with no reviewable changes (2)
  • src/EventStore.Core.Tests/Helpers/MiniClusterNode.cs
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_default_settings.cs
🚧 Files skipped from review as they are similar to previous changes (9)
  • samples/server/docker-compose.yaml
  • docker-compose.yml
  • src/EventStore.Core/Authentication/PassthroughAuthentication/PassthroughAuthenticationProvider.cs
  • src/EventStore.Core/Telemetry/TelemetryService.cs
  • src/EventStore.Core.Tests/Helpers/MiniNode.cs
  • src/EventStore.Core/ClusterVNode.cs
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_cluster_node_and_custom_settings.cs
  • src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_single_node_and_custom_settings.cs
  • shared.env
🧰 Additional context used
🧬 Code graph analysis (4)
src/EventStore.Core.XUnit.Tests/Configuration/ClusterVNodeOptionsTests.cs (3)
src/EventStore.Common.Tests/Configuration/ConfigurationRootExtensionsTest.cs (3)
  • Fact (10-24)
  • Fact (26-39)
  • Fact (41-54)
src/EventStore.Core/Configuration/ClusterVNodeOptions.Framework.cs (1)
  • ClusterVNodeOptions (23-51)
src/EventStore.Core/Configuration/TypeConverters.cs (1)
  • EndPoint (19-31)
src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs (1)
src/EventStore.SystemRuntime/RuntimeInformation.cs (1)
  • RuntimeInformation (15-50)
src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (4)
src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (16)
  • ClusterVNodeOptions (16-19)
  • ClusterVNodeOptions (21-23)
  • ClusterVNodeOptions (25-34)
  • ClusterVNodeOptions (41-44)
  • ClusterVNodeOptions (52-55)
  • ClusterVNodeOptions (62-67)
  • ClusterVNodeOptions (76-82)
  • ClusterVNodeOptions (90-97)
  • ClusterVNodeOptions (105-107)
  • ClusterVNodeOptions (115-120)
  • ClusterVNodeOptions (128-130)
  • ClusterVNodeOptions (138-140)
  • ClusterVNodeOptions (148-157)
  • ClusterVNodeOptions (164-171)
  • X509Certificate2 (178-216)
  • X509Certificate2Collection (227-273)
src/EventStore.Core/Configuration/TypeConverters.cs (4)
  • EndPoint (19-31)
  • GossipSeedConverter (34-57)
  • GossipEndPointConverter (10-32)
  • IPAddressConverter (59-67)
src/EventStore.Core/Util/Opts.cs (1)
  • Opts (7-33)
src/EventStore.Core/Configuration/Sources/EventStoreConfigurationKeys.cs (3)
  • EventStoreConfigurationKeys (13-94)
  • EventStoreConfigurationKeys (26-40)
  • Normalize (43-72)
src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (7)
src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_cluster_node_and_custom_settings.cs (9)
  • ClusterVNodeOptions (19-26)
  • ClusterVNodeOptions (87-88)
  • ClusterVNodeOptions (109-110)
  • ClusterVNodeOptions (129-137)
  • ClusterVNodeOptions (157-164)
  • ClusterVNodeOptions (185-192)
  • ClusterVNodeOptions (213-222)
  • ClusterVNodeOptions (246-250)
  • ClusterVNodeOptions (265-268)
src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_secure_tcp.cs (2)
  • ClusterVNodeOptions (21-33)
  • ClusterVNodeOptions (67-73)
src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_single_node_and_custom_settings.cs (3)
  • ClusterVNodeOptions (20-20)
  • ClusterVNodeOptions (39-40)
  • ClusterVNodeOptions (57-63)
src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (2)
  • ClusterVNodeOptions (32-32)
  • ClusterVNodeOptions (60-94)
src/EventStore.Common/Utils/EndpointExtensions.cs (2)
  • GetHost (23-31)
  • GetPort (43-50)
src/EventStore.Core/Certificates/CertificateUtils.cs (1)
  • CertificateUtils (13-315)
src/EventStore.Common/Utils/StringExtensions.cs (1)
  • IsNotEmptyString (14-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build / ubuntu-24.04
  • GitHub Check: Build / ubuntu-24.04-arm
  • GitHub Check: Build / bookworm-slim
  • GitHub Check: Docker Compose Smoke Test
  • GitHub Check: Build / noble
🔇 Additional comments (14)
src/EventStore.SystemRuntime/Diagnostics/DriveStats.cs (2)

31-35: Good resilience: logged failure with safe fallback.

Catching exceptions, logging with context, and returning a sentinel value avoids cascading failures in diagnostics paths. LGTM.


28-28: Confirm and document free-space semantics for DriveData.AvailableBytes

DriveStats.GetDriveInfo currently uses DriveInfo.AvailableFreeSpace (user-quota aware) to populate DriveData.AvailableBytes. That choice propagates into:

  • Metrics (SystemMetrics) → DriveUsedBytes & DriveTotalBytes (usage = UsedBytes/TotalBytes)
  • Monitoring (SystemStatsHelper) → sys-drive-…-availableBytes, usedBytes, usage, totalBytes
  • Telemetry (MachineInfo) → only TotalBytes (no free-space metric)

If your consumers expect raw, volume-level free space (ignoring per-account quotas), switch to DriveInfo.TotalFreeSpace. Otherwise, to avoid confusion, please:

  • Update the XML docs in DriveStats.GetDriveInfo and DriveData to explicitly state that AvailableBytes is user-quota aware.
  • Consider adding a comment at the instantiation site to call out the choice.

Locations to update:

  • src/EventStore.SystemRuntime/Diagnostics/DriveStats.cs (line 28 – constructor call)
  • src/EventStore.SystemRuntime/Diagnostics/DriveStats.cs (lines 43–44 – <param name="AvailableBytes">)

Suggested diff for clarity:

--- a/src/EventStore.SystemRuntime/Diagnostics/DriveStats.cs
+++ b/src/EventStore.SystemRuntime/Diagnostics/DriveStats.cs
@@ public static DriveData GetDriveInfo(string path)
-           return new DriveData(diskName, info.TotalSize, info.AvailableFreeSpace);
+           // Using AvailableFreeSpace (user-quota aware).  
+           // To measure raw, volume-level free space, replace with info.TotalFreeSpace.
+           return new DriveData(
+               diskName,
+               info.TotalSize,
+               info.AvailableFreeSpace
+           );
@@ <param name="AvailableBytes">The available free space on the disk in bytes.</param>
-/// <param name="AvailableBytes">The available free space on the disk in bytes.</param>
+/// <param name="AvailableBytes">
+///     The amount of free space available to this process’s account (user-quota aware).
+///     Use DriveInfo.TotalFreeSpace for raw, volume-level free space.
+/// </param>
src/EventStore.Core.XUnit.Tests/Telemetry/TelemetryServiceTests.cs (2)

52-55: Good: IConfiguration injection for cloudIdentifier

Passing IConfiguration into TelemetryService and supplying EventStore:Telemetry:CloudIdentifier via AddInMemoryCollection is clean and testable. This aligns the test with the new constructor.


213-221: Nice: test covers normalization to lowercase

Publishing “Baz” and asserting “baz” in sink verifies the new normalization logic.

src/EventStore.Core.XUnit.Tests/Configuration/ClusterVNodeOptionsTests.cs (2)

173-190: Good coverage: array-based GossipSeed binding

This test validates binding EndPoint[] from EventStore:GossipSeed:{index} keys. Nice to see both IP and DNS cases.


210-220: Good: environment variable binding for NodeIp

Validates EVENTSTORE_NODE_IP → Interface.NodeIp. Straightforward and helpful regression coverage.

src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests/when_building/with_secure_tcp.cs (3)

24-24: API migration looks correct

Switch from WithInternalSecureTcpOn/WithExternalSecureTcpOn to WithReplicationEndpointOn/WithExternalTcpOn is applied correctly here.


70-72: Consistent endpoint updates with Secure()

Chaining WithReplicationEndpointOn + WithExternalTcpOn before Secure(...) is consistent with the new API layout.


104-105: Potential IP conflation via WithExternalTcpOn (see extension implementation)

WithExternalTcpOn currently sets Interface.NodeIp under the hood (see ClusterVNodeOptionsExtensions). If a test sets HTTP endpoint via WithNodeEndpointOn to a different IP, WithExternalTcpOn may overwrite it. Ensure call order reflects intent, or address this in the extension (see my comment on the extension file).

src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (4)

350-351: QuorumSize computation looks correct

For single-node clusters quorum is 1; otherwise it’s majority (N/2 + 1). This is standard and matches expected semantics.


498-505: gRPC defaults look reasonable

KeepAliveInterval/Timeout defaults at 10s are sane and match typical gRPC server keepalive configurations.


218-230: Auth options: backwards-compatibility flag documented and default-safe

DisableFirstLevelHttpAuthorization is default false with clear description. Looks good.


70-88: LoadedOptions and GetLoadedOptions definitions confirmed
Both the LoadedOptions property and the GetLoadedOptions method are implemented in the companion partial:

  • src/EventStore.Core/Configuration/ClusterVNodeOptions.Framework.cs:
    • Line 58: public IReadOnlyDictionary<string, LoadedOption> LoadedOptions { get; init; } = new Dictionary<string, LoadedOption>();
    • Line 82: public static IReadOnlyDictionary<string, LoadedOption> GetLoadedOptions(IConfigurationRoot configurationRoot) { … }

No compilation issues—no changes needed.

src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs (1)

115-121: Endpoint helpers look consistent; LGTM

  • WithReplicationEndpointOn correctly sets ReplicationIp/Port.
  • WithNodeEndpointOn correctly sets NodeIp/Port.
  • AdvertiseInternalHostAs sets both ReplicationHostAdvertiseAs and ReplicationTcpPortAdvertiseAs.

No changes requested.

Also applies to: 128-131, 148-157

Comment on lines +32 to +33
public ClusterVNodeOptions() => FileStreamExtensions.ConfigureFlush(Database.UnsafeDisableFlushToDisk);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

ConfigureFlush runs before configuration is bound — final setting may be ignored

Calling ConfigureFlush in the constructor uses the default Database.UnsafeDisableFlushToDisk value (false) and doesn’t reflect the configuration-bound value. Ensure the final, bound value wins by invoking ConfigureFlush after binding in FromConfiguration. Keeping the constructor call is harmless if you also add the post-bind call (see next comment). If you prefer a single source of truth, remove it here.

Apply one of the following:

Option A (preferred): keep constructor as-is and add a post-bind call (see lines 93-94 comment).

Option B (remove here, rely only on post-bind):

-public ClusterVNodeOptions() => FileStreamExtensions.ConfigureFlush(Database.UnsafeDisableFlushToDisk);
+public ClusterVNodeOptions() { }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public ClusterVNodeOptions() => FileStreamExtensions.ConfigureFlush(Database.UnsafeDisableFlushToDisk);
++ b/src/EventStore.Core/Configuration/ClusterVNodeOptions.cs
@@ Lines 32-33
public ClusterVNodeOptions() { }
🤖 Prompt for AI Agents
In src/EventStore.Core/Configuration/ClusterVNodeOptions.cs around lines 32-33,
ConfigureFlush is being called in the constructor using the default
Database.UnsafeDisableFlushToDisk (so configuration-bound values may be
ignored); fix by ensuring ConfigureFlush is invoked after configuration binding
in FromConfiguration (around lines 93-94) so the bound value wins — preferred:
keep the constructor call and add a post-bind call to
FileStreamExtensions.ConfigureFlush(Database.UnsafeDisableFlushToDisk) in
FromConfiguration; alternatively, if you prefer a single source, remove the
constructor call and only call ConfigureFlush after binding.

Comment on lines +93 to +94
return options;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Ensure flush-to-disk honors loaded configuration

After binding all option groups, set the FileStream flush behavior using the bound value. This guarantees the effective configuration is applied even if the constructor previously configured a default.

-		return options;
+		// Ensure FileStream flush behavior matches the loaded configuration
+		FileStreamExtensions.ConfigureFlush(options.Database.UnsafeDisableFlushToDisk);
+		return options;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return options;
}
// Ensure FileStream flush behavior matches the loaded configuration
FileStreamExtensions.ConfigureFlush(options.Database.UnsafeDisableFlushToDisk);
return options;
}
🤖 Prompt for AI Agents
In src/EventStore.Core/Configuration/ClusterVNodeOptions.cs around lines 93-94,
after binding all option groups and before returning options, update the
FileStream flush-to-disk setting to use the bound configuration value (e.g.,
assign the bound flush-to-disk flag or call the FileStream options setter with
the bound value) so the effective configuration overrides any constructor
default; ensure this assignment uses the freshly bound options property and
occurs prior to the return.

Comment on lines +96 to +112
// Handle array-style options; currently only GossipSeed uses this
if (sourceDisplayName is "<UNKNOWN>" && value is null)
{
var parentPath = option.Value.Key;
var childValues = new List<string>();

foreach (var childKey in provider.GetChildKeys([], parentPath))
{
var absoluteChildKey = parentPath + ":" + childKey;
if (!provider.TryGet(absoluteChildKey, out var childValue) || childValue is null) continue;
childValues.Add(childValue);
sourceDisplayName = GetSourceDisplayName(absoluteChildKey, provider);
}

value = string.Join(", ", childValues);
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Array-style options handling is gated behind a successful TryGet; likely misses providers where parent has no value

The new block only runs if provider.TryGet(optionKey, out value) succeeded and value is null. For common providers (e.g., InMemory, environment), TryGet("EventStore:GossipSeed") returns false when only children exist (GossipSeed:0, GossipSeed:1), so this path is skipped and LoadedOptions will miss the setting.

Refactor to probe children when TryGet fails or value is null. Also set a hasValue flag and skip only if no value or children found.

Proposed refactor:

-               foreach (var option in Metadata.SelectMany(x => x.Options)) {
-                   if (!provider.TryGet(option.Value.Key, out var value)) continue;
-
-                   var title = GetTitle(option);
-                   var sourceDisplayName = GetSourceDisplayName(option.Value.Key, provider);
-                   var isDefault = provider.GetType() == typeof(EventStoreDefaultValuesConfigurationProvider);
-
-                   // Handle array-style options; currently only GossipSeed uses this
-                   if (sourceDisplayName is "<UNKNOWN>" && value is null)
-                   {
-                       var parentPath = option.Value.Key;
-                       var childValues = new List<string>();
-
-                       foreach (var childKey in provider.GetChildKeys([], parentPath))
-                       {
-                           var absoluteChildKey = parentPath + ":" + childKey;
-                           if (!provider.TryGet(absoluteChildKey, out var childValue) || childValue is null) continue;
-                           childValues.Add(childValue);
-                           sourceDisplayName = GetSourceDisplayName(absoluteChildKey, provider);
-                       }
-
-                       value = string.Join(", ", childValues);
-                   }
-
-                   loadedOptions[option.Value.Key] = new(
+               foreach (var option in Metadata.SelectMany(x => x.Options)) {
+                   var title = GetTitle(option);
+                   var isDefault = provider.GetType() == typeof(EventStoreDefaultValuesConfigurationProvider);
+                   string? value;
+                   var hasValue = provider.TryGet(option.Value.Key, out value);
+                   var sourceDisplayName = GetSourceDisplayName(option.Value.Key, provider);
+
+                   if (!hasValue || value is null) {
+                       var parentPath = option.Value.Key;
+                       var childValues = new List<string>();
+                       foreach (var childKey in provider.GetChildKeys([], parentPath)) {
+                           var absoluteChildKey = parentPath + ":" + childKey;
+                           if (!provider.TryGet(absoluteChildKey, out var childValue) || childValue is null) continue;
+                           childValues.Add(childValue);
+                           sourceDisplayName = GetSourceDisplayName(absoluteChildKey, provider);
+                           hasValue = true;
+                       }
+                       if (childValues.Count > 0)
+                           value = string.Join(", ", childValues);
+                   }
+
+                   if (!hasValue) continue;
+
+                   loadedOptions[option.Value.Key] = new(
                        metadata: option.Value,
                        title: title,
                        value: value,
                        sourceDisplayName: sourceDisplayName,
                        isDefault: isDefault
                    );
                }

Consider adding a small unit test around GetLoadedOptions to assert that a provider with keys EventStore:GossipSeed:0/1 is surfaced with a comma-joined value in LoadedOptions.


🏁 Script executed:

#!/bin/bash
# Display the context around the foreach to verify current implementation
rg -nC3 'foreach\s*\(var option in Metadata\.SelectMany' src/EventStore.Core/Configuration/ClusterVNodeOptions.Framework.cs

Length of output: 434


🏁 Script executed:

#!/bin/bash
# Display the first 200 lines of the file to verify the context and ensure the snippet matches the review scope
sed -n '1,200p' src/EventStore.Core/Configuration/ClusterVNodeOptions.Framework.cs

Length of output: 7832


Refactor GetLoadedOptions to surface array-style options when the parent key has no direct value

The current implementation in src/EventStore.Core/Configuration/ClusterVNodeOptions.Framework.cs (around lines 96–112) only attempts to gather child values when

provider.TryGet(option.Value.Key, out var value)

returns true—even if the parent key has no value. For providers like the environment or InMemory providers, TryGet("EventStore:GossipSeed") will return false when only children (GossipSeed:0, GossipSeed:1, etc.) exist, causing those array-style settings to be skipped entirely.

Refactor so that you:

  • First call TryGet to determine if the parent has a direct value
  • Always probe GetChildKeys when either there was no direct value or it was null
  • Only skip adding the option if neither a direct value nor any children were found

Proposed diff:

@@ foreach (var option in Metadata.SelectMany(x => x.Options)) {
-   if (!provider.TryGet(option.Value.Key, out var value)) continue;

-   var title = GetTitle(option);
-   var sourceDisplayName = GetSourceDisplayName(option.Value.Key, provider);
-   var isDefault = provider.GetType() == typeof(EventStoreDefaultValuesConfigurationProvider);

-   // Handle array-style options; currently only GossipSeed uses this
-   if (sourceDisplayName is "<UNKNOWN>" && value is null)
-   {
-       var parentPath = option.Value.Key;
-       var childValues = new List<string>();

-       foreach (var childKey in provider.GetChildKeys([], parentPath))
-       {
-           var absoluteChildKey = parentPath + ":" + childKey;
-           if (!provider.TryGet(absoluteChildKey, out var childValue) || childValue is null) continue;
-           childValues.Add(childValue);
-           sourceDisplayName = GetSourceDisplayName(absoluteChildKey, provider);
-       }

-       value = string.Join(", ", childValues);
-   }

-   loadedOptions[option.Value.Key] = new(
+   var title = GetTitle(option);
+   var isDefault = provider.GetType() == typeof(EventStoreDefaultValuesConfigurationProvider);
+   string? value;
+   var hasValue = provider.TryGet(option.Value.Key, out value);
+   var sourceDisplayName = GetSourceDisplayName(option.Value.Key, provider);

+   if (!hasValue || value is null) {
+       var parentPath = option.Value.Key;
+       var childValues = new List<string>();
+
+       foreach (var childKey in provider.GetChildKeys([], parentPath)) {
+           var absoluteChildKey = $"{parentPath}:{childKey}";
+           if (!provider.TryGet(absoluteChildKey, out var childValue) || childValue is null) continue;
+           childValues.Add(childValue);
+           sourceDisplayName = GetSourceDisplayName(absoluteChildKey, provider);
+           hasValue = true;
+       }
+
+       if (childValues.Count > 0)
+           value = string.Join(", ", childValues);
+   }
+
+   if (!hasValue) continue;
+
+   loadedOptions[option.Value.Key] = new(
        metadata: option.Value,
        title: title,
        value: value,
        sourceDisplayName: sourceDisplayName,
        isDefault: isDefault
    );

Additionally, please add a small unit test for GetLoadedOptions that verifies a configuration provider containing keys like EventStore:GossipSeed:0 and EventStore:GossipSeed:1 yields a single entry for EventStore:GossipSeed with a comma-joined value.

🤖 Prompt for AI Agents
In src/EventStore.Core/Configuration/ClusterVNodeOptions.Framework.cs around
lines 96 to 112, the code only gathers child values when the parent key already
had a value, causing array-style settings (e.g., GossipSeed:0, GossipSeed:1) to
be skipped if the parent key has no direct value; change the logic to first
attempt provider.TryGet(option.Value.Key, out var value) to capture any direct
value, then always call provider.GetChildKeys(...) when value is null or TryGet
returned false to collect child entries, set sourceDisplayName from the first
child found, join child values into value if any exist, and only skip adding the
option when neither a direct value nor any child values were found; also add a
unit test for GetLoadedOptions that creates a config provider containing
EventStore:GossipSeed:0 and EventStore:GossipSeed:1 and asserts there is a
single loaded option for EventStore:GossipSeed whose value is the two entries
joined by a comma.

Comment on lines +138 to 141
public static ClusterVNodeOptions
AdvertiseExternalHostAs(this ClusterVNodeOptions options, EndPoint endPoint) =>
options with { Interface = options.Interface with { NodeHostAdvertiseAs = endPoint.GetHost(), } };

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

AdvertiseExternalHostAs writes to NodeHostAdvertiseAs and collides with AdvertiseNodeAs

  • AdvertiseExternalHostAs currently sets Interface.NodeHostAdvertiseAs (and drops the port entirely).
  • AdvertiseNodeAs also sets Interface.NodeHostAdvertiseAs and NodePortAdvertiseAs.
  • Using both in one configuration (as tests do) creates last-write-wins conflicts and loses the external advertise host/port.

Route external advertising to dedicated properties and include the port:

 public static ClusterVNodeOptions
   AdvertiseExternalHostAs(this ClusterVNodeOptions options, EndPoint endPoint) =>
-  options with { Interface = options.Interface with { NodeHostAdvertiseAs = endPoint.GetHost(), } };
+  options with {
+    Interface = options.Interface with {
+      ExternalHostAdvertiseAs = endPoint.GetHost(),
+      ExternalTcpPortAdvertiseAs = endPoint.GetPort()
+    }
+  };

Optionally, have AdvertiseNodeAs guard against accidental override when External* is already set, or document precedence rules in XML docs.

To locate potential conflicts in the repo:

Also applies to: 164-171


🏁 Script executed:

#!/bin/bash
# Spots places where both advertise helpers are used together
rg -nPU -C2 --type=cs '(?s)\.AdvertiseExternalHostAs\(.+?\).*?\.AdvertiseNodeAs\('
rg -nPU -C2 --type=cs '(?s)\.AdvertiseNodeAs\(.+?\).*?\.AdvertiseExternalHostAs\('
# Confirm whether dedicated External* advertise fields exist
rg -nP --type=cs '\bExternalHostAdvertiseAs\b|\bExternalTcpPortAdvertiseAs\b|\bNodeHostAdvertiseAs\b|\bNodePortAdvertiseAs\b'

Length of output: 19291


Separate external-advertise settings from existing NodeHostAdvertiseAs

AdvertiseExternalHostAs and AdvertiseNodeAs both write to the same NodeHostAdvertiseAs/NodePortAdvertiseAs fields, causing last-write-wins collisions (and dropping the port in AdvertiseExternalHostAs). To correctly support distinct external/http advertise endpoints, we need to introduce dedicated External* properties, update the extension methods, and adjust the gossip-builder logic and tests accordingly.

Please address the following critical fixes:

• Add new properties in the Interface record (src/EventStore.Core/Configuration/ClusterVNodeOptions.cs):
public string? ExternalHostAdvertiseAs { get; init; } = null;
public int ExternalTcpPortAdvertiseAs { get; init; } = 0;

• Update AdvertiseExternalHostAs (src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs, ~L138):

 public static ClusterVNodeOptions
     AdvertiseExternalHostAs(this ClusterVNodeOptions options, EndPoint endPoint) =>
-    options with { Interface = options.Interface with { NodeHostAdvertiseAs = endPoint.GetHost(), } };
+    options with { Interface = options.Interface with {
+        ExternalHostAdvertiseAs   = endPoint.GetHost(),
+        ExternalTcpPortAdvertiseAs = endPoint.GetPort()
+    }};

• Optionally guard or document in AdvertiseNodeAs (src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs, ~L169) to avoid overriding External* when already set, e.g.:

if (options.Interface.ExternalHostAdvertiseAs != null) 
    throw new InvalidOperationException("External advertise host already configured");

—or add XML-doc rules on precedence.

• Adjust the gossip-builder in ClusterVNode (src/EventStore.Core/ClusterVNode.cs around L872 and L916) to prefer ExternalHostAdvertiseAs/ExternalTcpPortAdvertiseAs over NodeHostAdvertiseAs/NodePortAdvertiseAs when constructing the external HTTP endpoint.

• Update or add unit tests in src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests to verify:
– ExternalHostAdvertiseAs + ExternalTcpPortAdvertiseAs correctly populate GossipAdvertiseInfo.HttpEndPoint.
– AdvertiseNodeAs continues to set the internal cluster tcp advertise values.

These changes are required to avoid breaking existing behavior while correctly separating external HTTP advertise endpoints.

🤖 Prompt for AI Agents
In src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs around
lines 138-141, AdvertiseExternalHostAs currently writes into
Interface.NodeHostAdvertiseAs causing collisions with AdvertiseNodeAs and
dropping the port; add new External* properties to the Interface record in
src/EventStore.Core/Configuration/ClusterVNodeOptions.cs (public string?
ExternalHostAdvertiseAs { get; init; } = null; and public int
ExternalTcpPortAdvertiseAs { get; init; } = 0), update AdvertiseExternalHostAs
to set these new ExternalHostAdvertiseAs/ExternalTcpPortAdvertiseAs fields
instead of the existing Node* fields, optionally add a guard in AdvertiseNodeAs
(around src/EventStore.Core/Configuration/ClusterVNodeOptionsExtensions.cs
~L169) to throw or document if External* are already set to avoid silent
overrides, modify the gossip-builder logic in
src/EventStore.Core/ClusterVNode.cs (around L872 and L916) to prefer
ExternalHostAdvertiseAs/ExternalTcpPortAdvertiseAs when composing the external
HTTP endpoint and fall back to NodeHostAdvertiseAs/NodePortAdvertiseAs
otherwise, and update/add unit tests in
src/EventStore.Core.XUnit.Tests/Configuration/ClusterNodeOptionsTests to assert
ExternalHostAdvertiseAs + ExternalTcpPortAdvertiseAs populate
GossipAdvertiseInfo.HttpEndPoint and that AdvertiseNodeAs still sets internal
cluster TCP advertise values.

Comment on lines +19 to +28
foreach (var candidate in DriveInfo.GetDrives())
{
if (target.StartsWith(candidate.Name, StringComparison.InvariantCultureIgnoreCase) &&
candidate.Name.StartsWith(diskName, StringComparison.InvariantCultureIgnoreCase))
{
diskName = candidate.Name;
}
}

return new DriveData(diskName, info.TotalSize, info.AvailableFreeSpace);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Ensure DiskName is non-empty (UNC/network paths, container mountpoints).

DriveInfo.GetDrives may not include UNC roots (e.g., \server\share) or certain container overlays, leaving diskName as "". Fall back to the detected root (target) to avoid empty DiskName in telemetry.

 			foreach (var candidate in DriveInfo.GetDrives())
 			{
 				if (target.StartsWith(candidate.Name, StringComparison.InvariantCultureIgnoreCase) &&
 				    candidate.Name.StartsWith(diskName, StringComparison.InvariantCultureIgnoreCase))
 				{
 					diskName = candidate.Name;
 				}
 			}
 
+			if (string.IsNullOrEmpty(diskName))
+				diskName = target;
+
 			return new DriveData(diskName, info.TotalSize, info.AvailableFreeSpace);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
foreach (var candidate in DriveInfo.GetDrives())
{
if (target.StartsWith(candidate.Name, StringComparison.InvariantCultureIgnoreCase) &&
candidate.Name.StartsWith(diskName, StringComparison.InvariantCultureIgnoreCase))
{
diskName = candidate.Name;
}
}
return new DriveData(diskName, info.TotalSize, info.AvailableFreeSpace);
foreach (var candidate in DriveInfo.GetDrives())
{
if (target.StartsWith(candidate.Name, StringComparison.InvariantCultureIgnoreCase) &&
candidate.Name.StartsWith(diskName, StringComparison.InvariantCultureIgnoreCase))
{
diskName = candidate.Name;
}
}
if (string.IsNullOrEmpty(diskName))
diskName = target;
return new DriveData(diskName, info.TotalSize, info.AvailableFreeSpace);
🤖 Prompt for AI Agents
In src/EventStore.SystemRuntime/Diagnostics/DriveStats.cs around lines 19 to 28,
the loop that matches DriveInfo entries can leave diskName empty for UNC/network
paths or container mountpoints; update the code to ensure DiskName is never
empty by falling back to the detected root: if diskName is null or empty after
the loop, set it to the target root (use Path.GetPathRoot(target) or the
original target if no root is available) before constructing DriveData so
telemetry always has a non-empty DiskName.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant