ruststream-fred 0.7.0

Redis / Valkey broker implementation (Streams) for the RustStream messaging framework, backed by fred.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
//! Live tests for the Pub/Sub form against a real server.
//!
//! What lives here is what only the server can confirm: `PSUBSCRIBE` glob matching, the two
//! delivery modes that do not interoperate, and the moment the server starts routing a channel to
//! the subscriber. Each case reads the server's own view of its subscriptions
//! (`PUBSUB CHANNELS`, `PUBSUB SHARDCHANNELS`, `PUBSUB NUMPAT`) beside the deliveries, because a
//! transport that keeps nothing cannot be asked afterwards what it did.
//!
//! ```bash
//! just test-brokers
//! ```

use std::collections::BTreeSet;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use fred::interfaces::PubsubInterface;
use futures::StreamExt;
use ruststream::{
    AckError, Broker, ConnectedBroker, HeaderMap, IncomingMessage, OutgoingMessage, Publisher,
    Subscriber,
};
use ruststream_fred::{
    ConnectedRedisBroker, PubSubMode, RedisBroker, RedisPubSub, RedisPubSubPattern,
    RedisPubSubPublish,
};

mod live;

const WAIT: Duration = Duration::from_secs(5);

/// How long a case waits before calling a delivery absent. Pub/Sub routes within the round trip
/// the publish already costs, so anything not here by now is not coming.
const QUIET: Duration = Duration::from_millis(300);

fn env(key: &str) -> Option<String> {
    live::url(key)
}

/// A channel name unique to this run. Redis keeps Pub/Sub registrations per server, so a name
/// reused by another case - or by a run whose subscriber has not finished closing - would let one
/// subscriber answer another's publish.
fn unique_channel(base: &str) -> String {
    static RUN: OnceLock<u128> = OnceLock::new();
    static N: AtomicU64 = AtomicU64::new(0);
    let run = RUN.get_or_init(|| {
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("a clock after the epoch")
            .as_nanos()
    });
    format!(
        "ruststream-ps.{base}.{run}.{}",
        N.fetch_add(1, Ordering::Relaxed)
    )
}

async fn standalone(url: String) -> ConnectedRedisBroker {
    RedisBroker::standalone(url)
        .connect()
        .await
        .expect("connect to redis")
}

async fn next<S>(stream: &mut S) -> S::Item
where
    S: futures::Stream + Unpin,
{
    tokio::time::timeout(WAIT, stream.next())
        .await
        .expect("delivery within timeout")
        .expect("stream has a next item")
}

async fn none_within<S>(stream: &mut S, label: &str)
where
    S: futures::Stream + Unpin,
{
    let polled = tokio::time::timeout(QUIET, stream.next()).await;
    assert!(polled.is_err(), "{label}: expected no delivery");
}

/// The channels the server is currently routing that match `glob`, from its own registry.
async fn channels(broker: &ConnectedRedisBroker, glob: &str) -> Vec<String> {
    broker
        .pool_handle()
        .expect("live pool")
        .next()
        .pubsub_channels(glob)
        .await
        .expect("pubsub channels")
}

/// The same, for the sharded registry, which is a separate one.
async fn shard_channels(broker: &ConnectedRedisBroker, glob: &str) -> Vec<String> {
    broker
        .pool_handle()
        .expect("live pool")
        .next()
        .pubsub_shardchannels(glob)
        .await
        .expect("pubsub shardchannels")
}

/// How many glob subscriptions the server holds. There is no command that lists them, so a case
/// that needs a pattern's registration compares this count around its own subscribe.
async fn pattern_count(broker: &ConnectedRedisBroker) -> u64 {
    broker
        .pool_handle()
        .expect("live pool")
        .next()
        .pubsub_numpat()
        .await
        .expect("pubsub numpat")
}

async fn publish(broker: &ConnectedRedisBroker, policy: RedisPubSubPublish, channel: &str) {
    broker
        .pubsub_publisher(policy)
        .publish(OutgoingMessage::new(channel, b"hello"), None)
        .await
        .expect("publish");
}

