Skip to content

Commit

Permalink
Ch-ch-ch-changes
Browse files Browse the repository at this point in the history
  • Loading branch information
adambollen committed Oct 28, 2024
1 parent 103a0da commit 982a7f0
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 62 deletions.
8 changes: 5 additions & 3 deletions Fauna.Test/Configuration.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public void ConstructorWorksFine()

Assert.AreEqual("secret", b.Secret);
Assert.AreEqual(Endpoints.Default, b.Endpoint);
Assert.AreEqual(TimeSpan.FromSeconds(5), b.DefaultQueryOptions.QueryTimeout);
Assert.IsTrue(b.DisposeHttpClient);
}

Expand Down Expand Up @@ -83,13 +84,13 @@ public void FinalQueryOptions()
var defaults = new QueryOptions
{
TypeCheck = true,
QueryTags = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "luhrmann" } },
QueryTimeout = TimeSpan.FromSeconds(30)
QueryTags = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "luhrmann" } }
};
var overrides = new QueryOptions
{
Linearized = true,
QueryTags = new Dictionary<string, string> { { "foo", "yep" } }
QueryTags = new Dictionary<string, string> { { "foo", "yep" } },
QueryTimeout = TimeSpan.FromSeconds(30)
};

var finalOptions = QueryOptions.GetFinalQueryOptions(defaults, overrides);
Expand All @@ -101,5 +102,6 @@ public void FinalQueryOptions()
Assert.IsNotNull(finalOptions!.QueryTags);
Assert.AreEqual("yep", finalOptions!.QueryTags!["foo"]);
Assert.AreEqual("luhrmann", finalOptions!.QueryTags!["baz"]);
Assert.AreEqual(TimeSpan.FromSeconds(30), finalOptions!.QueryTimeout);
}
}
58 changes: 32 additions & 26 deletions Fauna/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,12 @@ internal override async Task<QuerySuccess<T>> QueryAsyncInternal<T>(
using var stream = new MemoryStream();
Serialize(stream, query, ctx);

using var httpResponse = await _connection.DoPostAsync(QueryUriPath, stream, headers, cancel);
using var httpResponse = await _connection.DoPostAsync(
QueryUriPath,
stream,
headers,
GetRequestTimeoutWithBuffer(finalOptions.QueryTimeout),
cancel);
var body = await httpResponse.Content.ReadAsStringAsync(cancel);
var res = QueryResponse.GetFromResponseBody<T>(ctx, serializer, httpResponse.StatusCode, body);
switch (res)
Expand Down Expand Up @@ -167,11 +172,10 @@ private void Serialize(Stream stream, Query query, MappingContext ctx)
writer.Flush();
}

private Dictionary<string, string> GetRequestHeaders(QueryOptions? queryOptions)
private Dictionary<string, string> GetRequestHeaders(QueryOptions queryOptions)
{
var headers = new Dictionary<string, string>
{

{ Headers.Authorization, $"Bearer {_config.Secret}"},
{ Headers.Format, "tagged" },
{ Headers.Driver, "C#" }
Expand All @@ -182,39 +186,41 @@ private Dictionary<string, string> GetRequestHeaders(QueryOptions? queryOptions)
headers.Add(Headers.LastTxnTs, LastSeenTxn.ToString());
}

if (queryOptions != null)
if (queryOptions.QueryTimeout != TimeSpan.Zero)
{
if (queryOptions.QueryTimeout.HasValue)
{
headers.Add(
Headers.QueryTimeoutMs,
queryOptions.QueryTimeout.Value.TotalMilliseconds.ToString(CultureInfo.InvariantCulture));
}
headers.Add(
Headers.QueryTimeoutMs,
queryOptions.QueryTimeout.TotalMilliseconds.ToString(CultureInfo.InvariantCulture));
}

if (queryOptions.QueryTags != null)
{
headers.Add(Headers.QueryTags, EncodeQueryTags(queryOptions.QueryTags));
}
if (queryOptions.QueryTags != null)
{
headers.Add(Headers.QueryTags, EncodeQueryTags(queryOptions.QueryTags));
}

if (!string.IsNullOrEmpty(queryOptions.TraceParent))
{
headers.Add(Headers.TraceParent, queryOptions.TraceParent);
}
if (!string.IsNullOrEmpty(queryOptions.TraceParent))
{
headers.Add(Headers.TraceParent, queryOptions.TraceParent);
}

if (queryOptions.Linearized != null)
{
headers.Add(Headers.Linearized, queryOptions.Linearized.ToString()!);
}
if (queryOptions.Linearized != null)
{
headers.Add(Headers.Linearized, queryOptions.Linearized.ToString()!);
}

if (queryOptions.TypeCheck != null)
{
headers.Add(Headers.TypeCheck, queryOptions.TypeCheck.ToString()!);
}
if (queryOptions.TypeCheck != null)
{
headers.Add(Headers.TypeCheck, queryOptions.TypeCheck.ToString()!);
}

return headers;
}

