Skip to content

Commit 01746eb

Browse files
committed
Draft support for 8.8 arrays (#3076)
* draft support for 8.8 arrays * - mark API as experimental - add keyspace notification tests - tidying - docs * clarify how last-items interacts with ring buffers * fix CI netfx compilation * use ValuePairInterleavedProcessorBase to ensure that jagged vs flat doesn't impact us (RESP2 vs RESP3) * make life even easier * add Array to signature prefix list * stabilize hotkeys CI * fix last-minute ARGREP result change
1 parent 9b3067b commit 01746eb

28 files changed

Lines changed: 2829 additions & 53 deletions

docs/Arrays.md

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# Redis Arrays
2+
3+
Redis Arrays provide sparse arrays of arbitrary Redis values with unsigned array indexes and a notional write head. SE.Redis exposes the array API as experimental Redis 8.8 APIs; callers should expect details to change while the server feature is still in preview.
4+
5+
## Prerequisites
6+
7+
Arrays require Redis 8.8 or later. The APIs are marked with the `SER006` experimental warning.
8+
9+
## Basic Usage
10+
11+
Use `ArraySetAsync` and `ArrayGetAsync` to write and read individual cells:
12+
13+
```csharp
14+
var db = conn.GetDatabase();
15+
RedisKey key = "events";
16+
17+
bool inserted = await db.ArraySetAsync(key, 0, "created");
18+
RedisValue value = await db.ArrayGetAsync(key, 0);
19+
RedisValue missing = await db.ArrayGetAsync(key, 1);
20+
21+
Console.WriteLine(inserted); // True when the cell did not previously have a value
22+
Console.WriteLine(value); // created
23+
Console.WriteLine(missing.IsNull); // True
24+
```
25+
26+
Array indexes use `RedisArrayIndex`, with implicit conversions from `int`, `long`, and `ulong`. This allows normal small indexes to be used directly, while still allowing the full unsigned index range when needed.
27+
28+
```csharp
29+
await db.ArraySetAsync(key, 42, "answer");
30+
await db.ArraySetAsync(key, new RedisArrayIndex(10_000_000UL), "large index");
31+
```
32+
33+
## Sparse Arrays
34+
35+
Arrays are sparse: unset cells do not have values. `ArrayLengthAsync` reports the notional length, which is the highest used index plus one. `ArrayCountAsync` reports only cells that currently have values.
36+
37+
```csharp
38+
await db.KeyDeleteAsync(key);
39+
40+
await db.ArraySetAsync(key, 0, "a");
41+
await db.ArraySetAsync(key, 10, "b");
42+
43+
RedisArrayIndex length = await db.ArrayLengthAsync(key); // 11
44+
RedisArrayIndex count = await db.ArrayCountAsync(key); // 2
45+
```
46+
47+
## Setting Multiple Values
48+
49+
To write a contiguous range, pass the first index and the values:
50+
51+
```csharp
52+
int inserted = await db.ArraySetAsync(key, 0, ["a", "b", "c"]);
53+
```
54+
55+
To write multiple specific indexes, use `RedisArrayEntry` values:
56+
57+
```csharp
58+
await db.ArraySetAsync(key,
59+
[
60+
new RedisArrayEntry(0, "alpha"),
61+
new RedisArrayEntry(5, "bravo"),
62+
new RedisArrayEntry(100, "charlie"),
63+
]);
64+
```
65+
66+
The returned `int` is the number of cells that were newly filled.
67+
68+
## Reading Multiple Values
69+
70+
Read selected indexes with `ArrayGetAsync`:
71+
72+
```csharp
73+
RedisValue[] values = await db.ArrayGetAsync(key, [0, 5, 6, 100]);
74+
```
75+
76+
Read a range with `ArrayGetRangeAsync`. Ranges can be read forward or backward:
77+
78+
```csharp
79+
RedisValue[] forward = await db.ArrayGetRangeAsync(key, 0, 5);
80+
RedisValue[] reverse = await db.ArrayGetRangeAsync(key, 5, 0);
81+
```
82+
83+
For sparse arrays, use `ArrayScanAsync` to return only populated cells in a range:
84+
85+
```csharp
86+
RedisArrayEntry[] entries = await db.ArrayScanAsync(key, 0, 100, limit: 50);
87+
88+
foreach (var entry in entries)
89+
{
90+
Console.WriteLine($"{entry.Index}: {entry.Value}");
91+
}
92+
```
93+
94+
## Deleting Values
95+
96+
Delete a single cell with `ArrayDeleteAsync`:
97+
98+
```csharp
99+
bool removed = await db.ArrayDeleteAsync(key, 5);
100+
```
101+
102+
Delete multiple specific cells by index:
103+
104+
```csharp
105+
int removedCount = await db.ArrayDeleteAsync(key, [0, 5, 100]);
106+
```
107+
108+
Delete one or more ranges:
109+
110+
```csharp
111+
await db.ArrayDeleteRangeAsync(key, 10, 20);
112+
113+
await db.ArrayDeleteRangeAsync(key,
114+
[
115+
new RedisArrayRange(100, 199),
116+
new RedisArrayRange(500, 599),
117+
]);
118+
```
119+
120+
## Searching
121+
122+
Use `ArrayGrepRequest` with `ArrayGrepAsync` to search values. When `Start` or `End` is not specified, the server's open-ended lower or upper bound is used.
123+
124+
```csharp
125+
var request = new ArrayGrepRequest
126+
{
127+
Limit = 10,
128+
};
129+
request.AddPredicate(ArrayGrepRequest.Predicate.Match("error"));
130+
131+
RedisArrayEntry[] matches = await db.ArrayGrepAsync(key, request);
132+
133+
foreach (var match in matches)
134+
{
135+
Console.WriteLine(match.Index);
136+
}
137+
```
138+
139+
Set `IncludeValues` to return values along with the matching indexes:
140+
141+
```csharp
142+
var request = new ArrayGrepRequest
143+
{
144+
IncludeValues = true,
145+
};
146+
request.AddPredicate(ArrayGrepRequest.Predicate.Regex("^ERR[0-9]+"));
147+
148+
RedisArrayEntry[] matches = await db.ArrayGrepAsync(key, request);
149+
150+
foreach (var match in matches)
151+
{
152+
Console.WriteLine($"{match.Index}: {match.Value}");
153+
}
154+
```
155+
156+
Multiple predicates can be combined. By default, predicates are combined as `OR`; set `IsIntersection` to combine them as `AND`.
157+
158+
```csharp
159+
var request = new ArrayGrepRequest
160+
{
161+
IsIntersection = true,
162+
};
163+
request.AddPredicate(ArrayGrepRequest.Predicate.Match("redis"));
164+
request.AddPredicate(ArrayGrepRequest.Predicate.Glob("*array*"));
165+
166+
RedisArrayEntry[] matches = await db.ArrayGrepAsync(key, request);
167+
```
168+
169+
## Write Head
170+
171+
Arrays have a write head used by insert operations. `ArrayInsertAsync` writes at the current write head and advances it.
172+
173+
```csharp
174+
RedisArrayIndex first = await db.ArrayInsertAsync(key, "first");
175+
RedisArrayIndex second = await db.ArrayInsertAsync(key, "second");
176+
177+
RedisArrayIndex? next = await db.ArrayNextAsync(key);
178+
```
179+
180+
Move the write head with `ArraySeekAsync`:
181+
182+
```csharp
183+
bool moved = await db.ArraySeekAsync(key, 1_000);
184+
RedisArrayIndex written = await db.ArrayInsertAsync(key, "later");
185+
```
186+
187+
## Ring Buffers
188+
189+
Use `ArrayRingAsync` to keep at most a fixed number of cells and wrap writes around that capacity:
190+
191+
```csharp
192+
for (int i = 0; i < 10; i++)
193+
{
194+
await db.ArrayRingAsync(key, maxLength: 5, value: i);
195+
}
196+
197+
RedisArrayIndex count = await db.ArrayCountAsync(key); // 5
198+
```
199+
200+
`ArrayLastItemsAsync` is intended for this capped ring-buffer model. It reads the last values in the ring-buffer sense, where "last" relates to the retained values after wrap-around and trimming:
201+
202+
```csharp
203+
RedisValue[] last = await db.ArrayLastItemsAsync(key, count: 10);
204+
RedisValue[] lastReversed = await db.ArrayLastItemsAsync(key, count: 10, reverse: true);
205+
```
206+
207+
## Operations and Info
208+
209+
Use `ArrayOperationAsync` for simple server-side operations over a range:
210+
211+
```csharp
212+
RedisValue sum = await db.ArrayOperationAsync(key, 0, 10, ArrayOperation.Sum);
213+
RedisValue used = await db.ArrayOperationAsync(key, 0, 10, ArrayOperation.Used);
214+
RedisValue matches = await db.ArrayOperationAsync(key, 0, 10, ArrayOperation.Match, "error");
215+
```
216+
217+
Use `ArrayInfoAsync` for metadata:
218+
219+
```csharp
220+
ArrayInfo info = await db.ArrayInfoAsync(key);
221+
222+
Console.WriteLine($"Count: {info.Count}");
223+
Console.WriteLine($"Length: {info.Length}");
224+
Console.WriteLine($"Next insert index: {info.NextInsertIndex}");
225+
```

docs/ReleaseNotes.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ From 3.0, [release notes will be maintained in GitHub only](https://github.com/S
1515

1616
## 2.12.14
1717

18-
- (none)
18+
- Add experimental Redis 8.8 array support, including array APIs on `IDatabase`/`IDatabaseAsync`,
19+
array helper types, `RedisType.Array`, and array delete keyspace notification event types.
1920

2021
## 2.13.1
2122

docs/exp/SER006.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ Redis 8.8 is currently in preview and may be subject to change.
22

33
New features in Redis 8.8:
44

5-
- `XNACK` for stream negative acknowledgements
5+
- Arrays (`ARGET`, `ARSET` etc)
6+
- Stream negative acknowledgements (`XNACK`)
67
- `Aggregate.Count` for sorted-set combination operations
7-
- Sub-key notifications
8+
- Sub-key (hash) keyspace/keyevent notifications
89

910
The corresponding library features must also be considered subject to change:
1011

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Documentation
4646
- [Using RESP3](Resp3) - information on using RESP3
4747
- [ServerMaintenanceEvent](ServerMaintenanceEvent) - how to listen and prepare for hosted server maintenance (e.g. Azure Cache for Redis)
4848
- [Streams](Streams) - how to use the Stream data type
49+
- [Arrays](Arrays) - how to use Redis Arrays as sparse arrays of values
4950
- [Vector Sets](VectorSets) - how to use Vector Sets for similarity search with embeddings
5051
- [Where are `KEYS` / `SCAN` / `FLUSH*`?](KeysScan) - how to use server-based commands
5152
- [Profiling](Profiling) - profiling interfaces, as well as how to profile in an `async` world
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
using RESPite;
5+
6+
namespace StackExchange.Redis;
7+
8+
/// <summary>
9+
/// Describes an array entry at a specific index.
10+
/// </summary>
11+
/// <param name="index">The array index.</param>
12+
/// <param name="value">The value at this index.</param>
13+
[Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
14+
public readonly struct RedisArrayEntry(RedisArrayIndex index, RedisValue value) : IEquatable<RedisArrayEntry>
15+
{
16+
private readonly RedisArrayIndex _index = index;
17+
private readonly RedisValue _value = value;
18+
19+
internal RedisArrayEntry(RedisArrayIndex index)
20+
: this(index, default)
21+
{
22+
}
23+
24+
/// <summary>
25+
/// The array index.
26+
/// </summary>
27+
public RedisArrayIndex Index => _index;
28+
29+
/// <summary>
30+
/// The value at this index.
31+
/// </summary>
32+
public RedisValue Value => _value;
33+
34+
/// <summary>
35+
/// Converts to a key/value pair.
36+
/// </summary>
37+
/// <param name="value">The <see cref="RedisArrayEntry"/> to create a <see cref="KeyValuePair{TKey, TValue}"/> from.</param>
38+
public static implicit operator KeyValuePair<RedisArrayIndex, RedisValue>(RedisArrayEntry value) =>
39+
new KeyValuePair<RedisArrayIndex, RedisValue>(value._index, value._value);
40+
41+
/// <summary>
42+
/// Converts from a key/value pair.
43+
/// </summary>
44+
/// <param name="value">The <see cref="KeyValuePair{TKey, TValue}"/> to get a <see cref="RedisArrayEntry"/> from.</param>
45+
public static implicit operator RedisArrayEntry(KeyValuePair<RedisArrayIndex, RedisValue> value) =>
46+
new RedisArrayEntry(value.Key, value.Value);
47+
48+
/// <summary>
49+
/// The "{index}: {value}" string representation.
50+
/// </summary>
51+
public override string ToString() => _index + ": " + _value;
52+
53+
/// <inheritdoc />
54+
public override int GetHashCode() => _index.GetHashCode() ^ _value.GetHashCode();
55+
56+
/// <summary>
57+
/// Compares two values for equality.
58+
/// </summary>
59+
/// <param name="obj">The <see cref="RedisArrayEntry"/> to compare to.</param>
60+
public override bool Equals(object? obj) => obj is RedisArrayEntry entry && Equals(entry);
61+
62+
/// <summary>
63+
/// Compares two values for equality.
64+
/// </summary>
65+
/// <param name="other">The <see cref="RedisArrayEntry"/> to compare to.</param>
66+
public bool Equals(RedisArrayEntry other) => _index == other._index && _value == other._value;
67+
68+
/// <summary>
69+
/// Compares two values for equality.
70+
/// </summary>
71+
/// <param name="x">The first <see cref="RedisArrayEntry"/> to compare.</param>
72+
/// <param name="y">The second <see cref="RedisArrayEntry"/> to compare.</param>
73+
public static bool operator ==(RedisArrayEntry x, RedisArrayEntry y) => x._index == y._index && x._value == y._value;
74+
75+
/// <summary>
76+
/// Compares two values for non-equality.
77+
/// </summary>
78+
/// <param name="x">The first <see cref="RedisArrayEntry"/> to compare.</param>
79+
/// <param name="y">The second <see cref="RedisArrayEntry"/> to compare.</param>
80+
public static bool operator !=(RedisArrayEntry x, RedisArrayEntry y) => x._index != y._index || x._value != y._value;
81+
}

0 commit comments

Comments
 (0)