Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for agent cache #225

Merged
merged 31 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
15ba79e
feat(WriteOptions): add new properties
winstxnhdw Jul 14, 2023
8b9f9e2
feat(QueryOptions): add new properties
winstxnhdw Jul 14, 2023
171a99a
feat(Lock): add `LockDelay` and `Namespace`
winstxnhdw Jul 14, 2023
0b7ae8c
chore(namespace): remove redudant namespace
winstxnhdw Jul 14, 2023
148a73a
chore(Lock): address lint suggestions
winstxnhdw Jul 14, 2023
984a1e8
chore: comment out enterprise-only properties
winstxnhdw Jul 20, 2023
1ae5ec3
feat: add support for agent cache
winstxnhdw Jul 20, 2023
ee53598
Merge branch 'G-Research:master' into master
winstxnhdw Jul 20, 2023
83be944
feat: add test for `UseCache`
winstxnhdw Jul 21, 2023
6b44a04
revert(Lock): remove enterprise features
winstxnhdw Jul 24, 2023
6f76030
revert: remove properties unrelated to agent cache
winstxnhdw Jul 24, 2023
891b1c8
feat: add test for setting cache to `QueryOptions`
winstxnhdw Jul 25, 2023
5c16d2b
feat: add `Headers` property to `QueryResult`
winstxnhdw Jul 25, 2023
a7a054b
feat: add overload for `Datacenters`
winstxnhdw Jul 25, 2023
a628faf
feat: add test for agent cache
winstxnhdw Jul 25, 2023
d089416
Merge branch 'master' into usecache
winstxnhdw Jul 25, 2023
0869acd
refactor(test): check for `X-Cache` header and validate
winstxnhdw Jul 26, 2023
4f7a4b3
fix: get correct seconds from `TimeSpan`
winstxnhdw Jul 26, 2023
dd7b483
Merge branch 'master' into usecache
winstxnhdw Jul 26, 2023
de6fcdb
refactor: use specific header properties instead
winstxnhdw Jul 27, 2023
e2f771f
fix: get correct seconds from `TimeSpan`
winstxnhdw Jul 27, 2023
f873136
fix: get correct seconds from `TimeSpan`
winstxnhdw Jul 27, 2023
fba5766
refactor: use nullable type for `CacheResult`
winstxnhdw Jul 27, 2023
d06f0b3
refactor: convert to `UInt64` instead of `Int32`
winstxnhdw Jul 27, 2023
fa27f08
refactor: prevent silent failures for incorrect `QueryOptions`
winstxnhdw Jul 28, 2023
fa39647
fix: update test to reflect updated `CacheResult`
winstxnhdw Jul 28, 2023
e42b30a
refactor: apply lint suggestions
winstxnhdw Jul 28, 2023
c2d9e99
refactor: prevent silent failures for incorrect `QueryOptions`
winstxnhdw Jul 28, 2023
2213f15
refactor: prevent silent failures for incorrect `QueryOptions`
winstxnhdw Jul 28, 2023
80e51f1
style(ICatalogEndpoint): apply lint suggestions
winstxnhdw Jul 28, 2023
452c0b9
style(Catalog): apply lint suggestions
winstxnhdw Jul 28, 2023
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
25 changes: 25 additions & 0 deletions Consul.Test/AgentTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,31 @@ public async Task Agent_Checks_ServiceBound()
await _client.Agent.ServiceDeregister(svcID);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task Agent_UseCache(bool useCache)
{

winstxnhdw marked this conversation as resolved.
Show resolved Hide resolved
var opts = new QueryOptions
{
UseCache = useCache,
MaxAge = TimeSpan.FromSeconds(10),
StaleIfError = TimeSpan.FromSeconds(10),
};

var response = await _client.Catalog.Datacenters(opts);

if (useCache)
{
Assert.NotEqual(QueryResult.CacheResult.UNDEFINED, response.XCache);
winstxnhdw marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
Assert.Equal(QueryResult.CacheResult.UNDEFINED, response.XCache);
}
}

[Fact]
public async Task Agent_Join()
{
Expand Down
45 changes: 37 additions & 8 deletions Consul.Test/ClientTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,31 @@ public async Task Client_SetQueryOptions()
Assert.Equal("1m40s", request.Params["wait"]);
}

