Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,24 +152,41 @@ Part:
### Handshake (the easiest place to get burned)

The handshake does **not** use the standard frame format — it's an ad-hoc
byte sequence. Translate it byte-for-byte from
`cppcache/src/TcrConnection.cpp::sendHandshakeForServer`. **Do not work
from memory.**
byte sequence. **Authoritative reference is the Java code, not cppcache** —
when they disagree, the Java server wins:

- client side: `geode-core/.../cache/client/internal/ClientSideHandshakeImpl.java::write`
- server side: `geode-core/.../cache/tier/sockets/ServerSideHandshakeImpl.java`
- shared : `geode-core/.../cache/tier/sockets/Handshake.java` (constants, helpers)

cppcache `TcrConnection.cpp::sendHandshakeForServer` is a parallel
implementation with stale comments; cross-check before trusting it. **Do
not work from memory.**

```
client → server:
ConnectionType u8 (100 = client-to-server)
ReplyOk u8 (59)
ProtocolVersion (major.minor.patch + ordinal)
ClientProxyMembershipID (serialised: host / PID / UUID / durable id)
Credentials (optional Properties)
ConnectionType u8 (100 = CLIENT_TO_SERVER, 101/102 = notification)
ProtocolVersion (ordinal only; 1 byte if ≤ 127, else sentinel + i16)
ReplyOk u8 (59)
ReadTimeout i32 (request/response only; notification writes port list instead)
ClientProxyMembershipID (one DataSerializable object on the wire:
FixedIDByte u8 = 1
DSFid u8 = 38
identity varint length + bytes
uniqueId i32)
Overrides[] u8 × N (currently always N = 1: conflation byte)
SecurityMode u8 (0 = none, 1 = normal + creds body, 3 = multi-user notification)
[Credentials body] (only when SecurityMode != none)

server → client:
AcceptanceCode u8 (38 = OK)
ServerQueueStatus u8
QueueSize i32
ServerMember (membership ID)
DeltaEnabled u8
AcceptanceCode u8 (59 = OK; 60 REFUSED / 61 INVALID / 66 AUTH_NOT_REQUIRED /
67 SERVER_IS_LOCATOR / 21 SSL_REQUIRED on rejection)
EndpointType u8 (subscription/queue role — drain in MVP)
QueueSize i32 (subscription queue size — drain in MVP)
ServerMember (DataSerializable membership ID — drain in MVP)
Message (UTF-8 str) (server diagnostic / refusal text; empty on success,
u16 length prefix)
DeltaEnabled u8 (bool) (delta propagation flag — drain in MVP)
```

### MVP MessageType subset
Expand Down
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="coverlet.collector" Version="10.0.0" />
<PackageVersion Include="Xunit.DependencyInjection" Version="11.2.1" />
</ItemGroup>
<ItemGroup Label="IntegrationTest">
<PackageVersion Include="Testcontainers" Version="4.0.0" />
</ItemGroup>
<ItemGroup Label="Build">
<PackageVersion Include="MinVer" Version="6.0.0" />
</ItemGroup>
</Project>
</Project>
4 changes: 4 additions & 0 deletions src/Geode.Client/Geode.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
<MinVerDefaultPreReleaseIdentifiers>alpha.0</MinVerDefaultPreReleaseIdentifiers>
</PropertyGroup>

<PropertyGroup>
<NoWarn>1701;1702;CA1873</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
Expand Down
63 changes: 63 additions & 0 deletions src/Geode.Client/GeodeClientServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Geode.Client.Options;
using Geode.Client.Protocol;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Geode.Client;

