From 90ee113aa6a6545067723b51ac47a44b6690227b Mon Sep 17 00:00:00 2001 From: Jasper Date: Wed, 9 Aug 2023 14:55:51 +0200 Subject: [PATCH] Use STJ src-gen for ServerInfo and ClientOptions (#106) * Use STJ src-gen for ClientOptions and ServerInfo * Fix deserialization of ServerInfo STJ src-gen does not support internal setters. Introduce a public interface instead to expose a read-only view of ServerInfo and make ServerInfo internal. * Remove defaults from src-gen options --- .../Commands/ProtocolWriter.cs | 5 +- src/NATS.Client.Core/IServerInfo.cs | 50 ++++++++++++ .../Internal/ClientOptions.cs | 13 +++- src/NATS.Client.Core/Internal/JsonContext.cs | 10 +++ .../Internal/NatsReadProtocolProcessor.cs | 7 +- src/NATS.Client.Core/Internal/ServerInfo.cs | 77 +++++++++++++++++++ src/NATS.Client.Core/NatsConnection.cs | 17 ++-- src/NATS.Client.Core/ServerInfo.cs | 77 ------------------- 8 files changed, 163 insertions(+), 93 deletions(-) create mode 100644 src/NATS.Client.Core/IServerInfo.cs create mode 100644 src/NATS.Client.Core/Internal/JsonContext.cs create mode 100644 src/NATS.Client.Core/Internal/ServerInfo.cs delete mode 100644 src/NATS.Client.Core/ServerInfo.cs diff --git a/src/NATS.Client.Core/Commands/ProtocolWriter.cs b/src/NATS.Client.Core/Commands/ProtocolWriter.cs index 84dd7659e..fae0e67fc 100644 --- a/src/NATS.Client.Core/Commands/ProtocolWriter.cs +++ b/src/NATS.Client.Core/Commands/ProtocolWriter.cs @@ -29,10 +29,7 @@ public void WriteConnect(ClientOptions options) WriteConstant(CommandConstants.ConnectWithPadding); var jsonWriter = new Utf8JsonWriter(_writer); - JsonSerializer.Serialize(jsonWriter, options, new JsonSerializerOptions - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - }); + JsonSerializer.Serialize(jsonWriter, options, JsonContext.Default.ClientOptions); WriteConstant(CommandConstants.NewLine); } diff --git a/src/NATS.Client.Core/IServerInfo.cs b/src/NATS.Client.Core/IServerInfo.cs new file mode 100644 index 000000000..f5859f1c2 --- /dev/null +++ b/src/NATS.Client.Core/IServerInfo.cs @@ -0,0 +1,50 @@ +namespace NATS.Client.Core; + +public interface IServerInfo +{ + string Id { get; } + + string Name { get; } + + string Version { get; } + + long ProtocolVersion { get; } + + string GitCommit { get; } + + string GoVersion { get; } + + string Host { get; } + + int Port { get; } + + bool HeadersSupported { get; } + + bool AuthRequired { get; } + + bool TlsRequired { get; } + + bool TlsVerify { get; } + + bool TlsAvailable { get; } + + int MaxPayload { get; } + + bool JetStreamAvailable { get; } + + ulong ClientId { get; } + + string ClientIp { get; } + + string? Nonce { get; } + + string? Cluster { get; } + + bool ClusterDynamic { get; } + + string[]? ClientConnectUrls { get; } + + string[]? WebSocketConnectUrls { get; } + + bool LameDuckMode { get; } +} diff --git a/src/NATS.Client.Core/Internal/ClientOptions.cs b/src/NATS.Client.Core/Internal/ClientOptions.cs index a3dc7d9ab..4d0be738d 100644 --- a/src/NATS.Client.Core/Internal/ClientOptions.cs +++ b/src/NATS.Client.Core/Internal/ClientOptions.cs @@ -3,11 +3,15 @@ namespace NATS.Client.Core.Internal; +// SYSLIB1037: The type 'ClientOptions' defines init-only properties, +// deserialization of which is currently not supported in source generation mode +#pragma warning disable SYSLIB1037 + // These connections options are serialized and sent to the server. // https://github.com/nats-io/nats-server/blob/a23b1b7/server/client.go#L536 internal sealed class ClientOptions { - public ClientOptions(NatsOptions options) + private ClientOptions(NatsOptions options) { Name = options.Name; Echo = options.Echo; @@ -86,6 +90,11 @@ public ClientOptions(NatsOptions options) [JsonPropertyName("no_responders")] public bool NoResponders { get; init; } = false; + public static ClientOptions Create(NatsOptions options) + { + return new ClientOptions(options); + } + private static string GetAssemblyVersion() { var asm = typeof(ClientOptions); @@ -107,3 +116,5 @@ private static string GetAssemblyVersion() return version; } } + +#pragma warning restore SYSLIB1037 diff --git a/src/NATS.Client.Core/Internal/JsonContext.cs b/src/NATS.Client.Core/Internal/JsonContext.cs new file mode 100644 index 000000000..1becafc5d --- /dev/null +++ b/src/NATS.Client.Core/Internal/JsonContext.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace NATS.Client.Core.Internal; + +[JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] +[JsonSerializable(typeof(ServerInfo), GenerationMode = JsonSourceGenerationMode.Metadata)] +[JsonSerializable(typeof(ClientOptions), GenerationMode = JsonSourceGenerationMode.Serialization)] +internal sealed partial class JsonContext : JsonSerializerContext +{ +} diff --git a/src/NATS.Client.Core/Internal/NatsReadProtocolProcessor.cs b/src/NATS.Client.Core/Internal/NatsReadProtocolProcessor.cs index d9333aec7..fd0d72023 100644 --- a/src/NATS.Client.Core/Internal/NatsReadProtocolProcessor.cs +++ b/src/NATS.Client.Core/Internal/NatsReadProtocolProcessor.cs @@ -103,7 +103,8 @@ private static ServerInfo ParseInfo(in ReadOnlySequence buffer) // skip `INFO` var jsonReader = new Utf8JsonReader(buffer.Slice(5)); - var serverInfo = JsonSerializer.Deserialize(ref jsonReader) ?? throw new NatsException("Can not parse ServerInfo."); + var serverInfo = JsonSerializer.Deserialize(ref jsonReader, JsonContext.Default.ServerInfo) + ?? throw new NatsException("Can not parse ServerInfo."); return serverInfo; } @@ -404,7 +405,7 @@ private async ValueTask> DispatchCommandAsync(int code, R var newPosition = newBuffer.PositionOf((byte)'\n'); var serverInfo = ParseInfo(newBuffer); - _connection.ServerInfo = serverInfo; + _connection.WritableServerInfo = serverInfo; _logger.LogInformation("Received ServerInfo: {0}", serverInfo); _waitForInfoSignal.TrySetResult(); await _infoParsed.ConfigureAwait(false); @@ -413,7 +414,7 @@ private async ValueTask> DispatchCommandAsync(int code, R else { var serverInfo = ParseInfo(buffer); - _connection.ServerInfo = serverInfo; + _connection.WritableServerInfo = serverInfo; _logger.LogInformation("Received ServerInfo: {0}", serverInfo); _waitForInfoSignal.TrySetResult(); await _infoParsed.ConfigureAwait(false); diff --git a/src/NATS.Client.Core/Internal/ServerInfo.cs b/src/NATS.Client.Core/Internal/ServerInfo.cs new file mode 100644 index 000000000..7d45e0626 --- /dev/null +++ b/src/NATS.Client.Core/Internal/ServerInfo.cs @@ -0,0 +1,77 @@ +using System.Text.Json.Serialization; + +namespace NATS.Client.Core.Internal; + +// Defined from `type Info struct` in nats-server +// https://github.com/nats-io/nats-server/blob/a23b1b7/server/server.go#L61 +internal sealed record ServerInfo : IServerInfo +{ + [JsonPropertyName("server_id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("server_name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("version")] + public string Version { get; set; } = string.Empty; + + [JsonPropertyName("proto")] + public long ProtocolVersion { get; set; } + + [JsonPropertyName("git_commit")] + public string GitCommit { get; set; } = string.Empty; + + [JsonPropertyName("go")] + public string GoVersion { get; set; } = string.Empty; + + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; + + [JsonPropertyName("port")] + public int Port { get; set; } + + [JsonPropertyName("headers")] + public bool HeadersSupported { get; set; } + + [JsonPropertyName("auth_required")] + public bool AuthRequired { get; set; } + + [JsonPropertyName("tls_required")] + public bool TlsRequired { get; set; } + + [JsonPropertyName("tls_verify")] + public bool TlsVerify { get; set; } + + [JsonPropertyName("tls_available")] + public bool TlsAvailable { get; set; } + + [JsonPropertyName("max_payload")] + public int MaxPayload { get; set; } + + [JsonPropertyName("jetstream")] + public bool JetStreamAvailable { get; set; } + + [JsonPropertyName("client_id")] + public ulong ClientId { get; set; } + + [JsonPropertyName("client_ip")] + public string ClientIp { get; set; } = string.Empty; + + [JsonPropertyName("nonce")] + public string? Nonce { get; set; } + + [JsonPropertyName("cluster")] + public string? Cluster { get; set; } + + [JsonPropertyName("cluster_dynamic")] + public bool ClusterDynamic { get; set; } + + [JsonPropertyName("connect_urls")] + public string[]? ClientConnectUrls { get; set; } + + [JsonPropertyName("ws_connect_urls")] + public string[]? WebSocketConnectUrls { get; set; } + + [JsonPropertyName("ldm")] + public bool LameDuckMode { get; set; } +} diff --git a/src/NATS.Client.Core/NatsConnection.cs b/src/NATS.Client.Core/NatsConnection.cs index 5cfad2354..81b71f8b7 100644 --- a/src/NATS.Client.Core/NatsConnection.cs +++ b/src/NATS.Client.Core/NatsConnection.cs @@ -24,6 +24,7 @@ public partial class NatsConnection : IAsyncDisposable, INatsConnection public Func<(string Host, int Port), ValueTask<(string Host, int Port)>>? OnConnectingAsync; internal readonly ConnectionStatsCounter Counter; // allow to call from external sources + internal ServerInfo? WritableServerInfo; #pragma warning restore SA1401 private readonly object _gate = new object(); private readonly WriterState _writerState; @@ -70,7 +71,7 @@ public NatsConnection(NatsOptions options) InboxPrefix = $"{options.InboxPrefix}.{Guid.NewGuid():n}."; SubscriptionManager = new SubscriptionManager(this, InboxPrefix); _logger = options.LoggerFactory.CreateLogger(); - _clientOptions = new ClientOptions(Options); + _clientOptions = ClientOptions.Create(Options); HeaderParser = new HeaderParser(options.HeaderEncoding); } @@ -85,7 +86,7 @@ public NatsConnection(NatsOptions options) public NatsConnectionState ConnectionState { get; private set; } - public ServerInfo? ServerInfo { get; internal set; } // server info is set when received INFO + public IServerInfo? ServerInfo => WritableServerInfo; // server info is set when received INFO public HeaderParser HeaderParser { get; } @@ -328,19 +329,19 @@ private async ValueTask SetupReaderWriterAsync(bool reconnect) // check to see if we should upgrade to TLS if (_socket is TcpConnection tcpConnection) { - if (Options.TlsOptions.Disabled && ServerInfo!.TlsRequired) + if (Options.TlsOptions.Disabled && WritableServerInfo!.TlsRequired) { throw new NatsException( $"Server {_currentConnectUri} requires TLS but TlsOptions.Disabled is set to true"); } - if (Options.TlsOptions.Required && !ServerInfo!.TlsRequired && !ServerInfo.TlsAvailable) + if (Options.TlsOptions.Required && !WritableServerInfo!.TlsRequired && !WritableServerInfo.TlsAvailable) { throw new NatsException( $"Server {_currentConnectUri} does not support TLS but TlsOptions.Disabled is set to true"); } - if (Options.TlsOptions.Required || ServerInfo!.TlsRequired || ServerInfo.TlsAvailable) + if (Options.TlsOptions.Required || WritableServerInfo!.TlsRequired || WritableServerInfo.TlsAvailable) { // do TLS upgrade // if the current URI is not a seed URI and is not a DNS hostname, check the server cert against the @@ -376,7 +377,7 @@ private async ValueTask SetupReaderWriterAsync(bool reconnect) infoParsedSignal.SetResult(); // Authentication - _userCredentials?.Authenticate(_clientOptions, ServerInfo); + _userCredentials?.Authenticate(_clientOptions, WritableServerInfo); // add CONNECT and PING command to priority lane _writerState.PriorityCommands.Clear(); @@ -428,8 +429,8 @@ private async void ReconnectLoop() var defaultScheme = _currentConnectUri!.Uri.Scheme; var urls = (Options.NoRandomize - ? ServerInfo?.ClientConnectUrls?.Select(x => new NatsUri(x, false, defaultScheme)).Distinct().ToArray() - : ServerInfo?.ClientConnectUrls?.Select(x => new NatsUri(x, false, defaultScheme)).OrderBy(_ => Guid.NewGuid()).Distinct().ToArray()) + ? WritableServerInfo?.ClientConnectUrls?.Select(x => new NatsUri(x, false, defaultScheme)).Distinct().ToArray() + : WritableServerInfo?.ClientConnectUrls?.Select(x => new NatsUri(x, false, defaultScheme)).OrderBy(_ => Guid.NewGuid()).Distinct().ToArray()) ?? Array.Empty(); if (urls.Length == 0) urls = Options.GetSeedUris(); diff --git a/src/NATS.Client.Core/ServerInfo.cs b/src/NATS.Client.Core/ServerInfo.cs deleted file mode 100644 index 74cc6a681..000000000 --- a/src/NATS.Client.Core/ServerInfo.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Text.Json.Serialization; - -namespace NATS.Client.Core; - -// Defined from `type Info struct` in nats-server -// https://github.com/nats-io/nats-server/blob/a23b1b7/server/server.go#L61 -public sealed record ServerInfo -{ - [JsonPropertyName("server_id")] - public string Id { get; init; } = string.Empty; - - [JsonPropertyName("server_name")] - public string Name { get; init; } = string.Empty; - - [JsonPropertyName("version")] - public string Version { get; init; } = string.Empty; - - [JsonPropertyName("proto")] - public long ProtocolVersion { get; init; } - - [JsonPropertyName("git_commit")] - public string GitCommit { get; init; } = string.Empty; - - [JsonPropertyName("go")] - public string GoVersion { get; init; } = string.Empty; - - [JsonPropertyName("host")] - public string Host { get; init; } = string.Empty; - - [JsonPropertyName("port")] - public int Port { get; init; } - - [JsonPropertyName("headers")] - public bool HeadersSupported { get; init; } - - [JsonPropertyName("auth_required")] - public bool AuthRequired { get; init; } - - [JsonPropertyName("tls_required")] - public bool TlsRequired { get; init; } - - [JsonPropertyName("tls_verify")] - public bool TlsVerify { get; init; } - - [JsonPropertyName("tls_available")] - public bool TlsAvailable { get; init; } - - [JsonPropertyName("max_payload")] - public int MaxPayload { get; init; } - - [JsonPropertyName("jetstream")] - public bool JetStreamAvailable { get; init; } - - [JsonPropertyName("client_id")] - public ulong ClientId { get; init; } - - [JsonPropertyName("client_ip")] - public string ClientIp { get; init; } = string.Empty; - - [JsonPropertyName("nonce")] - public string? Nonce { get; init; } - - [JsonPropertyName("cluster")] - public string? Cluster { get; init; } - - [JsonPropertyName("cluster_dynamic")] - public bool ClusterDynamic { get; init; } - - [JsonPropertyName("connect_urls")] - public string[]? ClientConnectUrls { get; init; } - - [JsonPropertyName("ws_connect_urls")] - public string[]? WebSocketConnectUrls { get; init; } - - [JsonPropertyName("ldm")] - public bool LameDuckMode { get; init; } -}