[Fact]
public async Task Client_SetQueryOptionsWithCache()
{
var opts = new QueryOptions()
{
Datacenter = "foo",
Consistency = ConsistencyMode.Default,
WaitIndex = 1000,
WaitTime = new TimeSpan(0, 0, 100),
Token = "12345",
UseCache = true,
MaxAge = new TimeSpan(0, 0, 10),
StaleIfError = new TimeSpan(0, 0, 10)
};
var request = _client.Get<KVPair>("/v1/kv/foo", opts);

await Assert.ThrowsAsync<ConsulRequestException>(async () => await request.Execute(CancellationToken.None));

Assert.Equal("foo", request.Params["dc"]);
Assert.Equal("1000", request.Params["index"]);
Assert.Equal("1m40s", request.Params["wait"]);
Assert.Equal(string.Empty, request.Params["cached"]);
Assert.Equal("max-age=10,stale-if-error=10", request.Params["Cache-Control"]);
winstxnhdw marked this conversation as resolved.
Show resolved Hide resolved
}

[Fact]
public async Task Client_SetClientOptions()
{
Expand Down Expand Up @@ -138,9 +163,11 @@ public async Task Client_CustomHttpClient()
hc.Timeout = TimeSpan.FromDays(10);
hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

var config = new ConsulClientConfiguration();
config.Address = TestHelper.HttpUri;
config.Token = TestHelper.MasterToken;
var config = new ConsulClientConfiguration
{
Address = TestHelper.HttpUri,
Token = TestHelper.MasterToken
};

#pragma warning disable CS0618 // Type or member is obsolete
using (var client = new ConsulClient(config, hc))
Expand Down Expand Up @@ -175,9 +202,11 @@ public async Task Client_DisposeBehavior()
[Fact]
public async Task Client_ReuseAndUpdateConfig()
{
var config = new ConsulClientConfiguration();
config.Address = TestHelper.HttpUri;
config.Token = TestHelper.MasterToken;
var config = new ConsulClientConfiguration
{
Address = TestHelper.HttpUri,
Token = TestHelper.MasterToken
};

#pragma warning disable CS0618 // Type or member is obsolete
using (var client = new ConsulClient(config))
Expand Down Expand Up @@ -219,8 +248,8 @@ public async Task Client_ReuseAndUpdateConfig()
[Fact]
public void Client_Constructors()
{
Action<ConsulClientConfiguration> cfgAction2 = (cfg) => { cfg.Token = "yep"; };
Action<ConsulClientConfiguration> cfgAction = (cfg) => { cfg.Datacenter = "foo"; cfgAction2(cfg); };
void cfgAction2(ConsulClientConfiguration cfg) { cfg.Token = "yep"; }
void cfgAction(ConsulClientConfiguration cfg) { cfg.Datacenter = "foo"; cfgAction2(cfg); }

using (var c = new ConsulClient())
{
Expand Down
9 changes: 9 additions & 0 deletions Consul/Catalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,15 @@ internal Catalog(ConsulClient c)
return _client.Get<string[]>("/v1/catalog/datacenters").Execute(ct);
}

/// <summary>
/// Datacenters is used to query for all the known datacenters with custom query options
/// </summary>
/// <returns>A list of datacenter names</returns>
public Task<QueryResult<string[]>> Datacenters(QueryOptions q, CancellationToken ct = default(CancellationToken))
{
return _client.Get<string[]>("/v1/catalog/datacenters", q).Execute(ct);
}

/// <summary>
/// Nodes is used to query all the known nodes
/// </summary>
Expand Down
75 changes: 71 additions & 4 deletions Consul/Client_GetRequests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
// -----------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -116,16 +117,15 @@ public async Task<QueryResult<Stream>> ExecuteStreaming(CancellationToken ct)
ApplyHeaders(message, Client.Config);
var response = await Client.HttpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);

ParseQueryHeaders(response, (result as QueryResult<TOut>));
ParseQueryHeaders(response, result as QueryResult<TOut>);
result.StatusCode = response.StatusCode;
ResponseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);

result.Response = ResponseStream;

if (response.StatusCode != HttpStatusCode.NotFound && !response.IsSuccessStatusCode)
{
throw new ConsulRequestException(string.Format("Unexpected response, status code {0}",
response.StatusCode), response.StatusCode);
throw new ConsulRequestException($"Unexpected response, status code {response.StatusCode}", response.StatusCode);
}

result.RequestTime = timer.Elapsed;
Expand Down Expand Up @@ -171,6 +171,27 @@ protected override void ApplyOptions(ConsulClientConfiguration clientConfig)
{
Params["near"] = Options.Near;
}

if (Options.UseCache)
{
Params["cached"] = string.Empty;
var cacheControl = new List<string>();

if (Options.MaxAge.HasValue && Options.MaxAge > TimeSpan.Zero)
winstxnhdw marked this conversation as resolved.
Show resolved Hide resolved
{
cacheControl.Add($"max-age={Convert.ToInt32(Options.MaxAge.Value.TotalSeconds)}");
}

if (Options.StaleIfError.HasValue && Options.StaleIfError > TimeSpan.Zero)
winstxnhdw marked this conversation as resolved.
Show resolved Hide resolved
{
cacheControl.Add($"stale-if-error={Convert.ToInt32(Options.StaleIfError.Value.TotalSeconds)}");
}

if (cacheControl.Count > 0)
{
Params["Cache-Control"] = string.Join(",", cacheControl);
}
}
}