/// <summary>
/// DI registration entry point for the Geode managed client.
/// </summary>
public static class GeodeClientServiceCollectionExtensions
{
/// <summary>
/// Register the Geode client services and bind
/// <see cref="GeodeClientOptions"/> from
/// <paramref name="configuration"/> (typically the <c>"Geode"</c>
/// section of <c>appsettings.json</c>).
/// </summary>
/// <example>
/// <code>
/// builder.Services.AddGeodeClient(
/// builder.Configuration.GetSection("Geode"));
/// </code>
/// </example>
/// <remarks>
/// <para>
/// Phase 2 surface — registers the bare minimum needed to open and
/// handshake a single connection:
/// </para>
/// <list type="bullet">
/// <item><see cref="GeodeClientOptions"/> bound from configuration.</item>
/// <item>
/// <see cref="ClientProxyMembershipIdBuilder"/> as a <b>singleton</b>
/// — process-scoped uniqueTag and identity-bytes cache must be
/// shared across all connections.
/// </item>
/// <item>
/// <see cref="TcrConnection"/> as <b>transient</b> — every borrow
/// yields a fresh connection. Phase 6 will replace this with a
/// pooled lifetime.
/// </item>
/// </list>
/// <para>
/// Logging is intentionally not registered here; callers are expected
/// to add their own <c>ILoggerFactory</c> via <c>AddLogging()</c> /
/// <c>AddHttpLogging()</c> / Serilog / etc. so the Geode client picks
/// up whatever logging stack the host already configured.
/// </para>
/// </remarks>
public static IServiceCollection AddGeodeClient(
this IServiceCollection services,
IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configuration);

services.AddOptions<GeodeClientOptions>().Bind(configuration);
services.AddSingleton<ClientProxyMembershipIdBuilder>();
services.AddTransient<TcrConnection>();

return services;
}
}
30 changes: 30 additions & 0 deletions src/Geode.Client/GeodeException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace Geode.Client;

/// <summary>
/// Base exception for Geode-specific protocol-level failures: server-side
/// refusals (e.g. handshake rejection), malformed wire bytes, and exceptions
/// returned by the server in <c>MessageType.Exception</c> replies.
/// </summary>
/// <remarks>
/// <para>
/// Distinct from <see cref="System.IO.IOException"/> /
/// <see cref="System.Net.Sockets.SocketException"/> which surface for
/// genuine transport failures, and from
/// <see cref="System.InvalidOperationException"/> which is reserved for API
/// misuse (e.g. <c>SendAsync</c> before <c>ConnectAsync</c>).
/// </para>
/// <para>
/// Catch this type to handle "the Geode server said something we couldn't
/// proceed with" without swallowing unrelated BCL failures.
/// </para>
/// </remarks>
public class GeodeException : Exception
{
public GeodeException() { }

public GeodeException(string message)
: base(message) { }

public GeodeException(string message, Exception innerException)
: base(message, innerException) { }
}
47 changes: 47 additions & 0 deletions src/Geode.Client/Options/GeodeClientOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
namespace Geode.Client.Options;

/// <summary>
/// User-facing configuration for the Geode client. Bound from the
/// <c>"Geode"</c> section of <c>appsettings.json</c> via
/// <c>IOptions&lt;GeodeClientOptions&gt;</c> and consumed by the (Phase 5)
/// <c>AddGeodeClient(...)</c> DI extension.
/// </summary>
/// <remarks>
/// <para>
/// Property set is derived from cppcache <c>SystemProperties</c> (file
/// <c>cppcache/include/geode/SystemProperties.hpp</c> + defaults in
/// <c>cppcache/src/SystemProperties.cpp</c>). The following cppcache
/// fields are intentionally <b>omitted</b> because the .NET runtime /
/// our architecture replaces them:
/// </para>
/// <list type="bullet">
/// <item>statistic-* (use <c>EventCounters</c> / OpenTelemetry).</item>
/// <item>log-* (use <c>ILogger</c> + filter levels).</item>
/// <item>heap-lru-* / tombstone-timeout (server-side concepts).</item>
/// <item>suspended-tx-timeout / bucket-wait-timeout (out of MVP scope).</item>
/// <item>max-fe-threads / enable-chunk-handler-thread (.NET ThreadPool managed).</item>
/// <item>security-client-dhalgo (Diffie-Hellman creds — deprecated upstream).</item>
/// <item>on-client-disconnect-clear-pdxType-Ids (Phase 11 PDX).</item>
/// <item>cache-xml-file (CLAUDE.md cuts <c>cache.xml</c> entirely).</item>
/// </list>
/// </remarks>
public class GeodeClientOptions
{
/// <summary>
/// Distributed-system / client name shown in server logs. Mirrors
/// cppcache <c>name</c>. Default empty.
/// </summary>
public string Name { get; set; } = string.Empty;

/// <summary>Connection-pool tuning. See <see cref="PoolOptions"/>.</summary>
public PoolOptions Pool { get; } = new();

/// <summary>TLS / SSL settings. See <see cref="TlsOptions"/>.</summary>
public TlsOptions Tls { get; } = new();

/// <summary>
/// Subscription / durable-client / event-notification settings.
/// See <see cref="SubscriptionOptions"/>.
/// </summary>
public SubscriptionOptions Subscription { get; } = new();
}
56 changes: 56 additions & 0 deletions src/Geode.Client/Options/PoolOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
namespace Geode.Client.Options;