private TimeSpan GetRequestTimeoutWithBuffer(TimeSpan queryTimeout)
{
return queryTimeout.Add(_config.ClientBufferTimeout);
}

private static string EncodeQueryTags(Dictionary<string, string> tags)
{
return string.Join(",", tags.Select(entry => entry.Key + "=" + entry.Value));
Expand Down
15 changes: 13 additions & 2 deletions Fauna/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@ public record class Configuration
/// </summary>
public bool DisposeHttpClient { get; } = true;

/// <summary>
/// Additional buffer to add to <see cref="QueryOptions.QueryTimeout"/> when setting the HTTP request
/// timeout on the <see cref="HttpClient"/>; default is 5 seconds.
/// </summary>
public TimeSpan ClientBufferTimeout { get; init; } = TimeSpan.FromSeconds(5);

/// <summary>
/// The HTTP Client to use for requests.
/// </summary>
public HttpClient HttpClient { get; } = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
public HttpClient HttpClient { get; init; }

/// <summary>
/// The secret key used for authentication.
Expand All @@ -33,7 +39,7 @@ public record class Configuration
/// <summary>
/// Default options for queries sent to Fauna.
/// </summary>
public QueryOptions? DefaultQueryOptions { get; init; } = null;
public QueryOptions DefaultQueryOptions { get; init; } = new();

/// <summary>
/// The retry configuration to apply to requests.
Expand Down Expand Up @@ -68,6 +74,11 @@ public Configuration(string secret = "", HttpClient? httpClient = null, ILogger?
HttpClient = httpClient;
DisposeHttpClient = false;
}
else
{
// Set Timeout to int.MaxValue milliseconds and configure each SendAsync with a timebound CancellationToken
HttpClient = new HttpClient { Timeout = TimeSpan.FromMilliseconds(int.MaxValue) };
}

if (logger != null)
{
Expand Down
57 changes: 39 additions & 18 deletions Fauna/Core/Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,42 +20,63 @@ internal class Connection : IConnection
private readonly Configuration _cfg;
private bool _disposed;

public TimeSpan BufferedRequestTimeout { get; init; }

/// <summary>
/// Initializes a new instance of the Connection class.
/// </summary>
/// <param name="configuration">The <see cref="Configuration"/> to use.</param>
public Connection(Configuration configuration)
{
_cfg = configuration;
BufferedRequestTimeout = _cfg.DefaultQueryOptions.QueryTimeout.Add(_cfg.ClientBufferTimeout);
}

public async Task<HttpResponseMessage> DoPostAsync(
string path,
Stream body,
Dictionary<string, string> headers,
TimeSpan requestTimeout,
CancellationToken cancel = default)
{
HttpResponseMessage response;

var policyResult = await _cfg.RetryConfiguration.RetryPolicy
.ExecuteAndCaptureAsync(() =>
_cfg.HttpClient.SendAsync(CreateHttpRequest(path, body, headers), cancel))
.ConfigureAwait(false);
response = policyResult.Outcome == OutcomeType.Successful
? policyResult.Result
: policyResult.FinalHandledResult ?? throw policyResult.FinalException;
using var timeboundCts = new CancellationTokenSource(requestTimeout);
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(timeboundCts.Token, cancel);

Logger.Instance.LogDebug(
"Fauna HTTP Response {status} from {uri}, headers: {headers}",
response.StatusCode.ToString(),
response.RequestMessage?.RequestUri?.ToString() ?? "UNKNOWN",
JsonSerializer.Serialize(
response.Headers.ToDictionary(kv => kv.Key, kv => kv.Value.ToList()))
);

Logger.Instance.LogTrace("Response body: {body}", await response.Content.ReadAsStringAsync(cancel));

return response;
try
{
var policyResult = await _cfg.RetryConfiguration.RetryPolicy
.ExecuteAndCaptureAsync(() =>
_cfg.HttpClient.SendAsync(CreateHttpRequest(path, body, headers), combinedCts.Token))
.ConfigureAwait(false);
response = policyResult.Outcome == OutcomeType.Successful
? policyResult.Result
: policyResult.FinalHandledResult ?? throw policyResult.FinalException;

Logger.Instance.LogDebug(
"Fauna HTTP Response {status} from {uri}, headers: {headers}",
response.StatusCode.ToString(),
response.RequestMessage?.RequestUri?.ToString() ?? "UNKNOWN",
JsonSerializer.Serialize(
response.Headers.ToDictionary(kv => kv.Key, kv => kv.Value.ToList()))
);

Logger.Instance.LogTrace("Response body: {body}", await response.Content.ReadAsStringAsync(cancel));

return response;
}
catch (TaskCanceledException ex)
{
if (timeboundCts.IsCancellationRequested)
{
throw new System.TimeoutException($"The HTTP request on {path} timed out after {requestTimeout.TotalMilliseconds} ms.", ex);
}
else
{
throw;
}
}
}

public async IAsyncEnumerable<Event<T>> OpenStream<T>(
Expand Down
2 changes: 2 additions & 0 deletions Fauna/Core/IConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ internal interface IConnection : IDisposable
/// <param name="path">The path of the resource to send the request to.</param>
/// <param name="body">The stream containing the request body.</param>
/// <param name="headers">A dictionary of headers to be included in the request.</param>
/// <param name="requestTimeout">The HTTP request timeout</param>
/// <param name="cancel">A cancellation token to use with the request.</param>
/// <returns>A Task representing the asynchronous operation, which upon completion contains the response from the server as <see cref="HttpResponseMessage"/>.</returns>
Task<HttpResponseMessage> DoPostAsync(
string path,
Stream body,
Dictionary<string, string> headers,
TimeSpan requestTimeout,
CancellationToken cancel);

/// <summary>
Expand Down
16 changes: 3 additions & 13 deletions Fauna/Core/QueryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ public class QueryOptions

/// <summary>
/// Gets or sets the query timeout. It defines how long the client waits for a query to complete.
/// Default value is 5 seconds.
/// </summary>
public TimeSpan? QueryTimeout { get; set; } = null;
public TimeSpan QueryTimeout { get; set; } = TimeSpan.FromSeconds(5);

/// <summary>
/// Gets or sets a string-encoded set of caller-defined tags for identifying the request in logs and response bodies.
Expand All @@ -37,19 +38,8 @@ public class QueryOptions
/// <param name="options">The default query options.</param>
/// <param name="overrides">The query options provided for a specific query, overriding the defaults.</param>
/// <returns>A <see cref="QueryOptions"/> object representing the final combined set of query options.</returns>
internal static QueryOptions? GetFinalQueryOptions(QueryOptions? options, QueryOptions? overrides)
internal static QueryOptions GetFinalQueryOptions(QueryOptions options, QueryOptions? overrides)
{

if (options == null && overrides == null)
{
return null;
}

if (options == null)
{
return overrides;
}

if (overrides == null)
{
return options;
Expand Down

0 comments on commit 982a7f0

Please sign in to comment.