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
7 changes: 5 additions & 2 deletions src/Couchbase.Analytics/Internal/AnalyticsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ private async Task<IQueryResult> ExecuteQueryAsync(StringContent content, HttpCl

try
{
var response = await httpClient.SendAsync(request,
var response = await httpClient.SendAsync(request,
asStreaming ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead,
cancellationToken)
.ConfigureAwait(false);
Expand Down Expand Up @@ -105,6 +105,9 @@ private async Task<IQueryResult> ExecuteWithRetryAsync(string statement, QueryOp
var timeout = options.Timeout ?? _clusterOptions.TimeoutOptions.QueryTimeout;
options = options with { Timeout = timeout };

// The QueryOptions' Deserializer should override the ClusterOptions'.
var deserializer = options.Deserializer ?? _clusterOptions.Deserializer;

var errorContext = new ErrorContext(options.ClientContextId, stopwatch, timeout);
Exception? lastException = null;

Expand Down Expand Up @@ -134,7 +137,7 @@ private async Task<IQueryResult> ExecuteWithRetryAsync(string statement, QueryOp
"Analytics query attempt {Attempt} starting for {ClientContextId} (elapsed: {Elapsed}ms)",
attempt + 1, options.ClientContextId, stopwatch.Elapsed.TotalMilliseconds);

var result = await ExecuteQueryAsync(content, httpClient, options.AsStreaming, options.Deserializer, errorContext, cancellationToken).ConfigureAwait(false);
var result = await ExecuteQueryAsync(content, httpClient, options.AsStreaming, deserializer, errorContext, cancellationToken).ConfigureAwait(false);

// Always read errors from the result
if (result.Errors is { Count: > 0 })
Expand Down
68 changes: 67 additions & 1 deletion src/Couchbase.Analytics/Internal/ConnectionString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,73 @@ public bool TryGetParameter(string key, out TimeSpan parameter)
{
if (TryGetParameter(key, out string value))
{
parameter = TimeSpan.FromMilliseconds(Convert.ToUInt32(value));
var trimmed = value.Trim();
if (trimmed.Length == 0)
{
parameter = default;
return false;
}

// Parse optional unit suffix: us, ms, s, m, h (case-insensitive)
// Default unit is milliseconds if no suffix is provided
var lower = trimmed.ToLowerInvariant();

// Determine unit and numeric portion (check longest suffixes first)
string unit;
string numericPortion;
if (lower.EndsWith("us"))
{
unit = "us";
numericPortion = trimmed[..^2].Trim();
}
else if (lower.EndsWith("ms"))
{
unit = "ms";
numericPortion = trimmed[..^2].Trim();
}
else if (lower.EndsWith("s"))
{
unit = "s";
numericPortion = trimmed[..^1].Trim();
}
else if (lower.EndsWith("m"))
{
unit = "m";
numericPortion = trimmed[..^1].Trim();
}
else if (lower.EndsWith("h"))
{
unit = "h";
numericPortion = trimmed[..^1].Trim();
}
else
{
unit = "ms";
numericPortion = trimmed;
}

if (numericPortion.Length == 0)
{
parameter = TimeSpan.Zero;
return false;
}

// Allow integer or decimal values
if (!double.TryParse(numericPortion, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var amount))
{
parameter = TimeSpan.Zero;
throw new FormatException($"Invalid numeric value for parameter '{key}': '{value}'");
}

parameter = unit switch
{
"us" => TimeSpan.FromMicroseconds(amount),
"ms" => TimeSpan.FromMilliseconds(amount),
"s" => TimeSpan.FromSeconds(amount),
"m" => TimeSpan.FromMinutes(amount),
"h" => TimeSpan.FromHours(amount),
_ => TimeSpan.FromMilliseconds(amount)
};
return true;
}

Expand Down
18 changes: 15 additions & 3 deletions src/Couchbase.Analytics/Options/ClusterOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
using Couchbase.AnalyticsClient.Internal;
using Couchbase.AnalyticsClient.Internal.DI;
using Couchbase.AnalyticsClient.Internal.Utils;
using Couchbase.Core.Json;
using Couchbase.Core.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
Expand All @@ -41,6 +42,8 @@ public record ClusterOptions
[InterfaceStability(StabilityLevel.Volatile)]
public uint MaxRetries { get; private set; } = 7;

public IDeserializer Deserializer { get; private set; } = new StjJsonDeserializer();

internal ConnectionString? ConnectionStringValue { get; private set; }

private ILoggerFactory? Logging { get; set; }
Expand Down Expand Up @@ -78,13 +81,24 @@ public ClusterOptions WithMaxRetries(uint maxRetries)
/// </summary>
/// <param name="loggerFactory">The logger factory.</param>
/// <returns>
/// A reference to this <see cref="ClusterOptions"/> object for method chaining.
/// A copy of this <see cref="ClusterOptions"/> object for method chaining.
/// </returns>
public ClusterOptions WithLogging(ILoggerFactory? loggerFactory = null)
{
return this with { Logging = loggerFactory };
}

/// <summary>
/// Sets the <see cref="IDeserializer"/> to use for deserializing JSON responses.
/// This can be overridden on a per-operation basis by passing a deserializer to the <see cref="QueryOptions"/>.
/// </summary>
/// <param name="deserializer">An implementation of <see cref="IDeserializer"/></param>
/// <returns>A copy of this <see cref="ClusterOptions"/> object for method chaining.</returns>
public ClusterOptions WithDeserializer(IDeserializer deserializer)
{
return this with { Deserializer = deserializer };
}

private readonly IDictionary<Type, IServiceFactory> _services = DefaultServices.GetDefaultServices();

internal ICouchbaseServiceProvider BuildServiceProvider(ICredential? credential = null)
Expand Down Expand Up @@ -172,6 +186,4 @@ internal string? ConnectionString
}
}
}

//internal JsonSerializer Serializer { get; init; } //static
}
2 changes: 1 addition & 1 deletion src/Couchbase.Analytics/Options/QueryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public record QueryOptions
/// Used to deserialize query rows.
/// Default to <see cref="StjJsonDeserializer"/>
/// </summary>
public IDeserializer Deserializer { get; init; } = new StjJsonDeserializer();
public IDeserializer? Deserializer { get; init; }

/// <summary>
/// Whether the query is read-only.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,27 @@ public void Test_ConnectionString_TimeoutParameter_QueryTimeout_Values(string ti
Assert.Equal(TimeSpan.FromMilliseconds(expectedMilliseconds), options.TimeoutOptions.QueryTimeout);
}

[Theory]
[InlineData("500us", 5_000L)] // 500 microseconds -> 5000 ticks
[InlineData("250ms", 2_500_000L)] // 250 milliseconds -> 2,500,000 ticks
[InlineData("30s", 300_000_000L)] // 30 seconds -> 300,000,000 ticks
[InlineData("2m", 1_200_000_000L)] // 2 minutes -> 1,200,000,000 ticks
[InlineData("1h", 36_000_000_000L)] // 1 hour -> 36,000,000,000 ticks
[InlineData("30S", 300_000_000L)] // case-insensitive
[InlineData("1.5s", 15_000_000L)] // fractional seconds
[InlineData("0s", 0L)] // zero value
public void Test_ConnectionString_TimeoutParameter_ConnectTimeout_WithUnits(string timeoutValue, long expectedTicks)
{
var connectionString = $"http://localhost:8095?timeout.connect_timeout={timeoutValue}";

var options = new ClusterOptions
{
ConnectionString = connectionString
};

Assert.Equal(expectedTicks, options.TimeoutOptions.ConnectTimeout.Ticks);
}

[Theory]
[InlineData("true", true)]
[InlineData("false", false)]
Expand Down
Loading