/// <summary>
/// Connection-pool tuning derived from cppcache
/// <c>SystemProperties</c>. Defaults match cppcache's own constants in
/// <c>SystemProperties.cpp</c> so behaviour is interchangeable until we
/// have reason to diverge.
/// </summary>
public class PoolOptions
{
/// <summary>
/// Number of TCP connections to maintain in the pool. Mirrors cppcache
/// <c>connection-pool-size</c>; default 5.
/// </summary>
/// <remarks>
/// Phase 6 (pool) consumer. CLAUDE.md schema splits this into
/// <c>MinConnections</c> / <c>MaxConnections</c>; for now we expose a
/// single fixed size like cppcache and revisit when the pool is built.
/// </remarks>
public int ConnectionPoolSize { get; set; } = 5;

/// <summary>
/// Time budget for the TCP connect + handshake. Mirrors cppcache
/// <c>connect-timeout</c>; default 59 seconds.
/// </summary>
public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(59);

/// <summary>
/// Extra wait between failed connect attempts. Mirrors cppcache
/// <c>connect-wait-timeout</c>; default <see cref="TimeSpan.Zero"/>
/// (= disabled). Linux-specific in cppcache; kept here for parity but
/// likely unused by .NET socket APIs.
/// </summary>
public TimeSpan ConnectWaitTimeout { get; set; } = TimeSpan.Zero;

/// <summary>
/// Send / receive buffer size hint for the underlying socket. Mirrors
/// cppcache <c>max-socket-buffer-size</c>; default 65 × 1024 = 66560 bytes.
/// </summary>
public int MaxSocketBufferSize { get; set; } = 65 * 1024;

/// <summary>
/// Idle keep-alive ping cadence. Mirrors cppcache <c>ping-interval</c>;
/// default 10 seconds. The pool sends a <c>MessageType.Ping</c> on idle
/// connections at this rate so the server doesn't time them out.
/// </summary>
public TimeSpan PingInterval { get; set; } = TimeSpan.FromSeconds(10);

/// <summary>
/// Whether to randomise the order in which servers are tried.
/// cppcache uses the inverted <c>disable-shuffling-of-endpoints</c>
/// (default false ⇒ shuffle by default), so the equivalent default here
/// is <c>true</c>.
/// </summary>
public bool ShuffleEndpoints { get; set; } = true;
}
72 changes: 72 additions & 0 deletions src/Geode.Client/Options/SubscriptionOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
namespace Geode.Client.Options;

