Skip to content

Commit 2c62fde

Browse files
committed
Feat: add Redis governance query candidate selection
1 parent 603338a commit 2c62fde

5 files changed

Lines changed: 254 additions & 0 deletions
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using ModularityKit.Mutator.Governance.Redis.Storage.Candidates.Models;
2+
using ModularityKit.Mutator.Governance.Redis.Storage.Identifiers.Loading;
3+
4+
namespace ModularityKit.Mutator.Governance.Redis.Storage.Candidates.Execution;
5+
6+
/// <summary>
7+
/// Executes candidate id lookup plans against Redis set data.
8+
/// </summary>
9+
internal sealed class RedisMutationRequestCandidateExecutor(RedisMutationRequestIdSetReader idSetReader)
10+
{
11+
private readonly RedisMutationRequestIdSetReader _idSetReader = idSetReader ?? throw new ArgumentNullException(nameof(idSetReader));
12+
13+
/// <summary>
14+
/// Executes a candidate plan and returns the resulting request identifiers.
15+
/// </summary>
16+
/// <param name="plan">The candidate plan to execute.</param>
17+
/// <param name="cancellationToken">The cancellation token.</param>
18+
/// <returns>The resolved request identifiers.</returns>
19+
public async Task<IReadOnlyList<string>> ExecuteAsync(RedisMutationRequestCandidatePlan plan, CancellationToken cancellationToken)
20+
{
21+
ArgumentNullException.ThrowIfNull(plan);
22+
23+
return plan.Operation switch
24+
{
25+
RedisMutationRequestCandidateOperation.ExplicitIds => plan.ExplicitRequestIds ?? [],
26+
RedisMutationRequestCandidateOperation.SingleSet => await LoadSingleSetAsync(plan, cancellationToken).ConfigureAwait(false),
27+
RedisMutationRequestCandidateOperation.Union => await LoadUnionAsync(plan, cancellationToken).ConfigureAwait(false),
28+
RedisMutationRequestCandidateOperation.Intersection => await LoadIntersectionAsync(plan, cancellationToken).ConfigureAwait(false),
29+
_ => throw new InvalidOperationException($"Unsupported candidate operation '{plan.Operation}'.")
30+
};
31+
}
32+
33+
private Task<IReadOnlyList<string>> LoadSingleSetAsync(RedisMutationRequestCandidatePlan plan, CancellationToken cancellationToken) =>
34+
_idSetReader.LoadIdsAsync(plan.Keys[0], cancellationToken);
35+
36+
private Task<IReadOnlyList<string>> LoadUnionAsync(RedisMutationRequestCandidatePlan plan, CancellationToken cancellationToken) =>
37+
_idSetReader.LoadUnionedIdsAsync(plan.Keys, cancellationToken);
38+
39+
private Task<IReadOnlyList<string>> LoadIntersectionAsync(RedisMutationRequestCandidatePlan plan, CancellationToken cancellationToken) =>
40+
_idSetReader.LoadIntersectedIdsAsync(plan.Keys[0], plan.Keys[1], cancellationToken);
41+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace ModularityKit.Mutator.Governance.Redis.Storage.Candidates.Models;
2+
3+
/// <summary>
4+
/// Defines how Redis request id candidates should be loaded for queries.
5+
/// </summary>
6+
internal enum RedisMutationRequestCandidateOperation
7+
{
8+
/// <summary>
9+
/// Uses an already known explicit list of request identifiers.
10+
/// </summary>
11+
ExplicitIds = 0,
12+
13+
/// <summary>
14+
/// Loads identifiers from a single Redis set.
15+
/// </summary>
16+
SingleSet = 1,
17+
18+
/// <summary>
19+
/// Loads identifiers from the union of multiple Redis sets.
20+
/// </summary>
21+
Union = 2,
22+
23+
/// <summary>
24+
/// Loads identifiers from the intersection of multiple Redis sets.
25+
/// </summary>
26+
Intersection = 3
27+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using StackExchange.Redis;
2+
3+
namespace ModularityKit.Mutator.Governance.Redis.Storage.Candidates.Models;
4+
5+
/// <summary>
6+
/// Represents a planned Redis candidate-id lookup operation.
7+
/// </summary>
8+
/// <param name="Operation">The candidate lookup operation to execute.</param>
9+
/// <param name="Keys">The Redis keys participating in the operation.</param>
10+
/// <param name="ExplicitRequestIds">The explicit request identifiers when no Redis set lookup is required.</param>
11+
internal sealed record RedisMutationRequestCandidatePlan(
12+
RedisMutationRequestCandidateOperation Operation,
13+
IReadOnlyList<RedisKey> Keys,
14+
IReadOnlyList<string>? ExplicitRequestIds = null);
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
2+
using ModularityKit.Mutator.Governance.Abstractions.Queries.Model;
3+
using ModularityKit.Mutator.Governance.Redis.Keys;
4+
using ModularityKit.Mutator.Governance.Redis.Storage.Candidates.Models;
5+
using StackExchange.Redis;
6+
7+
namespace ModularityKit.Mutator.Governance.Redis.Storage.Candidates.Planning;
8+
9+
/// <summary>
10+
/// Builds candidate-id lookup plans for Redis-backed request queries.
11+
/// </summary>
12+
internal sealed class RedisMutationRequestCandidatePlanBuilder(RedisMutationRequestKeyspace keyspace)
13+
{
14+
private readonly RedisMutationRequestKeyspace _keyspace = keyspace ?? throw new ArgumentNullException(nameof(keyspace));
15+
16+
/// <summary>
17+
/// Builds a plan that loads all known request identifiers.
18+
/// </summary>
19+
/// <returns>The candidate plan.</returns>
20+
public RedisMutationRequestCandidatePlan BuildAllRequestsPlan()
21+
=> Single(_keyspace.RequestIds());
22+
23+
/// <summary>
24+
/// Builds a plan that loads request identifiers for a specific state.
25+
/// </summary>
26+
/// <param name="stateId">The state identifier.</param>
27+
/// <returns>The candidate plan.</returns>
28+
public RedisMutationRequestCandidatePlan BuildByStateIdPlan(string stateId)
29+
=> Single(_keyspace.RequestsByStateId(stateId));
30+
31+
/// <summary>
32+
/// Builds a plan that loads pending request identifiers, optionally narrowed by pending reason.
33+
/// </summary>
34+
/// <param name="reason">The optional pending reason.</param>
35+
/// <returns>The candidate plan.</returns>
36+
public RedisMutationRequestCandidatePlan BuildPendingPlan(PendingMutationReason? reason)
37+
=> Single(GetPendingKey(reason));
38+
39+
/// <summary>
40+
/// Builds a plan that loads pending request identifiers for a specific state.
41+
/// </summary>
42+
/// <param name="stateId">The state identifier.</param>
43+
/// <param name="reason">The optional pending reason.</param>
44+
/// <returns>The candidate plan.</returns>
45+
public RedisMutationRequestCandidatePlan BuildPendingByStateIdPlan(string stateId, PendingMutationReason? reason)
46+
=> Intersect(_keyspace.RequestsByStateId(stateId), GetPendingKey(reason));
47+
48+
/// <summary>
49+
/// Builds a best-effort Redis candidate plan for the supplied request query.
50+
/// </summary>
51+
/// <param name="query">The request query to analyze.</param>
52+
/// <returns>The candidate plan.</returns>
53+
public RedisMutationRequestCandidatePlan BuildQueryPlan(MutationRequestQuery query)
54+
{
55+
ArgumentNullException.ThrowIfNull(query);
56+
57+
if (query.RequestIds.Count > 0)
58+
return Explicit(query.RequestIds);
59+
60+
if (query.PendingReasons.Count > 0)
61+
return Union(query.PendingReasons.Select(_keyspace.PendingRequestIds));
62+
63+
if (query.Statuses.Count > 0)
64+
{
65+
var pendingOnly = query.Statuses.All(status => status == MutationRequestStatus.Pending);
66+
return pendingOnly
67+
? BuildPendingPlan(reason: null)
68+
: Union(query.Statuses.Select(_keyspace.RequestsByStatus));
69+
}
70+
71+
if (query.StateIds.Count > 0)
72+
return Union(query.StateIds.Select(_keyspace.RequestsByStateId));
73+
74+
return BuildAllRequestsPlan();
75+
}
76+
77+
private RedisMutationRequestCandidatePlan Explicit(IEnumerable<string> requestIds)
78+
=> new(RedisMutationRequestCandidateOperation.ExplicitIds, Keys: [],
79+
ExplicitRequestIds: requestIds
80+
.Where(requestId => !string.IsNullOrWhiteSpace(requestId))
81+
.Distinct(StringComparer.Ordinal)
82+
.ToArray());
83+
84+
private RedisMutationRequestCandidatePlan Single(RedisKey key)
85+
=> new(RedisMutationRequestCandidateOperation.SingleSet, Keys: [key]);
86+
87+
private RedisMutationRequestCandidatePlan Union(IEnumerable<RedisKey> keys)
88+
{
89+
var materialized = keys.Distinct().ToArray();
90+
if (materialized.Length == 1)
91+
return Single(materialized[0]);
92+
93+
return new RedisMutationRequestCandidatePlan(
94+
RedisMutationRequestCandidateOperation.Union,
95+
materialized);
96+
}
97+
98+
private RedisMutationRequestCandidatePlan Intersect(RedisKey left, RedisKey right)
99+
=> new(RedisMutationRequestCandidateOperation.Intersection, [left, right]);
100+
101+
private RedisKey GetPendingKey(PendingMutationReason? reason)
102+
=> reason.HasValue ? _keyspace.PendingRequestIds(reason.Value) : _keyspace.PendingRequestIds();
103+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using ModularityKit.Mutator.Governance.Abstractions.Lifecycle.Model;
2+
using ModularityKit.Mutator.Governance.Abstractions.Queries.Model;
3+
using ModularityKit.Mutator.Governance.Redis.Storage.Candidates.Execution;
4+
using ModularityKit.Mutator.Governance.Redis.Storage.Candidates.Models;
5+
using ModularityKit.Mutator.Governance.Redis.Storage.Candidates.Planning;
6+
7+
namespace ModularityKit.Mutator.Governance.Redis.Storage.Candidates;
8+
9+
/// <summary>
10+
/// Selects Redis request-id candidates for higher-level request queries.
11+
/// </summary>
12+
internal sealed class RedisMutationRequestQueryCandidateSelector(
13+
RedisMutationRequestCandidatePlanBuilder planBuilder,
14+
RedisMutationRequestCandidateExecutor candidateExecutor)
15+
{
16+
private readonly RedisMutationRequestCandidatePlanBuilder _planBuilder =
17+
planBuilder ?? throw new ArgumentNullException(nameof(planBuilder));
18+
19+
private readonly RedisMutationRequestCandidateExecutor _candidateExecutor =
20+
candidateExecutor ?? throw new ArgumentNullException(nameof(candidateExecutor));
21+
22+
/// <summary>
23+
/// Loads all known request identifiers.
24+
/// </summary>
25+
/// <param name="cancellationToken">The cancellation token.</param>
26+
/// <returns>The resolved request identifiers.</returns>
27+
public Task<IReadOnlyList<string>> LoadAllRequestIdsAsync(CancellationToken cancellationToken) =>
28+
LoadAsync(_planBuilder.BuildAllRequestsPlan(), cancellationToken);
29+
30+
/// <summary>
31+
/// Loads request identifiers for a specific state.
32+
/// </summary>
33+
/// <param name="stateId">The state identifier.</param>
34+
/// <param name="cancellationToken">The cancellation token.</param>
35+
/// <returns>The resolved request identifiers.</returns>
36+
public Task<IReadOnlyList<string>> LoadByStateIdAsync(string stateId, CancellationToken cancellationToken) =>
37+
LoadAsync(_planBuilder.BuildByStateIdPlan(stateId), cancellationToken);
38+
39+
/// <summary>
40+
/// Loads pending request identifiers, optionally narrowed by reason.
41+
/// </summary>
42+
/// <param name="reason">The optional pending reason.</param>
43+
/// <param name="cancellationToken">The cancellation token.</param>
44+
/// <returns>The resolved request identifiers.</returns>
45+
public Task<IReadOnlyList<string>> LoadPendingAsync(PendingMutationReason? reason, CancellationToken cancellationToken) =>
46+
LoadAsync(_planBuilder.BuildPendingPlan(reason), cancellationToken);
47+
48+
/// <summary>
49+
/// Loads pending request identifiers for a specific state.
50+
/// </summary>
51+
/// <param name="stateId">The state identifier.</param>
52+
/// <param name="reason">The optional pending reason.</param>
53+
/// <param name="cancellationToken">The cancellation token.</param>
54+
/// <returns>The resolved request identifiers.</returns>
55+
public Task<IReadOnlyList<string>> LoadPendingByStateIdAsync(string stateId, PendingMutationReason? reason, CancellationToken cancellationToken) =>
56+
LoadAsync(_planBuilder.BuildPendingByStateIdPlan(stateId, reason), cancellationToken);
57+
58+
/// <summary>
59+
/// Loads request identifiers for a general request query using Redis-side candidate narrowing.
60+
/// </summary>
61+
/// <param name="query">The query to analyze.</param>
62+
/// <param name="cancellationToken">The cancellation token.</param>
63+
/// <returns>The resolved request identifiers.</returns>
64+
public Task<IReadOnlyList<string>> LoadQueryCandidatesAsync(MutationRequestQuery query, CancellationToken cancellationToken) =>
65+
LoadAsync(_planBuilder.BuildQueryPlan(query), cancellationToken);
66+
67+
private Task<IReadOnlyList<string>> LoadAsync(RedisMutationRequestCandidatePlan plan, CancellationToken cancellationToken) =>
68+
_candidateExecutor.ExecuteAsync(plan, cancellationToken);
69+
}

0 commit comments

Comments
 (0)