protected void ParseQueryHeaders(HttpResponseMessage res, QueryResult<TOut> meta)
Expand Down Expand Up @@ -224,6 +245,31 @@ protected void ParseQueryHeaders(HttpResponseMessage res, QueryResult<TOut> meta
throw new ConsulRequestException("Failed to parse X-Consul-Translate-Addresses", res.StatusCode, ex);
}
}

if (headers.Contains("X-Cache"))
{
try
{

meta.XCache = headers.GetValues("X-Cache").Single() == "HIT" ? QueryResult.CacheResult.HIT : QueryResult.CacheResult.MISS;
}
catch (Exception ex)
{
throw new ConsulRequestException("Failed to parse X-Cache", res.StatusCode, ex);
}
}

if (headers.Contains("Age"))
{
try
{
meta.Age = TimeSpan.FromSeconds(double.Parse(headers.GetValues("Age").Single()));
}
catch (Exception ex)
{
throw new ConsulRequestException("Failed to parse Age", res.StatusCode, ex);
}
}
}

protected override void ApplyHeaders(HttpRequestMessage message, ConsulClientConfiguration clientConfig)
Expand All @@ -246,7 +292,7 @@ public GetRequest(ConsulClient client, string url, QueryOptions options = null)
{
if (string.IsNullOrEmpty(url))
{
throw new ArgumentException(nameof(url));
throw new ArgumentException(null, nameof(url));
}
Options = options ?? QueryOptions.Default;
}
Expand Down Expand Up @@ -306,6 +352,27 @@ protected override void ApplyOptions(ConsulClientConfiguration clientConfig)
{
Params["dc"] = Options.Datacenter;
}

if (Options.UseCache && Options.Consistency != ConsistencyMode.Consistent)
winstxnhdw marked this conversation as resolved.
Show resolved Hide resolved
{
Params["cached"] = string.Empty;
var cacheControl = new List<string>();

if (Options.MaxAge.HasValue && Options.MaxAge > TimeSpan.Zero)
winstxnhdw marked this conversation as resolved.
Show resolved Hide resolved
{
cacheControl.Add($"max-age={Options.MaxAge.Value.Seconds}");
winstxnhdw marked this conversation as resolved.
Show resolved Hide resolved
}

if (Options.StaleIfError.HasValue && Options.StaleIfError > TimeSpan.Zero)
winstxnhdw marked this conversation as resolved.
Show resolved Hide resolved
{
cacheControl.Add($"stale-if-error={Options.StaleIfError.Value.Seconds}");
winstxnhdw marked this conversation as resolved.
Show resolved Hide resolved
}

if (cacheControl.Count > 0)
{
Params["Cache-Control"] = string.Join(",", cacheControl);
}
}
}

protected override void ApplyHeaders(HttpRequestMessage message, ConsulClientConfiguration clientConfig)
Expand Down
28 changes: 26 additions & 2 deletions Consul/Client_Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,39 @@ public class QueryOptions
};