/// <summary>
/// Subscription, durable-client, and event-notification settings.
/// Mirrors the subscription-related fields of cppcache
/// <c>SystemProperties</c>. The whole group is dormant until Phase 12+
/// adds CQ / register-interest / event listeners.
/// </summary>
public class SubscriptionOptions
{
/// <summary>
/// Stable client identifier that lets the server retain this client's
/// subscription queue across reconnects. Mirrors cppcache
/// <c>durable-client-id</c>; default empty (= non-durable, server
/// discards the queue on disconnect).
/// </summary>
/// <remarks>
/// Set a stable string (e.g. <c>"order-service-pod-1"</c>) to opt into
/// durable subscriptions. Consumed by the
/// <c>ClientProxyMembershipID</c> builder when subscriptions ship in
/// Phase 12+.
/// </remarks>
public string DurableClientId { get; set; } = string.Empty;

/// <summary>
/// How long the server should retain this client's subscription queue
/// after a disconnect before giving up. Mirrors cppcache
/// <c>durable-timeout</c>; default 300 seconds. Only meaningful when
/// <see cref="DurableClientId"/> is set.
/// </summary>
public TimeSpan DurableTimeout { get; set; } = TimeSpan.FromSeconds(300);

/// <summary>
/// Whether a non-durable client starts receiving subscription events
/// automatically once regions are created. Mirrors cppcache
/// <c>auto-ready-for-events</c>; default <c>true</c>. Set to
/// <c>false</c> to require an explicit "ready" call after wiring up
/// listeners (Phase 12+ API).
/// </summary>
public bool AutoReadyForEvents { get; set; } = true;

/// <summary>
/// How often the client checks subscription redundancy (HA queue copy
/// count). Mirrors cppcache <c>redundancy-monitor-interval</c>;
/// default 10 seconds.
/// </summary>
public TimeSpan RedundancyMonitorInterval { get; set; } = TimeSpan.FromSeconds(10);

/// <summary>
/// Periodic ack cadence for received subscription notifications.
/// Mirrors cppcache <c>notify-ack-interval</c>; default 1 second.
/// </summary>
public TimeSpan NotifyAckInterval { get; set; } = TimeSpan.FromSeconds(1);

/// <summary>
/// How long an idle event-id map entry is kept for duplicate-event
/// detection on the subscription channel. Mirrors cppcache
/// <c>notify-dupcheck-life</c>; default 300 seconds.
/// </summary>
public TimeSpan NotifyDupCheckLife { get; set; } = TimeSpan.FromSeconds(300);

/// <summary>
/// Per-client event-conflation override sent in the handshake's
/// "overrides" byte. Tristate: <c>null</c> (default) defers to the
/// server-side setting, <c>true</c> forces conflation on for this
/// client, <c>false</c> forces it off. Mirrors cppcache
/// <c>conflate-events</c>'s string values <c>"server"</c> /
/// <c>"true"</c> / <c>"false"</c>, but <c>bool?</c> is the type-safe
/// way to express the same three states in C#.
/// </summary>
public bool? ConflateEvents { get; set; }
}
34 changes: 34 additions & 0 deletions src/Geode.Client/Options/TlsOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Geode.Client.Options;

/// <summary>
/// TLS / SSL configuration. Mirrors cppcache <c>ssl-*</c> settings but
/// will eventually layer on top of <c>System.Net.Security.SslStream</c>
/// (Phase 8) — file paths may be replaced or augmented with
/// <c>X509Certificate2</c> handles when we get there.
/// </summary>
public class TlsOptions
{
/// <summary>
/// Whether to upgrade the socket with TLS after TCP connect. Mirrors
/// cppcache <c>ssl-enabled</c>; default <c>false</c>.
/// </summary>
public bool Enabled { get; set; }

/// <summary>
/// Path to the client keystore (.pem in cppcache). Mirrors cppcache
/// <c>ssl-keystore</c>; default empty.
/// </summary>
public string KeyStorePath { get; set; } = string.Empty;

/// <summary>
/// Password protecting the keystore at <see cref="KeyStorePath"/>.
/// Mirrors cppcache <c>ssl-keystore-password</c>; default empty.
/// </summary>
public string KeyStorePassword { get; set; } = string.Empty;

/// <summary>
/// Path to the truststore used to validate the server certificate
/// chain. Mirrors cppcache <c>ssl-truststore</c>; default empty.
/// </summary>
public string TrustStorePath { get; set; } = string.Empty;
}
Loading