/// A subscribe that has returned is a subscribe the server is already routing, so the publish
/// right after it is delivered rather than dropped on a channel nobody is on yet. The negative
/// half is the transport's own rule: what was published before the subscribe is gone.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_subscribe_returns_only_once_the_server_routes_the_channel() {
    let Some(url) = env("REDIS_TEST_URL") else {
        return;
    };
    let broker = standalone(url).await;
    let channel = unique_channel("confirmed");

    assert_eq!(
        channels(&broker, &channel).await,
        Vec::<String>::new(),
        "the channel must be idle before anything subscribes to it",
    );
    // Nobody is listening yet, so this one is lost by design and cannot be confused with the one
    // published after the subscribe.
    publish(&broker, RedisPubSubPublish::new(), &channel).await;

    let mut sub = broker
        .subscribe_pubsub(RedisPubSub::new(&channel))
        .await
        .expect("subscribe pubsub");

    assert_eq!(
        channels(&broker, &channel).await,
        vec![channel.clone()],
        "the server must already route the channel when the subscribe returns",
    );

    let mut stream = Box::pin(sub.stream());
    none_within(&mut stream, "the message published before the subscribe").await;

    // One publish, one delivery: no retry loop, because the registration is already in place.
    publish(&broker, RedisPubSubPublish::new(), &channel).await;
    let msg = next(&mut stream).await.expect("delivery ok");
    assert_eq!(msg.payload(), b"hello");

    drop(stream);
    broker.shutdown().await.expect("shutdown");
}

/// Pub/Sub settles nothing, and says so instead of reporting a success it did not perform.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_pubsub_delivery_cannot_be_settled() {
    let Some(url) = env("REDIS_TEST_URL") else {
        return;
    };
    let broker = standalone(url).await;
    let channel = unique_channel("unsettleable");

    let mut sub = broker
        .subscribe_pubsub(RedisPubSub::new(&channel))
        .await
        .expect("subscribe pubsub");
    let mut stream = Box::pin(sub.stream());

    publish(&broker, RedisPubSubPublish::new(), &channel).await;
    let first = next(&mut stream).await.expect("delivery ok");
    assert!(
        matches!(first.ack().await, Err(AckError::Unsupported)),
        "a channel has nothing to acknowledge with",
    );

    publish(&broker, RedisPubSubPublish::new(), &channel).await;
    let second = next(&mut stream).await.expect("second delivery");
    assert!(
        matches!(second.nack(true).await, Err(AckError::Unsupported)),
        "a requeue on a channel would be a republish the transport never makes",
    );

    drop(stream);
    broker.shutdown().await.expect("shutdown");
}

/// The glob form: one subscription reads every channel the pattern matches, each delivery naming
/// the concrete channel it arrived on, and a channel outside the glob reaches it not at all.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_pattern_subscription_reads_every_channel_its_glob_matches() {
    let Some(url) = env("REDIS_TEST_URL") else {
        return;
    };
    let broker = standalone(url).await;
    let base = unique_channel("pattern");
    let matching = [format!("{base}.created"), format!("{base}.shipped")];
    let outside = format!("{base}-other.created");

    let before = pattern_count(&broker).await;
    let mut sub = broker
        .subscribe_pubsub_pattern(RedisPubSubPattern::new(format!("{base}.*")))
        .await
        .expect("subscribe pattern");
    assert!(
        pattern_count(&broker).await > before,
        "the server must hold the glob subscription once the subscribe returns",
    );

    let mut stream = Box::pin(sub.stream());
    for channel in &matching {
        publish(&broker, RedisPubSubPublish::new(), channel).await;
    }

    // A pool spreads publishes over its connections, so the two channels may arrive either way
    // round; what the glob promises is that both do.
    let mut seen = BTreeSet::new();
    for _ in 0..matching.len() {
        let msg = next(&mut stream).await.expect("matched delivery");
        assert_eq!(msg.payload(), b"hello");
        assert!(
            msg.from_pattern(),
            "a delivery through a glob must report itself as one",
        );
        seen.insert(msg.channel().to_owned());
    }
    assert_eq!(
        seen,
        matching.iter().cloned().collect::<BTreeSet<_>>(),
        "each delivery names the concrete channel it was published to, not the glob",
    );

    // One character off the glob is off the subscription.
    publish(&broker, RedisPubSubPublish::new(), &outside).await;
    none_within(&mut stream, "a channel the glob does not match").await;

    drop(stream);
    broker.shutdown().await.expect("shutdown");
}