/// <summary>
/// Providing a datacenter overwrites the DC provided by the Config
/// Providing a datacenter overwrites the DC provided by the Config.
/// </summary>
public string Datacenter { get; set; }

/// <summary>
/// The consistency level required for the operation
/// The consistency level required for the operation.
/// </summary>
public ConsistencyMode Consistency { get; set; }

/// <summary>
/// UseCache requests that the agent cache results locally.
/// See https://www.consul.io/api/features/caching.html for more details on the semantics.
/// </summary>
public bool UseCache { get; set; }

/// <summary>
/// MaxAge limits how old a cached value will be returned if UseCache is true.
/// If there is a cached response that is older than the MaxAge, it is treated as a cache miss and a new fetch invoked.
/// If the fetch fails, the error is returned.
/// Clients that wish to allow for stale results on error can set StaleIfError to a longer duration to change this behavior.
/// It is ignored if the endpoint supports background refresh caching.
/// See https://www.consul.io/api/features/caching.html for more details.
/// </summary>
public TimeSpan? MaxAge { get; set; }

/// <summary>
/// StaleIfError specifies how stale the client will accept a cached response if the servers are unavailable to fetch a fresh one.
/// Only makes sense when UseCache is true and MaxAge is set to a lower, non-zero value.
/// It is ignored if the endpoint supports background refresh caching.
/// See https://www.consul.io/api/features/caching.html for more details.
/// </summary>
public TimeSpan? StaleIfError { get; set; }

/// <summary>
/// WaitIndex is used to enable a blocking query. Waits until the timeout or the next index is reached
/// </summary>
Expand Down
18 changes: 18 additions & 0 deletions Consul/Client_Results.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,24 @@ public ConsulResult(ConsulResult other)
/// </summary>
public class QueryResult : ConsulResult
{
public enum CacheResult
{
UNDEFINED,
MISS,
winstxnhdw marked this conversation as resolved.
Show resolved Hide resolved
HIT
}

/// <summary>
/// In all cases the HTTP `X-Cache` header is always set in the response to either `HIT` or `MISS` indicating whether the response was served from cache or not.
/// </summary>
public CacheResult XCache { get; set; }
winstxnhdw marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// For cache hits, the HTTP `Age` header is always set in the response to indicate how many seconds since that response was fetched from the servers.
/// As long as the local agent has an active connection to the servers, the age will always be 0 since the value is up-to-date.
/// </summary>
public TimeSpan Age { get; set; }

/// <summary>
/// The index number when the query was serviced. This can be used as a WaitIndex to perform a blocking query
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions Consul/Interfaces/ICatalogEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ namespace Consul
public interface ICatalogEndpoint
{
Task<QueryResult<string[]>> Datacenters(CancellationToken ct = default(CancellationToken));
Task<QueryResult<string[]>> Datacenters(QueryOptions q, CancellationToken ct = default(CancellationToken));
winstxnhdw marked this conversation as resolved.
Show resolved Hide resolved
Task<WriteResult> Deregister(CatalogDeregistration reg, CancellationToken ct = default(CancellationToken));
Task<WriteResult> Deregister(CatalogDeregistration reg, WriteOptions q, CancellationToken ct = default(CancellationToken));
Task<QueryResult<CatalogNode>> Node(string node, CancellationToken ct = default(CancellationToken));
Expand Down
2 changes: 1 addition & 1 deletion Consul/Lock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ public TimeSpan LockRetryTime
/// <summary>
/// When set to false, <see cref="Lock.Acquire"/> will block forever until the lock is acquired. LockWaitTime is ignored in this case.
/// <br/>
/// When set to true, <see cref="Lock.Acquire"/> the lock within a timestamp (It is analogous to <c>SemaphoreSlim.Wait(Timespan timeout)</c>.
/// When set to true, <see cref="Lock.Acquire"/> the lock within a timestamp (It is analogous to <c>SemaphoreSlim.Wait(Timespan timeout)</c>.
/// Under the hood, it attempts to acquire the lock multiple times if needed (due to the HTTP Long Poll returning early),
/// and will do so as many times as it can within the bounds set by LockWaitTime.
/// If LockWaitTime is set to 0, there will be only single attempt to acquire the lock.
Expand Down