/// Sharded delivery is a registry of its own: a sharded subscriber is invisible to `PUBLISH`, and
/// the server says so before any message is sent.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_sharded_subscription_is_reached_only_by_a_sharded_publish() {
    let Some(url) = env("REDIS_TEST_URL") else {
        return;
    };
    let broker = standalone(url).await;
    let channel = unique_channel("sharded");

    let mut sub = broker
        .subscribe_pubsub(RedisPubSub::new(&channel).mode(PubSubMode::Sharded))
        .await
        .expect("subscribe sharded");

    assert_eq!(
        shard_channels(&broker, &channel).await,
        vec![channel.clone()],
        "a sharded subscribe registers in the sharded registry",
    );
    assert_eq!(
        channels(&broker, &channel).await,
        Vec::<String>::new(),
        "and nowhere else: `PUBLISH` cannot see it",
    );

    let mut stream = Box::pin(sub.stream());

    publish(&broker, RedisPubSubPublish::new(), &channel).await;
    none_within(&mut stream, "a classic publish to a sharded subscriber").await;

    publish(
        &broker,
        RedisPubSubPublish::new().mode(PubSubMode::Sharded),
        &channel,
    )
    .await;
    let msg = next(&mut stream).await.expect("sharded delivery");
    assert_eq!(msg.payload(), b"hello");

    drop(stream);
    broker.shutdown().await.expect("shutdown");
}

/// The other direction of the same rule, so neither mode can be the one that quietly works both
/// ways.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_classic_subscription_is_not_reached_by_a_sharded_publish() {
    let Some(url) = env("REDIS_TEST_URL") else {
        return;
    };
    let broker = standalone(url).await;
    let channel = unique_channel("classic");

    let mut sub = broker
        .subscribe_pubsub(RedisPubSub::new(&channel))
        .await
        .expect("subscribe classic");

    assert_eq!(
        channels(&broker, &channel).await,
        vec![channel.clone()],
        "a classic subscribe registers in the classic registry",
    );
    assert_eq!(
        shard_channels(&broker, &channel).await,
        Vec::<String>::new(),
        "and nowhere else: `SPUBLISH` cannot see it",
    );

    let mut stream = Box::pin(sub.stream());

    publish(
        &broker,
        RedisPubSubPublish::new().mode(PubSubMode::Sharded),
        &channel,
    )
    .await;
    none_within(&mut stream, "a sharded publish to a classic subscriber").await;

    publish(&broker, RedisPubSubPublish::new(), &channel).await;
    let msg = next(&mut stream).await.expect("classic delivery");
    assert_eq!(msg.payload(), b"hello");

    drop(stream);
    broker.shutdown().await.expect("shutdown");
}

/// A channel is shared with whatever else publishes to it, so a value that carries no frame of
/// this crate's arrives as the payload it is, with no headers invented around it.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_value_published_by_another_client_arrives_as_the_bare_payload() {
    let Some(url) = env("REDIS_TEST_URL") else {
        return;
    };
    let broker = standalone(url).await;
    let channel = unique_channel("foreign");

    let mut sub = broker
        .subscribe_pubsub(RedisPubSub::new(&channel))
        .await
        .expect("subscribe pubsub");
    let mut stream = Box::pin(sub.stream());

    // A plain `PUBLISH`, the way any other Redis client would make it.
    let _: i64 = broker
        .pool_handle()
        .expect("live pool")
        .next()
        .publish(channel.as_str(), "plain text")
        .await
        .expect("publish");

    let msg = next(&mut stream).await.expect("delivery ok");
    assert_eq!(msg.payload(), b"plain text");
    assert_eq!(
        msg.headers(),
        &HeaderMap::new(),
        "an unframed value carries no headers, and none are read out of its bytes",
    );

    drop(stream);
    broker.shutdown().await.expect("shutdown");
}

/// Both delivery modes on a cluster, which is where they differ in what they cost: a classic
/// publish is broadcast to every node, a sharded one stays on the slot its channel hashes to.
/// The subscriptions are opened against the cluster the same way a service opens them.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_cluster_serves_both_delivery_modes() {
    let Some(node) = env("REDIS_CLUSTER_TEST_URL") else {
        return;
    };
    let broker = RedisBroker::cluster([node])
        .connect()
        .await
        .expect("connect to the cluster");

    for mode in [PubSubMode::Classic, PubSubMode::Sharded] {
        let channel = unique_channel("cluster");
        let mut sub = broker
            .subscribe_pubsub(RedisPubSub::new(&channel).mode(mode))
            .await
            .unwrap_or_else(|err| panic!("subscribe {mode:?}: {err}"));
        let mut stream = Box::pin(sub.stream());

        publish(&broker, RedisPubSubPublish::new().mode(mode), &channel).await;
        let msg = next(&mut stream).await.expect("delivery");
        assert_eq!(msg.payload(), b"hello", "{mode:?} lost its message");
        assert_eq!(msg.channel(), channel);

        drop(stream);
    }

    broker.shutdown().await.expect("shutdown");
}