Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-store/src/redis_store.rs
Line
Count
Source
1
// Copyright 2024-2026 Trace Machina, Inc. All rights reserved.
2
//
3
// Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//    See LICENSE file for details
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
use core::cmp;
16
use core::fmt::Debug;
17
use core::marker::PhantomData;
18
use core::ops::{Bound, RangeBounds};
19
use core::pin::Pin;
20
use core::str::FromStr;
21
use core::time::Duration;
22
use std::borrow::Cow;
23
use std::collections::{HashMap, HashSet};
24
use std::sync::{Arc, Weak};
25
use std::time::Instant;
26
27
use async_trait::async_trait;
28
use bytes::Bytes;
29
use const_format::formatcp;
30
use futures::stream::FuturesUnordered;
31
use futures::{Stream, StreamExt, TryFutureExt, TryStreamExt, future};
32
use itertools::izip;
33
use nativelink_config::stores::{RedisMode, RedisSpec};
34
use nativelink_error::{Code, Error, ResultExt, make_err, make_input_err};
35
use nativelink_metric::MetricsComponent;
36
use nativelink_redis_tester::SubscriptionManagerNotify;
37
use nativelink_util::buf_channel::{DropCloserReadHalf, DropCloserWriteHalf};
38
use nativelink_util::common::DigestInfo;
39
use nativelink_util::health_utils::{HealthRegistryBuilder, HealthStatus, HealthStatusIndicator};
40
use nativelink_util::metrics::{record_connection_acquired, record_connection_reconnect};
41
use nativelink_util::store_trait::{
42
    BoolValue, RemoveCallback, SchedulerCurrentVersionProvider, SchedulerIndexProvider,
43
    SchedulerStore, SchedulerStoreDataProvider, SchedulerStoreDecodeTo, SchedulerStoreKeyProvider,
44
    SchedulerSubscription, SchedulerSubscriptionManager, StoreDriver, StoreKey, UploadSizeInfo,
45
};
46
use nativelink_util::task::JoinHandleDropGuard;
47
use nativelink_util::{background_spawn, spawn};
48
use parking_lot::{Mutex, RwLock};
49
use patricia_tree::StringPatriciaMap;
50
use redis::aio::{ConnectionLike, ConnectionManager, ConnectionManagerConfig};
51
use redis::cluster::ClusterClient;
52
use redis::cluster_async::ClusterConnection;
53
use redis::sentinel::{SentinelClient, SentinelNodeConnectionInfo, SentinelServerType};
54
use redis::{
55
    AsyncCommands, AsyncIter, Client, IntoConnectionInfo, PushInfo, RedisError, ScanOptions,
56
    Script, SetExpiry, SetOptions, Value, pipe,
57
};
58
use serde::Deserialize;
59
use serde::de::IntoDeserializer;
60
use tokio::select;
61
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
62
use tokio::sync::{Mutex as AsyncMutex, OnceCell, OwnedSemaphorePermit, Semaphore};
63
use tokio::time::{sleep, timeout};
64
use tokio_stream::wrappers::UnboundedReceiverStream;
65
use tracing::{debug, error, info, trace, warn};
66
use url::Url;
67
use uuid::Uuid;
68
69
use crate::cas_utils::is_zero_digest;
70
use crate::redis_utils::index_migration::{
71
    self, IndexKind, Replacement, Step, drop_keeping_documents, index_kind, is_cursor_lost,
72
    is_out_of_memory, migrated_name,
73
};
74
use crate::redis_utils::{
75
    FtAggregateCursor, FtAggregateOptions, FtCreateOptions, SearchSchema, ft_aggregate, ft_create,
76
    ft_info, ft_info_indexing, ft_info_pair, ft_search_count,
77
};
78
79
/// The default size of the read chunk when reading data from Redis.
80
/// Note: If this changes it should be updated in the config documentation.
81
const DEFAULT_READ_CHUNK_SIZE: usize = 64 * 1024;
82
83
/// The default size of the connection pool if not specified.
84
/// Note: If this changes it should be updated in the config documentation.
85
const DEFAULT_CONNECTION_POOL_SIZE: usize = 3;
86
87
/// The default delay between retries if not specified.
88
/// Note: If this changes it should be updated in the config documentation.
89
const DEFAULT_RETRY_DELAY: f32 = 0.1;
90
91
/// Maximum attempts (re-resolving the master each time) for an operation to
92
/// ride out a transient Redis topology change — a failover, a dropped
93
/// connection, or brief replica lag. Used by `update`'s verify and by the read
94
/// paths so a transient error doesn't fail an otherwise healthy request.
95
const MAX_REDIS_RETRY_ATTEMPTS: u32 = 5;
96
97
/// Whether a Redis error is worth re-resolving the master and retrying: a
98
/// dropped/refused connection, an IO error, or a write that hit a freshly
99
/// demoted replica. Anything else (e.g. a real protocol/logic error) is
100
/// returned as-is.
101
272
fn is_retryable_redis_error(err: &RedisError) -> bool {
102
272
    err.is_connection_dropped()
103
33
        || err.is_connection_refusal()
104
33
        || err.is_io_error()
105
31
        || err.kind() == redis::ErrorKind::Server(redis::ServerErrorKind::ReadOnly)
106
272
}
107
108
/// The default connection timeout in milliseconds if not specified.
109
/// Note: If this changes it should be updated in the config documentation.
110
const DEFAULT_CONNECTION_TIMEOUT_MS: u64 = 3000;
111
112
/// The default command timeout in milliseconds if not specified.
113
/// Note: If this changes it should be updated in the config documentation.
114
const DEFAULT_COMMAND_TIMEOUT_MS: u64 = 10_000;
115
116
/// The default `check_health` PING ceiling in milliseconds if not specified.
117
/// Note: If this changes it should be updated in the config documentation.
118
const DEFAULT_HEALTH_CHECK_TIMEOUT_MS: u64 = 4000;
119
120
/// The default maximum number of chunk uploads per update.
121
/// Note: If this changes it should be updated in the config documentation.
122
pub const DEFAULT_MAX_CHUNK_UPLOADS_PER_UPDATE: usize = 10;
123
124
/// The default COUNT value passed when scanning keys in Redis.
125
/// Note: If this changes it should be updated in the config documentation.
126
const DEFAULT_SCAN_COUNT: usize = 10_000;
127
128
/// The default COUNT value passed when scanning search indexes
129
/// Note: If this changes it should be updated in the config documentation.
130
pub const DEFAULT_MAX_COUNT_PER_CURSOR: u64 = 1_500;
131
132
const DEFAULT_CLIENT_PERMITS: usize = 500;
133
134
/// Converts a TTL to the seconds `EXPIRE` takes.
135
///
136
/// Clamped to at least one second. Redis treats `EXPIRE` with a value of zero
137
/// or less as "delete now", so a sub-second duration reaching here would
138
/// destroy the value it was meant to protect. Config already rejects zero, so
139
/// this only guards a future caller.
140
30
fn ttl_seconds(key_ttl: Duration) -> i64 {
141
30
    i64::try_from(key_ttl.as_secs()).unwrap_or(i64::MAX).max(1)
142
30
}
143
144
/// Lifetime of an upload's temp key on a store without `key_ttl_s`. Every
145
/// chunk arms it again, so it bounds the gap between two chunks, not the
146
/// upload.
147
///
148
/// Longer than any pause a client is allowed between chunks (Bazel's
149
/// `--remote_timeout` is 60 s by default, and a resumed upload waits at most
150
/// the `ByteStream` server's disconnect timeout), and short enough that the
151
/// uploads in flight when a CAS dies are gone within the hour. An upload that
152
/// goes quiet for longer fails rather than publishing a key that was
153
/// recreated without its first chunks.
154
const TEMP_KEY_TTL: Duration = Duration::from_hours(1);
155
156
/// `RENAME` of a key that is not there. On a retried publish that means
157
/// the first attempt applied before its reply was lost, or the failover
158
/// lost the temp key; the caller tells the two apart by the final key.
159
2
fn is_no_such_key(err: &RedisError) -> bool {
160
2
    err.kind() == redis::ErrorKind::Server(redis::ServerErrorKind::ResponseError)
161
2
        && err
162
2
            .detail()
163
2
            .is_some_and(|detail| detail.contains("no such key"))
164
2
}
165
166
/// A wrapper around Redis to allow it to be reconnected.
167
pub trait RedisManager<C>
168
where
169
    C: ConnectionLike + Clone,
170
{
171
    /// Get a connection manager and a unique identifier for this connection
172
    /// which may be used to issue a reconnect later.
173
    fn get_connection(&self) -> impl Future<Output = Result<(C, Uuid), Error>> + Send;
174
175
    /// Reconnect if the uuid matches the uuid returned from `get_connection()`.
176
    fn reconnect(&self, uuid: Uuid) -> impl Future<Output = Result<(C, Uuid), Error>> + Send;
177
178
    /// Get an invocation of the update version script for a given `key`.
179
    fn update_script(&self, key: &str) -> redis::ScriptInvocation<'_>;
180
181
    /// Configure the connection to have a psubscribe on it and perform the
182
    /// subscription on reconnect.
183
    fn psubscribe(&self, pattern: &str) -> impl Future<Output = Result<(), Error>> + Send;
184
}
185
186
#[derive(Debug)]
187
pub struct ClusterRedisManager<C>
188
where
189
    C: ConnectionLike + Clone,
190
{
191
    /// A constant Uuid, we never reconnect.
192
    uuid: Uuid,
193
194
    /// Redis script used to update a value in redis if the version matches.
195
    /// This is done by incrementing the version number and then setting the new
196
    /// data only if the version number matches the existing version number.
197
    update_if_version_matches_script: Script,
198
199
    /// The client pool connecting to the backing Redis instance(s).
200
    connection_manager: C,
201
}
202
203
impl<C> ClusterRedisManager<C>
204
where
205
    C: ConnectionLike + Clone,
206
{
207
50
    pub async fn new(mut connection_manager: C) -> Result<Self, Error> {
208
50
        let update_if_version_matches_script = Script::new(LUA_VERSION_SET_SCRIPT);
209
50
        update_if_version_matches_script
210
50
            .load_async(&mut connection_manager)
211
50
            .await
?0
;
212
50
        Ok(Self {
213
50
            uuid: Uuid::new_v4(),
214
50
            update_if_version_matches_script,
215
50
            connection_manager,
216
50
        })
217
50
    }
218
}
219
220
impl<C> RedisManager<C> for ClusterRedisManager<C>
221
where
222
    C: ConnectionLike + Clone + Send + Sync,
223
{
224
147
    fn get_connection(&self) -> impl Future<Output = Result<(C, Uuid), Error>> + Send {
225
147
        future::ready(Ok((self.connection_manager.clone(), self.uuid)))
226
147
    }
227
228
13
    fn reconnect(&self, _uuid: Uuid) -> impl Future<Output = Result<(C, Uuid), Error>> + Send {
229
13
        self.get_connection()
230
13
    }
231
232
0
    fn update_script(&self, key: &str) -> redis::ScriptInvocation<'_> {
233
0
        self.update_if_version_matches_script.key(key)
234
0
    }
235
236
4
    fn psubscribe(&self, _pattern: &str) -> impl Future<Output = Result<(), Error>> + Send {
237
        // This is a no-op for cluster connections.
238
4
        future::ready(Ok(()))
239
4
    }
240
}
241
242
type RedisConnectFuture<C> = dyn Future<Output = Result<C, Error>> + Send;
243
type RedisConnectFn<C> = dyn Fn() -> Pin<Box<RedisConnectFuture<C>>> + Send + Sync;
244
245
pub struct StandardRedisManager<C>
246
where
247
    C: ConnectionLike + Clone,
248
{
249
    /// Function used to re-connect to Redis.
250
    connect_func: Box<RedisConnectFn<C>>,
251
252
    /// Redis script used to update a value in redis if the version matches.
253
    /// This is done by incrementing the version number and then setting the new
254
    /// data only if the version number matches the existing version number.
255
    update_if_version_matches_script: Script,
256
257
    /// The client pool connecting to the backing Redis instance(s) and a Uuid
258
    /// for this connection in order to avoid multiple reconnection attempts.
259
    connection_manager: tokio::sync::RwLock<(C, Uuid)>,
260
261
    /// Serializes reconnect attempts so a Sentinel master failover triggers a
262
    /// single re-resolution instead of a thundering herd. Kept separate from
263
    /// `connection_manager` on purpose: the (potentially multi-second) connect
264
    /// runs while holding only this lock, so in-flight `get_connection` readers
265
    /// keep using the existing handle and are never frozen behind a slow
266
    /// reconnect. See [`Self::reconnect`].
267
    reconnect_lock: tokio::sync::Mutex<()>,
268
269
    /// A list of subscription that should be performed on reconnect.
270
    subscriptions: Mutex<HashSet<String>>,
271
}
272
273
impl<C> Debug for StandardRedisManager<C>
274
where
275
    C: ConnectionLike + Clone,
276
{
277
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
278
0
        f.debug_struct("StandardRedisManager")
279
0
            .field(
280
0
                "update_if_version_matches_script",
281
0
                &self.update_if_version_matches_script,
282
0
            )
283
0
            .field("subscriptions", &self.subscriptions)
284
0
            .finish()
285
0
    }
286
}
287
288
impl<C> StandardRedisManager<C>
289
where
290
    C: ConnectionLike + Clone + Send + Sync,
291
{
292
131
    async fn configure(&self, connection_manager: &mut C) -> Result<(), Error> {
293
131
        self.update_if_version_matches_script
294
131
            .load_async(connection_manager)
295
131
            .await
?0
;
296
131
        Ok(())
297
131
    }
298
299
116
    async fn new(connect_func: Box<RedisConnectFn<C>>) -> Result<Self, Error> {
300
116
        let 
connection_manager113
= connect_func().await
?3
;
301
113
        let update_if_version_matches_script = Script::new(LUA_VERSION_SET_SCRIPT);
302
113
        let connection = Self {
303
113
            connect_func,
304
113
            update_if_version_matches_script,
305
113
            connection_manager: tokio::sync::RwLock::new((connection_manager, Uuid::new_v4())),
306
113
            reconnect_lock: tokio::sync::Mutex::new(()),
307
113
            subscriptions: Mutex::new(HashSet::new()),
308
113
        };
309
        {
310
113
            let mut connection_manager = connection.connection_manager.write().await;
311
113
            connection.configure(&mut connection_manager.0).await
?0
;
312
        }
313
113
        Ok(connection)
314
116
    }
315
}
316
317
impl RedisManager<ConnectionManager> for StandardRedisManager<ConnectionManager> {
318
691k
    async fn get_connection(&self) -> Result<(ConnectionManager, Uuid), Error> {
319
691k
        Ok(self.connection_manager.read().await.clone())
320
691k
    }
321
322
249
    async fn reconnect(&self, uuid: Uuid) -> Result<(ConnectionManager, Uuid), Error> {
323
        // Fast path: another caller already reconnected past this generation,
324
        // so the handle is fresh — hand it back without re-resolving.
325
        {
326
249
            let guard = self.connection_manager.read().await;
327
249
            if guard.1 != uuid {
328
0
                return Ok(guard.clone());
329
249
            }
330
        }
331
        // Serialize reconnect attempts on a dedicated lock so a failover storm
332
        // resolves the new master once. Critically this is NOT the
333
        // `connection_manager` lock, so the slow connect below does not block
334
        // the `get_connection` readers that every Redis op needs — they keep
335
        // using the old (now-failing) handle and fail fast on their own
336
        // command timeout instead of all freezing behind one reconnect.
337
249
        let _reconnect_guard = self.reconnect_lock.lock().await;
338
        // Re-check: a reconnect may have completed while we waited for the lock.
339
        {
340
249
            let guard = self.connection_manager.read().await;
341
249
            if guard.1 != uuid {
342
2
                return Ok(guard.clone());
343
247
            }
344
        }
345
247
        record_connection_reconnect("redis");
346
247
        let 
mut connection_manager18
= (self.connect_func)().await
?229
;
347
18
        let new_uuid = Uuid::new_v4();
348
18
        self.configure(&mut connection_manager).await
?0
;
349
18
        let subscriptions = {
350
18
            let guard = self.subscriptions.lock();
351
18
            guard.iter().map(Clone::clone).collect::<Vec<_>>()
352
        };
353
18
        for 
subscription15
in &subscriptions {
354
15
            connection_manager.psubscribe(subscription).await
?0
;
355
        }
356
18
        let mut replayed = subscriptions.into_iter().collect::<HashSet<_>>();
357
        // Publish the new handle under a brief exclusive lock. Before
358
        // publishing, replay any patterns registered since the snapshot above:
359
        // `psubscribe` records its pattern in `self.subscriptions` *before* it
360
        // fetches a connection, so a pattern missing from this final delta was
361
        // inserted while we hold the write lock, and its `get_connection` will
362
        // block here and observe the new handle — it subscribes on the live
363
        // connection itself. Either way there is no window where a pattern is
364
        // recorded as active but subscribed only on the old connection.
365
        {
366
18
            let mut guard = self.connection_manager.write().await;
367
18
            let missing = {
368
18
                let subs = self.subscriptions.lock();
369
18
                subs.iter()
370
18
                    .filter(|s| !
replayed16
.
contains16
(
*s16
))
371
18
                    .cloned()
372
18
                    .collect::<Vec<_>>()
373
            };
374
18
            for 
subscription1
in missing {
375
1
                connection_manager.psubscribe(&subscription).await
?0
;
376
1
                replayed.insert(subscription);
377
            }
378
18
            *guard = (connection_manager.clone(), new_uuid);
379
        }
380
18
        info!(old = %uuid, new = %new_uuid, "StandardRedisManager re-resolved the Redis master");
381
18
        Ok((connection_manager, new_uuid))
382
249
    }
383
384
7.91k
    fn update_script(&self, key: &str) -> redis::ScriptInvocation<'_> {
385
7.91k
        self.update_if_version_matches_script.key(key)
386
7.91k
    }
387
388
98
    async fn psubscribe(&self, pattern: &str) -> Result<(), Error> {
389
98
        debug!(pattern, "new psubscribe");
390
        // Record the pattern BEFORE fetching a connection. A concurrent
391
        // `reconnect` either sees the pattern in the snapshot it replays under
392
        // its pre-publish write lock (subscribing it on the new connection),
393
        // or it published first — in which case `get_connection` below returns
394
        // the new live connection and we subscribe on it ourselves. Inserting
395
        // after fetching the connection (the previous order) left a window
396
        // where the pattern was recorded as active but PSUBSCRIBEd only on the
397
        // old, about-to-be-replaced connection.
398
98
        let new_subscription = self.subscriptions.lock().insert(String::from(pattern));
399
98
        if new_subscription {
400
98
            let result = async {
401
98
                let mut connection = self.get_connection().await
?0
.0;
402
98
                connection.psubscribe(pattern).await.map_err(Error::from)
403
98
            }
404
98
            .await;
405
98
            if result.is_err() {
406
0
                self.subscriptions.lock().remove(pattern);
407
98
            }
408
98
            result
?0
;
409
0
        }
410
98
        debug!(pattern, new_subscription, "new psubscribe complete");
411
98
        Ok(())
412
98
    }
413
}
414
415
/// A [`StoreDriver`] implementation that uses Redis as a backing store.
416
#[derive(MetricsComponent)]
417
pub struct RedisStore<C, M>
418
where
419
    C: ConnectionLike + Clone,
420
    M: RedisManager<C>,
421
{
422
    /// The client pool connecting to the backing Redis instance(s).
423
    connection_manager: M,
424
425
    /// The underlying connection type in the connection manager.
426
    _connection_type: PhantomData<C>,
427
428
    /// A channel to publish updates to when a key is added, removed, or modified.
429
    #[metric(
430
        help = "The pubsub channel to publish updates to when a key is added, removed, or modified"
431
    )]
432
    pub_sub_channel: Option<String>,
433
434
    /// A function used to generate names for temporary keys.
435
    temp_name_generator_fn: fn() -> String,
436
437
    /// A common prefix to append to all keys before they are sent to Redis.
438
    ///
439
    /// See [`RedisStore::key_prefix`](`nativelink_config::stores::RedisStore::key_prefix`).
440
    #[metric(help = "Prefix to append to all keys before sending to Redis")]
441
    key_prefix: String,
442
443
    /// The amount of data to read from Redis at a time.
444
    #[metric(help = "The amount of data to read from Redis at a time")]
445
    read_chunk_size: usize,
446
447
    /// The maximum number of chunk uploads per update.
448
    /// This is used to limit the number of chunk uploads per update to prevent
449
    /// overloading when uploading large blocks of data
450
    #[metric(help = "The maximum number of chunk uploads per update")]
451
    max_chunk_uploads_per_update: usize,
452
453
    /// The COUNT value passed when scanning keys in Redis.
454
    /// This is used to hint the amount of work that should be done per response.
455
    #[metric(help = "The COUNT value passed when scanning keys in Redis")]
456
    scan_count: usize,
457
458
    /// The COUNT value used with search indexes
459
    #[metric(help = "The maximum number of results to return per cursor")]
460
    max_count_per_cursor: u64,
461
462
    /// See [`RedisSpec::migrate_temporary_indexes`].
463
    migrate_temporary_indexes: bool,
464
465
    /// For each scheduler index, the name reads use and until when that
466
    /// answer holds. A settled answer is rechecked after
467
    /// `INDEX_MIGRATION_RECHECK`, so an original that a process without the
468
    /// migration recreates is retired again.
469
    index_names: Mutex<HashMap<String, (String, Instant)>>,
470
471
    /// Lets one read at a time run [`Self::migration_step`]. Concurrent
472
    /// reads whose cached name has expired would otherwise each repeat the
473
    /// `FT.DEBUG`/`FT.INFO` checks — and, while a replacement backfills with
474
    /// no original left, each sit in its own `FT.INFO` poll — against a
475
    /// Redis that is already memory-stressed. Waiters re-read the cache the
476
    /// winner refreshed instead.
477
    index_migration_gate: AsyncMutex<()>,
478
479
    /// A manager for subscriptions to keys in Redis.
480
    subscription_manager: Arc<RedisSubscriptionManager>,
481
482
    /// Permits to limit inflight Redis requests. Technically only
483
    /// limits the calls to `get_client()`, but the requests per client
484
    /// are small enough that it works well enough.
485
    client_permits: Arc<Semaphore>,
486
487
    /// Per-call ceiling for `check_health` PING.
488
    health_check_timeout: Duration,
489
490
    /// Expire keys this store writes after this long. `None` keeps them
491
    /// forever, which is the default and what every store other than a BEP
492
    /// store wants.
493
    key_ttl: Option<Duration>,
494
495
    /// Have we done a subscribe for messages for `remove_callback` subscribes?
496
    has_remove_callback_subscribe: OnceCell<()>,
497
498
    remove_callbacks: Arc<async_lock::Mutex<Vec<RemoveCallback>>>,
499
}
500
501
impl<C, M> Debug for RedisStore<C, M>
502
where
503
    C: ConnectionLike + Clone,
504
    M: RedisManager<C>,
505
{
506
0
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
507
0
        f.debug_struct("RedisStore")
508
0
            .field("temp_name_generator_fn", &self.temp_name_generator_fn)
509
0
            .field("key_prefix", &self.key_prefix)
510
0
            .field("read_chunk_size", &self.read_chunk_size)
511
0
            .field(
512
0
                "max_chunk_uploads_per_update",
513
0
                &self.max_chunk_uploads_per_update,
514
0
            )
515
0
            .field("scan_count", &self.scan_count)
516
0
            .field("subscription_manager", &self.subscription_manager)
517
0
            .field("client_permits", &self.client_permits)
518
0
            .finish()
519
0
    }
520
}
521
522
struct ClientWithPermit<C: ConnectionLike> {
523
    connection_manager: C,
524
    uuid: Uuid,
525
526
    // here so it sticks around with the client and doesn't get dropped until that does
527
    #[allow(dead_code)]
528
    semaphore_permit: OwnedSemaphorePermit,
529
}
530
531
impl<C: ConnectionLike + Clone> ClientWithPermit<C> {
532
241
    async fn reconnect<M: RedisManager<C> + Sync>(&mut self, manager: &M) -> Result<(), Error> {
533
241
        (
self.connection_manager17
,
self.uuid17
) = manager.reconnect(self.uuid).await
?224
;
534
17
        Ok(())
535
241
    }
536
}
537
538
impl<C: ConnectionLike> Drop for ClientWithPermit<C> {
539
674k
    fn drop(&mut self) {
540
674k
        trace!(
541
2.64k
            remaining = self.semaphore_permit.semaphore().available_permits(),
542
            "Dropping a client permit"
543
        );
544
674k
    }
545
}
546
547
/// Decode a `StoreKey` coming from Redis
548
35
pub fn decode_key<'a>(
549
35
    key_prefix: &String,
550
35
    encoded_key: Cow<'a, str>,
551
35
) -> Result<StoreKey<'a>, Error> {
552
35
    let key_no_prefix = if key_prefix.is_empty() {
553
30
        encoded_key
554
    } else {
555
5
        match encoded_key.strip_prefix(key_prefix) {
556
5
            Some(r) => Cow::from(r.to_string()),
557
            None => {
558
0
                return Err(make_err!(
559
0
                    Code::InvalidArgument,
560
0
                    "Redis key ({}) is missing prefix ({})",
561
0
                    encoded_key,
562
0
                    key_prefix
563
0
                ));
564
            }
565
        }
566
    };
567
35
    let maybe_digest_info: Result<_, serde::de::value::Error> =
568
35
        DigestInfo::deserialize(key_no_prefix.clone().into_deserializer());
569
35
    if let Ok(
digest_info8
) = maybe_digest_info {
570
8
        Ok(StoreKey::Digest(digest_info))
571
    } else {
572
27
        Ok(StoreKey::Str(key_no_prefix))
573
    }
574
35
}
575
576
impl<C, M> RedisStore<C, M>
577
where
578
    C: ConnectionLike + Clone + Sync,
579
    M: RedisManager<C> + Sync,
580
{
581
    /// Used for testing when determinism is required.
582
    #[expect(clippy::too_many_arguments)]
583
162
    pub async fn new_from_builder_and_parts(
584
162
        pub_sub_channel: Option<String>,
585
162
        temp_name_generator_fn: fn() -> String,
586
162
        key_prefix: String,
587
162
        read_chunk_size: usize,
588
162
        max_chunk_uploads_per_update: usize,
589
162
        scan_count: usize,
590
162
        max_client_permits: usize,
591
162
        max_count_per_cursor: u64,
592
162
        health_check_timeout: Duration,
593
162
        key_ttl: Option<Duration>,
594
162
        subscriber_channel: UnboundedReceiver<PushInfo>,
595
162
        connection_manager: M,
596
162
    ) -> Result<Self, Error> {
597
162
        info!("Redis index fingerprint: {FINGERPRINT_CREATE_INDEX_HEX}");
598
162
        let remove_callbacks = Arc::new(async_lock::Mutex::new(Vec::new()));
599
162
        let subscription_manager = Arc::new(RedisSubscriptionManager::new(
600
162
            subscriber_channel,
601
162
            remove_callbacks.clone(),
602
162
            key_prefix.clone(),
603
        ));
604
162
        if let Some(
channel91
) = &pub_sub_channel {
605
91
            connection_manager.psubscribe(channel).await
?0
;
606
71
        }
607
608
162
        Ok(Self {
609
162
            connection_manager,
610
162
            _connection_type: PhantomData,
611
162
            pub_sub_channel,
612
162
            temp_name_generator_fn,
613
162
            key_prefix,
614
162
            read_chunk_size,
615
162
            max_chunk_uploads_per_update,
616
162
            scan_count,
617
162
            subscription_manager,
618
162
            client_permits: Arc::new(Semaphore::new(max_client_permits)),
619
162
            max_count_per_cursor,
620
162
            migrate_temporary_indexes: false,
621
162
            index_names: Mutex::new(HashMap::new()),
622
162
            index_migration_gate: AsyncMutex::new(()),
623
162
            health_check_timeout,
624
162
            key_ttl,
625
162
            has_remove_callback_subscribe: OnceCell::const_new(),
626
162
            remove_callbacks,
627
162
        })
628
162
    }
629
630
    /// Turns on [`RedisSpec::migrate_temporary_indexes`].
631
    #[must_use]
632
0
    pub fn with_temporary_index_migration(mut self) -> Self {
633
0
        info!("Redis temporary-index migration enabled");
634
0
        self.migrate_temporary_indexes = true;
635
0
        self
636
0
    }
637
638
    /// `STRLEN` of a key, retried on a transient error like the reads are.
639
2
    async fn strlen(
640
2
        &self,
641
2
        client: &mut ClientWithPermit<C>,
642
2
        encoded_key: &str,
643
2
    ) -> Result<u64, Error> {
644
2
        let mut attempt: u32 = 0;
645
        loop {
646
2
            attempt += 1;
647
2
            match redis::cmd("STRLEN")
648
2
                .arg(encoded_key)
649
2
                .query_async::<u64>(&mut client.connection_manager)
650
2
                .await
651
            {
652
2
                Ok(v) => return Ok(v),
653
0
                Err(err)
654
0
                    if attempt < MAX_REDIS_RETRY_ATTEMPTS && is_retryable_redis_error(&err) =>
655
                {
656
0
                    client.reconnect(&self.connection_manager).await?;
657
0
                    sleep(Duration::from_secs_f32(DEFAULT_RETRY_DELAY)).await;
658
                }
659
0
                Err(err) => {
660
0
                    return Err(Error::from(err).append("In RedisStore::get_part::strlen"));
661
                }
662
            }
663
        }
664
2
    }
665
666
689k
    async fn get_client(&self) -> Result<ClientWithPermit<C>, Error> {
667
689k
        let local_client_permits = self.client_permits.clone();
668
689k
        let remaining = local_client_permits.available_permits();
669
        // Zero here means every client is busy and this call is about to wait,
670
        // which is the saturation signal worth alerting on.
671
689k
        record_connection_acquired("redis", Some(remaining), remaining == 0);
672
689k
        let 
semaphore_permit674k
= local_client_permits.acquire_owned().await
?0
;
673
674k
        trace!(remaining, "Got a client permit");
674
674k
        let (connection_manager, uuid) = self.connection_manager.get_connection().await
?0
;
675
674k
        Ok(ClientWithPermit {
676
674k
            connection_manager,
677
674k
            uuid,
678
674k
            semaphore_permit,
679
674k
        })
680
674k
    }
681
682
    /// Encode a `StoreKey` so it can be sent to Redis.
683
689k
    pub fn encode_key<'a>(&self, key: &'a StoreKey<'a>) -> Cow<'a, str> {
684
689k
        let key_body = key.as_str();
685
689k
        if self.key_prefix.is_empty() {
686
689k
            key_body
687
        } else {
688
            // This is in the hot path for all redis operations, so we try to reuse the allocation
689
            // from `key.as_str()` if possible.
690
9
            match key_body {
691
8
                Cow::Owned(mut encoded_key) => {
692
8
                    encoded_key.insert_str(0, &self.key_prefix);
693
8
                    Cow::Owned(encoded_key)
694
                }
695
1
                Cow::Borrowed(body) => {
696
1
                    let mut encoded_key = String::with_capacity(self.key_prefix.len() + body.len());
697
1
                    encoded_key.push_str(&self.key_prefix);
698
1
                    encoded_key.push_str(body);
699
1
                    Cow::Owned(encoded_key)
700
                }
701
            }
702
        }
703
689k
    }
704
705
115
    fn set_spec_defaults(spec: &mut RedisSpec) -> Result<(), Error> {
706
115
        if spec.addresses.is_empty() {
707
0
            return Err(make_err!(
708
0
                Code::InvalidArgument,
709
0
                "No addresses were specified in redis store configuration."
710
0
            ));
711
115
        }
712
713
115
        if spec.broadcast_channel_capacity != 0 {
714
1
            warn!("broadcast_channel_capacity in Redis spec is deprecated and ignored");
715
114
        }
716
115
        if spec.response_timeout_s != 0 {
717
0
            warn!(
718
                "response_timeout_s in Redis spec is deprecated and ignored, use command_timeout_ms"
719
            );
720
115
        }
721
115
        if spec.connection_timeout_s != 0 {
722
0
            if spec.connection_timeout_ms != 0 {
723
0
                return Err(make_err!(
724
0
                    Code::InvalidArgument,
725
0
                    "Both connection_timeout_s and connection_timeout_ms were set, can only have one!"
726
0
                ));
727
0
            }
728
0
            warn!("connection_timeout_s in Redis spec is deprecated, use connection_timeout_ms");
729
0
            spec.connection_timeout_ms = spec.connection_timeout_s * 1000;
730
115
        }
731
115
        if spec.connection_timeout_ms == 0 {
732
110
            spec.connection_timeout_ms = DEFAULT_CONNECTION_TIMEOUT_MS;
733
110
        
}5
734
115
        if spec.command_timeout_ms == 0 {
735
109
            spec.command_timeout_ms = DEFAULT_COMMAND_TIMEOUT_MS;
736
109
        
}6
737
115
        if spec.health_check_timeout_ms == 0 {
738
115
            spec.health_check_timeout_ms = DEFAULT_HEALTH_CHECK_TIMEOUT_MS;
739
115
        
}0
740
115
        if spec.connection_pool_size == 0 {
741
115
            spec.connection_pool_size = DEFAULT_CONNECTION_POOL_SIZE;
742
115
        
}0
743
115
        if spec.read_chunk_size == 0 {
744
115
            spec.read_chunk_size = DEFAULT_READ_CHUNK_SIZE;
745
115
        
}0
746
115
        if spec.max_count_per_cursor == 0 {
747
115
            spec.max_count_per_cursor = DEFAULT_MAX_COUNT_PER_CURSOR;
748
115
        
}0
749
115
        if spec.max_chunk_uploads_per_update == 0 {
750
115
            spec.max_chunk_uploads_per_update = DEFAULT_MAX_CHUNK_UPLOADS_PER_UPDATE;
751
115
        
}0
752
115
        if spec.scan_count == 0 {
753
115
            spec.scan_count = DEFAULT_SCAN_COUNT;
754
115
        
}0
755
115
        if spec.max_client_permits == 0 {
756
115
            spec.max_client_permits = DEFAULT_CLIENT_PERMITS;
757
115
        
}0
758
115
        if spec.retry.delay == 0.0 {
759
115
            spec.retry.delay = DEFAULT_RETRY_DELAY;
760
115
        
}0
761
115
        if spec.retry.max_retries == 0 {
762
115
            spec.retry.max_retries = 1;
763
115
        
}0
764
115
        trace!(?spec, "redis spec is after setting defaults");
765
115
        Ok(())
766
115
    }
767
768
    // Only used by tests, because we need to make a real redis connection, then fix this to get fixed values
769
11
    pub fn replace_temp_name_generator(&mut self, replacement: fn() -> String) {
770
11
        self.temp_name_generator_fn = replacement;
771
11
    }
772
}
773
774
impl RedisStore<ClusterConnection, ClusterRedisManager<ClusterConnection>> {
775
0
    pub async fn new_cluster(mut spec: RedisSpec) -> Result<Arc<Self>, Error> {
776
0
        let migrate_temporary_indexes = spec.migrate_temporary_indexes;
777
0
        if spec.mode != RedisMode::Cluster {
778
0
            return Err(Error::new(
779
0
                Code::InvalidArgument,
780
0
                "new_cluster only works for Cluster mode".to_string(),
781
0
            ));
782
0
        }
783
0
        Self::set_spec_defaults(&mut spec)?;
784
785
0
        let parsed_addrs: Vec<_> = spec
786
0
            .addresses
787
0
            .iter_mut()
788
0
            .map(|addr| {
789
0
                addr.clone().into_connection_info().map(|connection_info| {
790
0
                    let redis_settings = connection_info
791
0
                        .redis_settings()
792
0
                        .clone()
793
                        // We need RESP3 here because the cluster mode doesn't support RESP2 pubsub
794
                        // See also https://docs.rs/redis/latest/redis/cluster_async/index.html#pubsub
795
0
                        .set_protocol(redis::ProtocolVersion::RESP3);
796
0
                    connection_info.set_redis_settings(redis_settings)
797
0
                })
798
0
            })
799
0
            .collect::<Result<Vec<_>, _>>()?;
800
801
0
        let connection_timeout = Duration::from_millis(spec.connection_timeout_ms);
802
0
        let command_timeout = Duration::from_millis(spec.command_timeout_ms);
803
0
        let (tx, subscriber_channel) = unbounded_channel();
804
805
0
        let builder = ClusterClient::builder(parsed_addrs)
806
0
            .connection_timeout(connection_timeout)
807
0
            .response_timeout(command_timeout)
808
0
            .push_sender(tx)
809
0
            .retries(u32::try_from(spec.retry.max_retries)?);
810
811
0
        let client = builder.build()?;
812
813
0
        Self::new_from_builder_and_parts(
814
0
            spec.experimental_pub_sub_channel,
815
0
            || Uuid::new_v4().to_string(),
816
0
            spec.key_prefix.clone(),
817
0
            spec.read_chunk_size,
818
0
            spec.max_chunk_uploads_per_update,
819
0
            spec.scan_count,
820
0
            spec.max_client_permits,
821
0
            spec.max_count_per_cursor,
822
0
            Duration::from_millis(spec.health_check_timeout_ms),
823
0
            (spec.key_ttl_s > 0).then(|| Duration::from_secs(spec.key_ttl_s)),
824
0
            subscriber_channel,
825
0
            ClusterRedisManager::new(client.get_async_connection().await?).await?,
826
        )
827
0
        .await
828
0
        .map(|store| {
829
0
            if migrate_temporary_indexes {
830
0
                store.with_temporary_index_migration()
831
            } else {
832
0
                store
833
            }
834
0
        })
835
0
        .map(Arc::new)
836
0
    }
837
}
838
839
impl RedisStore<ConnectionManager, StandardRedisManager<ConnectionManager>> {
840
361
    async fn connect(
841
361
        spec: RedisSpec,
842
361
        tx: UnboundedSender<PushInfo>,
843
361
    ) -> Result<ConnectionManager, Error> {
844
361
        let connection_timeout = Duration::from_millis(spec.connection_timeout_ms);
845
361
        let command_timeout = Duration::from_millis(spec.command_timeout_ms);
846
847
361
        let addr = &spec.addresses[0];
848
361
        let local_addr = addr.clone();
849
361
        let mut parsed_addr = local_addr
850
361
            .replace("redis+sentinel://", "redis://")
851
361
            .into_connection_info()
?0
;
852
853
361
        let redis_settings = parsed_addr
854
361
            .redis_settings()
855
361
            .clone()
856
            // We need RESP3 here because we want to do set_push_sender
857
361
            .set_protocol(redis::ProtocolVersion::RESP3);
858
361
        parsed_addr = parsed_addr.set_redis_settings(redis_settings);
859
361
        debug!(?parsed_addr, "Parsed redis addr");
860
861
361
        let 
client360
= timeout(
862
361
            connection_timeout,
863
361
            spawn!("connect", async move {
864
361
                match spec.mode {
865
352
                    RedisMode::Standard => Client::open(parsed_addr).map_err(Into::<Error>::into),
866
                    RedisMode::Cluster => {
867
0
                        return Err(Error::new(
868
0
                            Code::Internal,
869
0
                            "Use RedisStore::new_cluster for cluster connections".to_owned(),
870
0
                        ));
871
                    }
872
9
                    RedisMode::Sentinel => async {
873
9
                        let url_parsing = Url::parse(&local_addr)
?0
;
874
9
                        let master_name = url_parsing
875
9
                            .query_pairs()
876
9
                            .find(|(key, _)| 
key1
==
"sentinelServiceName"1
)
877
9
                            .map_or_else(|| 
"master"8
.
into8
(), |(_, value)|
value1
.
to_string1
());
878
879
9
                        let redis_connection_info = parsed_addr.redis_settings().clone();
880
9
                        let sentinel_connection_info = SentinelNodeConnectionInfo::default()
881
9
                            .set_redis_connection_info(redis_connection_info);
882
883
                        // We fish this out because sentinels don't support db, we need to set it
884
                        // on the client only. See also https://github.com/redis-rs/redis-rs/issues/1950
885
9
                        let original_db = parsed_addr.redis_settings().db();
886
9
                        if original_db != 0 {
887
1
                            // sentinel_connection_info has the actual DB set
888
1
                            let revised_settings = parsed_addr.redis_settings().clone().set_db(0);
889
1
                            parsed_addr = parsed_addr.set_redis_settings(revised_settings);
890
8
                        }
891
892
9
                        SentinelClient::build(
893
9
                            vec![parsed_addr],
894
9
                            master_name,
895
9
                            Some(sentinel_connection_info),
896
9
                            SentinelServerType::Master,
897
                        )
898
9
                        .map_err(Into::<Error>::into)
899
9
                    }
900
18
                    .
and_then9
(|mut s| async move
{9
Ok(
s9
.async_get_client().await) })
901
9
                    .await
?0
902
9
                    .map_err(Into::<Error>::into),
903
                }
904
                // Keep the underlying error's code rather than forcing
905
                // InvalidArgument. Not every connect failure is a bad URL: a
906
                // sentinel failover in progress reports the master as missing,
907
                // which is transient and retryable, and overriding it to a
908
                // permanent status made clients give up on a recoverable blip.
909
361
                .err_tip(|| 
format!1
("While connecting to redis with url: {local_addr}"))
910
361
            }),
911
        )
912
361
        .await
913
361
        .err_tip(|| 
format!0
("Timeout while connecting to redis with url: {addr}"))
?0
?0
?1
;
914
915
360
        let connection_manager_config = {
916
360
            ConnectionManagerConfig::new()
917
360
                .set_number_of_retries(spec.retry.max_retries)
918
360
                .set_connection_timeout(Some(connection_timeout))
919
360
                .set_response_timeout(Some(command_timeout))
920
360
                .set_push_sender(tx)
921
        };
922
923
129
        let mut connection_manager =
924
360
            ConnectionManager::new_with_config(client, connection_manager_config)
925
360
                .await
926
360
                .err_tip(|| 
format!231
("While connecting to redis with url: {addr}"))
?231
;
927
928
129
        if let Some(
pub_sub_channel105
) = spec.experimental_pub_sub_channel {
929
105
            connection_manager.psubscribe(pub_sub_channel).await
?0
;
930
24
        }
931
932
129
        Ok(connection_manager)
933
361
    }
934
935
    /// Create a new `RedisStore` from the given configuration.
936
115
    pub async fn new_standard(mut spec: RedisSpec) -> Result<Arc<Self>, Error> {
937
115
        Self::set_spec_defaults(&mut spec)
?0
;
938
939
115
        if spec.addresses.len() != 1 {
940
0
            return Err(make_err!(
941
0
                Code::Unimplemented,
942
0
                "Connecting directly to multiple redis nodes in a cluster is currently unsupported. Please specify a single URL to a single node, and nativelink will use cluster discover to find the other nodes."
943
0
            ));
944
115
        }
945
946
115
        let (tx, subscriber_channel) = unbounded_channel();
947
115
        let migrate_temporary_indexes = spec.migrate_temporary_indexes;
948
949
112
        Self::new_from_builder_and_parts(
950
115
            spec.experimental_pub_sub_channel.clone(),
951
1
            || Uuid::new_v4().to_string(),
952
115
            spec.key_prefix.clone(),
953
115
            spec.read_chunk_size,
954
115
            spec.max_chunk_uploads_per_update,
955
115
            spec.scan_count,
956
115
            spec.max_client_permits,
957
115
            spec.max_count_per_cursor,
958
115
            Duration::from_millis(spec.health_check_timeout_ms),
959
115
            (spec.key_ttl_s > 0).then(|| 
Duration::from_secs0
(
spec.key_ttl_s0
)),
960
115
            subscriber_channel,
961
361
            
StandardRedisManager::new115
(
Box::new115
(move || {
962
361
                Box::pin(Self::connect(spec.clone(), tx.clone()))
963
361
            }))
964
115
            .await
?3
,
965
        )
966
112
        .await
967
112
        .map(|store| {
968
112
            if migrate_temporary_indexes {
969
0
                store.with_temporary_index_migration()
970
            } else {
971
112
                store
972
            }
973
112
        })
974
112
        .map(Arc::new)
975
115
    }
976
}
977
978
/// Deletes the temp key of an upload that did not publish, however it
979
/// ended. `delete` is for an error the store sees. `Drop` is for the update
980
/// future being dropped mid-stream, which is how the `ByteStream` server ends
981
/// an upload whose client went away or closed the stream early: no error
982
/// reaches the store, so without this the key stayed until its TTL. A chunk
983
/// still in flight on this connection was queued before the DEL, so Redis
984
/// applies the DEL after it; a chunk retried on another connection has no
985
/// such order and can recreate the key after the DEL, which is why every
986
/// chunk write arms the lifetime again and deletes a key it finds recreated
987
/// (see `update_through_temp_key`). What this cannot delete, the lifetime
988
/// removes.
989
struct TempKeyGuard<C>
990
where
991
    C: ConnectionLike + Clone + Send + Sync + 'static,
992
{
993
    connection: C,
994
    temp_key: String,
995
    armed: bool,
996
}
997
998
impl<C> TempKeyGuard<C>
999
where
1000
    C: ConnectionLike + Clone + Send + Sync + 'static,
1001
{
1002
    /// The upload published; the key is gone under its final name.
1003
16
    fn disarm(mut self) {
1004
16
        self.armed = false;
1005
16
    }
1006
1007
0
    async fn delete(self) {
1008
0
        let connection = self.connection.clone();
1009
0
        self.delete_with(connection).await;
1010
0
    }
1011
1012
    /// `delete` on `connection` rather than the one the guard was given:
1013
    /// after a failover the upload ended on a newer one.
1014
11
    async fn delete_with(mut self, mut connection: C) {
1015
11
        self.armed = false;
1016
11
        if let Err(
err2
) = connection.del::<_, ()>(&self.temp_key).await {
1017
2
            debug!(
1018
                ?err,
1019
                temp_key = %self.temp_key,
1020
                "Could not delete the temp key of a failed upload; its TTL removes it"
1021
            );
1022
9
        }
1023
11
    }
1024
}
1025
1026
impl<C> Drop for TempKeyGuard<C>
1027
where
1028
    C: ConnectionLike + Clone + Send + Sync + 'static,
1029
{
1030
28
    fn drop(&mut self) {
1031
28
        if !self.armed {
1032
27
            return;
1033
1
        }
1034
        // Dropped mid-stream. Nothing can be awaited here, so the delete runs
1035
        // on its own task; without a runtime (shutdown) the TTL does it.
1036
1
        let Ok(handle) = tokio::runtime::Handle::try_current() else {
1037
0
            return;
1038
        };
1039
1
        let temp_key = core::mem::take(&mut self.temp_key);
1040
1
        let mut connection = self.connection.clone();
1041
1
        handle.spawn(async move {
1042
1
            if let Err(
err0
) = connection.del::<_, ()>(&temp_key).await {
1043
0
                debug!(
1044
                    ?err,
1045
                    temp_key,
1046
                    "Could not delete the temp key of a dropped upload; its TTL removes it"
1047
                );
1048
1
            }
1049
1
        });
1050
28
    }
1051
}
1052
1053
impl<C, M> RedisStore<C, M>
1054
where
1055
    C: ConnectionLike + Clone + Send + Sync + Unpin + 'static,
1056
    M: RedisManager<C> + Unpin + Send + Sync + 'static,
1057
{
1058
    /// Stages an upload under `temp_key` and publishes it as `final_key`.
1059
    ///
1060
    /// The temp key is created with a lifetime before any byte lands, and
1061
    /// every chunk arms it again, so an upload that stops anywhere (the
1062
    /// client went away, this process did) leaves a key Redis removes on its
1063
    /// own. Nothing else cleans temp keys up: on a store without `key_ttl_s`
1064
    /// they lived forever, and a CAS that failed or restarted under load left
1065
    /// them in numbers. The lifetime is `key_ttl_s` when set, else
1066
    /// [`TEMP_KEY_TTL`]; a key that expired or was lost in a failover and was
1067
    /// recreated by a later chunk is refused rather than published, see the
1068
    /// TTL checks on each chunk and after them. The caller's [`TempKeyGuard`]
1069
    /// deletes the key when this does not publish.
1070
28
    async fn update_through_temp_key(
1071
28
        &self,
1072
28
        key: &StoreKey<'_>,
1073
28
        final_key: &str,
1074
28
        temp_key: &str,
1075
28
        reader: DropCloserReadHalf,
1076
28
        upload_size: UploadSizeInfo,
1077
28
    ) -> Result<u64, Error> {
1078
28
        let mut client = self.get_client().await
?0
;
1079
1080
28
        let temp_ttl_secs = self.key_ttl.unwrap_or(TEMP_KEY_TTL).as_secs().max(1);
1081
28
        let temp_ttl = ttl_seconds(self.key_ttl.unwrap_or(TEMP_KEY_TTL));
1082
28
        let staged = SetOptions::default().with_expiration(SetExpiry::EX(temp_ttl_secs));
1083
28
        match client
1084
28
            .connection_manager
1085
28
            .set_options::<_, _, ()>(temp_key, "", staged.clone())
1086
28
            .await
1087
        {
1088
25
            Ok(()) => {}
1089
3
            Err(
err2
) if is_retryable_redis_error(&err
)2
=> {
1090
2
                let (connection_manager, uuid) =
1091
2
                    self.connection_manager.reconnect(client.uuid).await
?0
;
1092
2
                client.connection_manager = connection_manager;
1093
2
                client.uuid = uuid;
1094
2
                client
1095
2
                    .connection_manager
1096
2
                    .set_options::<_, _, ()>(temp_key, "", staged)
1097
2
                    .await
1098
2
                    .err_tip(|| 
{1
1099
1
                        format!(
1100
                            "(after reconnect) while creating temp key ({temp_key}) in RedisStore::update"
1101
                        )
1102
1
                    })?;
1103
            }
1104
1
            Err(err) => {
1105
1
                return Err(Error::from(err).append(format!(
1106
1
                    "While creating temp key ({temp_key}) in RedisStore::update"
1107
1
                )));
1108
            }
1109
        }
1110
1111
26
        let mut read_stream = reader
1112
26
            .scan(0u32, |bytes_read, chunk_res| {
1113
26
                future::ready(Some(
1114
26
                    chunk_res
1115
26
                        .err_tip(|| "Failed to read chunk in update in redis store")
1116
26
                        .and_then(|chunk| {
1117
26
                            let offset = isize::try_from(*bytes_read).err_tip(|| "Could not convert offset to isize in RedisStore::update")
?0
;
1118
26
                            let chunk_len = u32::try_from(chunk.len()).err_tip(
1119
                                || "Could not convert chunk length to u32 in RedisStore::update",
1120
0
                            )?;
1121
26
                            let new_bytes_read = bytes_read
1122
26
                                .checked_add(chunk_len)
1123
26
                                .err_tip(|| "Overflow protection in RedisStore::update")
?0
;
1124
26
                            *bytes_read = new_bytes_read;
1125
26
                            Ok::<_, Error>((offset, *bytes_read, chunk))
1126
26
                        }),
1127
                ))
1128
26
            })
1129
26
            .map(|res| {
1130
26
                let (offset, end_pos, chunk) = res
?0
;
1131
26
                Ok(async move {
1132
26
                    let (mut connection_manager, connect_id) = self.connection_manager.get_connection().await
?0
;
1133
                    // The chunk, the key's TTL as it stood before the chunk,
1134
                    // and the lifetime armed again, in one round trip. A key
1135
                    // that expired or was lost in a failover is recreated by
1136
                    // the SETRANGE, zero-filled and with no TTL, and only the
1137
                    // TTL tells. The EXPIRE makes the lifetime a bound on the
1138
                    // gap between chunks rather than on the upload, and gives
1139
                    // even a recreated key a lifetime, so a chunk that lands
1140
                    // after the upload's DEL (one retried on another
1141
                    // connection, say) leaves nothing behind for good.
1142
26
                    let mut write = pipe();
1143
26
                    write
1144
26
                        .setrange(temp_key, offset, chunk.to_vec())
1145
26
                        .ttl(temp_key)
1146
26
                        .expire(temp_key, temp_ttl);
1147
26
                    let (_, 
ttl_before25
, _):
(usize, i64, i64)25
= match write
1148
26
                        .query_async(&mut connection_manager)
1149
26
                        .await
1150
                    {
1151
24
                        Ok(written) => written,
1152
2
                        Err(
err1
) if is_retryable_redis_error(&err
)1
=> {
1153
1
                            let (reconnected, _connect_id) =
1154
1
                                self.connection_manager.reconnect(connect_id).await
?0
;
1155
1
                            connection_manager = reconnected;
1156
1
                            write
1157
1
                                .query_async(&mut connection_manager)
1158
1
                                .await
1159
1
                                .err_tip(|| 
{0
1160
0
                                    format!("(after reconnect) while appending to temp key ({temp_key}) in RedisStore::update. offset = {offset}. end_pos = {end_pos}")
1161
0
                                })?
1162
                        }
1163
1
                        Err(err) => {
1164
1
                            let mut error: Error = err.into();
1165
1
                            error
1166
1
                                .messages
1167
1
                                .push(format!("While appending to temp key ({temp_key}) in RedisStore::update. offset = {offset}. end_pos = {end_pos}"));
1168
1
                            return Err(error);
1169
                        }
1170
                    };
1171
25
                    if ttl_before < 0 {
1172
                        // Recreated by this chunk: gone now rather than at
1173
                        // the end of the lifetime the EXPIRE just gave it.
1174
1
                        if let Err(
err0
) = connection_manager.del::<_, ()>(temp_key).await {
1175
0
                            debug!(
1176
                                ?err,
1177
                                temp_key,
1178
                                "Could not delete a recreated temp key; its TTL removes it"
1179
                            );
1180
1
                        }
1181
1
                        return Err(make_err!(
1182
1
                            Code::Aborted,
1183
1
                            "Chunk at offset {offset} of {}({temp_key}) found the temp key gone (expired after {temp_ttl}s without a chunk, or lost in a failover) and recreated it; not published, send the upload again",
1184
1
                            key.as_str(),
1185
1
                        ));
1186
24
                    }
1187
24
                    Ok::<u32, Error>(end_pos)
1188
26
                })
1189
26
            })
1190
26
            .try_buffer_unordered(self.max_chunk_uploads_per_update);
1191
1192
26
        let mut total_len: u32 = 0;
1193
50
        while let Some(
last_pos24
) = read_stream.try_next().await
?2
{
1194
24
            if last_pos > total_len {
1195
24
                total_len = last_pos;
1196
24
            
}0
1197
        }
1198
1199
23
        let expected_len = usize::try_from(total_len).unwrap_or(usize::MAX);
1200
23
        upload_size.validate_size(u64::from(total_len))
?3
;
1201
1202
        // The chunk writes above reconnect on any transient failover error, so on a mid-write Redis
1203
        // failover the data lands on the *current* master. The length check and
1204
        // rename below must run against that same master: the connection
1205
        // captured before the writes may now point at a demoted replica, where
1206
        // strlen reads 0 and would fail an otherwise healthy write. Re-resolve
1207
        // the master and retry so a transient topology change (failover or brief
1208
        // replica lag) doesn't drop the value.
1209
20
        let mut attempt: u32 = 0;
1210
18
        let blob_len = loop {
1211
26
            attempt += 1;
1212
            // The master can go away between the chunk writes and this check
1213
            // too, and the connection then waits out the command timeout on a
1214
            // dead peer. Re-resolve the master and ask it, like every other
1215
            // step here does, instead of failing the upload. The error can
1216
            // also be immediate (connection refused while the old master is
1217
            // gone and Sentinel has not promoted a replica yet), so back off
1218
            // before each retry, doubling, so the attempts together span a
1219
            // typical failover of a few seconds rather than a few milliseconds.
1220
            //
1221
            // The key's remaining TTL comes back in the same round trip.
1222
26
            let (
blob_len25
,
remaining_ttl25
):
(usize, i64)25
= match pipe()
1223
26
                .strlen(temp_key)
1224
26
                .ttl(temp_key)
1225
26
                .query_async(&mut client.connection_manager)
1226
26
                .await
1227
            {
1228
25
                Ok(checked) => checked,
1229
1
                Err(err)
1230
1
                    if is_retryable_redis_error(&err) && attempt < MAX_REDIS_RETRY_ATTEMPTS =>
1231
                {
1232
1
                    let (connection_manager, uuid) =
1233
1
                        self.connection_manager.reconnect(client.uuid).await
?0
;
1234
1
                    client.connection_manager = connection_manager;
1235
1
                    client.uuid = uuid;
1236
1
                    let backoff =
1237
1
                        DEFAULT_RETRY_DELAY * 2f32.powi(i32::try_from(attempt).unwrap_or(i32::MAX));
1238
1
                    sleep(Duration::from_secs_f32(backoff)).await;
1239
1
                    continue;
1240
                }
1241
0
                Err(err) => {
1242
0
                    return Err(Error::from(err)
1243
0
                        .append(format!("In RedisStore::update strlen check for {temp_key}")));
1244
                }
1245
            };
1246
            // Safety check: reject if a retried append double-wrote the data.
1247
25
            if blob_len > expected_len {
1248
0
                return Err(make_input_err!(
1249
0
                    "Data length mismatch in RedisStore::update for {}({}) - expected {} bytes, got {} bytes",
1250
0
                    key.as_str(),
1251
0
                    temp_key,
1252
0
                    total_len,
1253
0
                    blob_len,
1254
0
                ));
1255
25
            }
1256
25
            if blob_len == expected_len {
1257
                // SETRANGE on a key that has expired or was lost creates it
1258
                // again, zero-filled up to the offset and with no TTL, and
1259
                // the length still matches. Each chunk checks this on its
1260
                // own write; this is the check for whatever happened between
1261
                // the last chunk's EXPIRE and here. Publishing would store
1262
                // zeros under a digest they do not hash to.
1263
19
                if remaining_ttl < 0 {
1264
1
                    return Err(make_err!(
1265
1
                        Code::Aborted,
1266
1
                        "Upload of {}({}) lost its temp key (expired after {}s without a chunk, or lost in a failover) and a later chunk recreated it; not published, send it again",
1267
1
                        key.as_str(),
1268
1
                        temp_key,
1269
1
                        temp_ttl_secs,
1270
1
                    ));
1271
18
                }
1272
18
                break blob_len;
1273
6
            }
1274
            // blob_len < expected (typically 0): the temp key isn't visible on
1275
            // this connection yet; re-resolve the master and retry.
1276
6
            if attempt >= MAX_REDIS_RETRY_ATTEMPTS {
1277
1
                return Err(make_input_err!(
1278
1
                    "Data length mismatch in RedisStore::update for {}({}) - expected {} bytes, got {} bytes after {} attempts",
1279
1
                    key.as_str(),
1280
1
                    temp_key,
1281
1
                    total_len,
1282
1
                    blob_len,
1283
1
                    attempt,
1284
1
                ));
1285
5
            }
1286
5
            let (connection_manager, uuid) = self.connection_manager.reconnect(client.uuid).await
?0
;
1287
5
            client.connection_manager = connection_manager;
1288
5
            client.uuid = uuid;
1289
5
            sleep(Duration::from_secs_f32(DEFAULT_RETRY_DELAY)).await;
1290
        };
1291
1292
        // Rename the temp key so that the data appears under the real key. Any data already present in the real key is lost.
1293
        // The rename carries the temp key's TTL across, so the real key gets
1294
        // its own lifetime in the same transaction: `key_ttl_s` counted from
1295
        // completion rather than from the first byte, or none at all. One
1296
        // transaction, so nothing can be published with the temp key's
1297
        // lifetime still on it, and so a transaction that applied on the
1298
        // old master before its reply was lost in a failover left the final
1299
        // key complete, which the retry below recognizes.
1300
        // Reconnect once on a transient failover error in case the master moved between the verify and here.
1301
18
        let mut publish = pipe();
1302
18
        publish.atomic().rename(temp_key, final_key);
1303
18
        match self.key_ttl {
1304
2
            Some(key_ttl) => publish.expire(final_key, ttl_seconds(key_ttl)),
1305
16
            None => publish.persist(final_key),
1306
        };
1307
18
        let published: Option<((), bool)> =
1308
18
            match publish.query_async(&mut client.connection_manager).await {
1309
15
                Ok(published) => Some(published),
1310
3
                Err(err) if is_retryable_redis_error(&err) => {
1311
3
                    let (connection_manager, uuid) =
1312
3
                        self.connection_manager.reconnect(client.uuid).await
?0
;
1313
3
                    client.connection_manager = connection_manager;
1314
3
                    client.uuid = uuid;
1315
3
                    match publish.query_async(&mut client.connection_manager).await {
1316
1
                        Ok(published) => Some(published),
1317
2
                        Err(err) if is_no_such_key(&err) => None,
1318
0
                        Err(err) => {
1319
0
                            return Err(Error::from(err).append(format!(
1320
0
                            "While publishing {final_key} (after reconnect) in RedisStore::update()"
1321
0
                        )));
1322
                        }
1323
                    }
1324
                }
1325
0
                Err(err) => {
1326
0
                    return Err(Error::from(err).append(format!(
1327
0
                        "While publishing {final_key} in RedisStore::update()"
1328
0
                    )));
1329
                }
1330
            };
1331
18
        let 
kept17
= if let Some(((),
kept16
)) = published {
1332
16
            kept
1333
        } else {
1334
            // The temp key is gone on the master the retry reached.
1335
            // Either the first attempt applied and the final key is this
1336
            // upload, or the failover lost the temp key and the final key
1337
            // is whatever was there before. For a digest key the two are
1338
            // the same bytes; for a string key this accepts the value
1339
            // already there, as an upload that lost a race does.
1340
2
            let len: usize = client
1341
2
                    .connection_manager
1342
2
                    .strlen(final_key)
1343
2
                    .await
1344
2
                    .err_tip(|| 
{0
1345
0
                        format!(
1346
                            "While checking {final_key} after a publish whose reply was lost in RedisStore::update()"
1347
                        )
1348
0
                    })?;
1349
2
            if len != expected_len {
1350
1
                return Err(make_err!(
1351
1
                    Code::Aborted,
1352
1
                    "Upload of {}({}) lost its temp key in a failover and {} is not it ({} bytes, expected {}); send it again",
1353
1
                    key.as_str(),
1354
1
                    temp_key,
1355
1
                    final_key,
1356
1
                    len,
1357
1
                    expected_len,
1358
1
                ));
1359
1
            }
1360
1
            true
1361
        };
1362
        // EXPIRE and PERSIST answer 0 only when the key is gone, which inside
1363
        // the transaction means it expired as the RENAME ran. The client has
1364
        // to send it again.
1365
17
        if !kept {
1366
1
            return Err(make_err!(
1367
1
                Code::Aborted,
1368
1
                "Upload of {}({}) expired as it was published; not stored, send it again",
1369
1
                key.as_str(),
1370
1
                final_key,
1371
1
            ));
1372
16
        }
1373
1374
        // If we have a publish channel configured, send a notice that the key has been set.
1375
16
        if let Some(
pub_sub_channel0
) = &self.pub_sub_channel {
1376
0
            client
1377
0
                .connection_manager
1378
0
                .publish::<_, _, ()>(pub_sub_channel, final_key)
1379
0
                .await?;
1380
16
        }
1381
1382
16
        Ok(blob_len.try_into().unwrap_or(0))
1383
27
    }
1384
}
1385
1386
#[async_trait]
1387
impl<C, M> StoreDriver for RedisStore<C, M>
1388
where
1389
    C: ConnectionLike + Clone + Send + Sync + Unpin + 'static,
1390
    M: RedisManager<C> + Unpin + Send + Sync + 'static,
1391
{
1392
0
    async fn post_init(self: Arc<Self>) -> Result<(), Error> {
1393
        Ok(())
1394
0
    }
1395
1396
    async fn has_with_results(
1397
        self: Pin<&Self>,
1398
        keys: &[StoreKey<'_>],
1399
        results: &mut [Option<u64>],
1400
25
    ) -> Result<(), Error> {
1401
        // TODO(palfrey) We could use pipeline here, but it makes retry more
1402
        // difficult and it doesn't work very well in cluster mode.
1403
        // If we wanted to optimize this with pipeline be careful to
1404
        // implement retry and to support cluster mode.
1405
1406
        izip!(keys.iter(), results.iter_mut(),)
1407
25
            .map(|(key, result)| async move {
1408
                // We need to do a special pass to ensure our zero key exist.
1409
25
                if is_zero_digest(key.borrow()) {
1410
2
                    *result = Some(0);
1411
2
                    return Ok::<_, Error>(());
1412
23
                }
1413
23
                let encoded_key = self.encode_key(key);
1414
1415
23
                let mut client = self.get_client().await
?0
;
1416
1417
                // Redis returns 0 when the key doesn't exist
1418
                // AND when the key exists with value of length 0.
1419
                // Therefore, we need to check both length and existence
1420
                // and do it in a pipeline for efficiency
1421
                // Re-resolve the master and retry on a transient failover so a
1422
                // topology change doesn't fail the existence check.
1423
23
                let (blob_len, exists) = {
1424
23
                    let mut attempt: u32 = 0;
1425
                    loop {
1426
24
                        attempt += 1;
1427
24
                        match pipe()
1428
24
                            .strlen(encoded_key.as_ref())
1429
24
                            .exists(encoded_key.as_ref())
1430
24
                            .query_async::<(u64, bool)>(&mut client.connection_manager)
1431
24
                            .await
1432
                        {
1433
23
                            Ok(v) => break v,
1434
1
                            Err(err)
1435
1
                                if attempt < MAX_REDIS_RETRY_ATTEMPTS
1436
1
                                    && is_retryable_redis_error(&err) =>
1437
                            {
1438
1
                                client.reconnect(&self.connection_manager).await
?0
;
1439
1
                                sleep(Duration::from_secs_f32(DEFAULT_RETRY_DELAY)).await;
1440
                            }
1441
0
                            Err(err) => {
1442
0
                                return Err(
1443
0
                                    Error::from(err).append("In RedisStore::has_with_results::all")
1444
0
                                );
1445
                            }
1446
                        }
1447
                    }
1448
                };
1449
1450
                // An empty value cannot be a blob whose digest names bytes: a
1451
                // failed upload left it. Reported present it is read as
1452
                // DataLoss forever and never uploaded again.
1453
23
                *result = match (exists, blob_len) {
1454
6
                    (false, _) => None,
1455
1
                    (true, 0) if matches!(key.borrow(), StoreKey::Digest(digest) if digest.size_bytes() > 0) => {
1456
1
                        warn!(
1457
                            ?key,
1458
                            "Redis holds an empty value for a digest that names bytes; reporting it missing"
1459
                        );
1460
1
                        None
1461
                    }
1462
16
                    (true, len) => Some(len),
1463
                };
1464
1465
23
                Ok::<_, Error>(())
1466
50
            })
1467
            .collect::<FuturesUnordered<_>>()
1468
            .try_collect()
1469
            .await
1470
25
    }
1471
1472
    async fn list(
1473
        self: Pin<&Self>,
1474
        range: (Bound<StoreKey<'_>>, Bound<StoreKey<'_>>),
1475
        handler: &mut (dyn for<'a> FnMut(&'a StoreKey) -> bool + Send + Sync + '_),
1476
8
    ) -> Result<u64, Error> {
1477
        let range = (
1478
            range.0.map(StoreKey::into_owned),
1479
            range.1.map(StoreKey::into_owned),
1480
        );
1481
        let pattern = match range.0 {
1482
            Bound::Included(ref start) | Bound::Excluded(ref start) => match range.1 {
1483
                Bound::Included(ref end) | Bound::Excluded(ref end) => {
1484
                    let start = start.as_str();
1485
                    let end = end.as_str();
1486
                    let max_length = start.len().min(end.len());
1487
                    let length = start
1488
                        .chars()
1489
                        .zip(end.chars())
1490
20
                        .position(|(a, b)| a != b)
1491
                        .unwrap_or(max_length);
1492
                    format!("{}{}*", self.key_prefix, &start[..length])
1493
                }
1494
                Bound::Unbounded => format!("{}*", self.key_prefix),
1495
            },
1496
            Bound::Unbounded => format!("{}*", self.key_prefix),
1497
        };
1498
        let mut client = self.get_client().await?;
1499
        trace!(%pattern, count=self.scan_count, "Running SCAN");
1500
        // Restart the scan on a transient failover. Redis SCAN may re-emit keys
1501
        // even without failures, so callers already tolerate duplicates;
1502
        // re-resolving the master and rescanning is within that contract and
1503
        // avoids failing on a topology change mid-iteration.
1504
        let mut attempt: u32 = 0;
1505
        loop {
1506
            attempt += 1;
1507
            let opts = ScanOptions::default()
1508
                .with_pattern(pattern.clone())
1509
                .with_count(self.scan_count);
1510
            // Scan via a cloned connection handle so the iterator doesn't hold a
1511
            // borrow on `client` across a reconnect.
1512
            let mut conn = client.connection_manager.clone();
1513
            let mut scan_stream: AsyncIter<Value> = match conn.scan_options(opts).await {
1514
                Ok(s) => s,
1515
                Err(err)
1516
                    if attempt < MAX_REDIS_RETRY_ATTEMPTS && is_retryable_redis_error(&err) =>
1517
                {
1518
                    client.reconnect(&self.connection_manager).await?;
1519
                    sleep(Duration::from_secs_f32(DEFAULT_RETRY_DELAY)).await;
1520
                    continue;
1521
                }
1522
                Err(err) => return Err(Error::from(err).append("During scan_options")),
1523
            };
1524
            let mut iterations = 0;
1525
            let mut errors = vec![];
1526
            let mut transient_err = false;
1527
            while let Some(key) = scan_stream.next_item().await {
1528
                match key {
1529
                    Ok(Value::BulkString(raw_key)) => {
1530
                        let Ok(str_key) = str::from_utf8(&raw_key) else {
1531
                            error!(?raw_key, "Non-utf8 key");
1532
                            errors.push(format!("Non-utf8 key {raw_key:?}"));
1533
                            continue;
1534
                        };
1535
                        match decode_key(&self.key_prefix, Cow::from(str_key)) {
1536
                            Ok(key) => {
1537
                                if range.contains(&key) {
1538
                                    iterations += 1;
1539
                                    if !handler(&key) {
1540
                                        error!("Issue in handler");
1541
                                        errors.push("Issue in handler".to_string());
1542
                                    }
1543
                                } else {
1544
                                    trace!(%key, ?range, "Key not in range");
1545
                                }
1546
                            }
1547
                            Err(e) => {
1548
                                errors.push(e.to_string());
1549
                            }
1550
                        }
1551
                    }
1552
                    Err(err)
1553
                        if attempt < MAX_REDIS_RETRY_ATTEMPTS && is_retryable_redis_error(&err) =>
1554
                    {
1555
                        // Connection dropped mid-scan; restart from scratch.
1556
                        transient_err = true;
1557
                        break;
1558
                    }
1559
                    other => {
1560
                        error!(?other, "Non-string in key");
1561
                        errors.push("Non-string in key".to_string());
1562
                    }
1563
                }
1564
            }
1565
            if transient_err {
1566
                drop(scan_stream);
1567
                client.reconnect(&self.connection_manager).await?;
1568
                sleep(Duration::from_secs_f32(DEFAULT_RETRY_DELAY)).await;
1569
                continue;
1570
            }
1571
            return if errors.is_empty() {
1572
                Ok(iterations)
1573
            } else {
1574
                error!(?errors, "Errors in scan stream");
1575
                Err(Error::new(Code::Internal, format!("Errors: {errors:?}")))
1576
            };
1577
        }
1578
8
    }
1579
1580
    async fn update(
1581
        self: Pin<&Self>,
1582
        key: StoreKey<'_>,
1583
        mut reader: DropCloserReadHalf,
1584
        upload_size: UploadSizeInfo,
1585
30
    ) -> Result<u64, Error> {
1586
        let final_key = self.encode_key(&key);
1587
1588
        // While the name generation function can be supplied by the user, we need to have the curly
1589
        // braces in place in order to manage redis' hashing behavior and make sure that the temporary
1590
        // key name and the final key name are directed to the same cluster node. See
1591
        // https://redis.io/blog/redis-clustering-best-practices-with-keys/
1592
        //
1593
        // The TL;DR is that if we're in cluster mode and the names hash differently, we can't use request
1594
        // pipelining. By using these braces, we tell redis to only hash the part of the temporary key that's
1595
        // identical to the final key -- so they will always hash to the same node.
1596
        let temp_key = format!("temp-{}-{{{final_key}}}", (self.temp_name_generator_fn)());
1597
1598
        if is_zero_digest(key.borrow()) {
1599
            let chunk = reader
1600
                .peek()
1601
                .await
1602
                .err_tip(|| "Failed to peek in RedisStore::update")?;
1603
            if chunk.is_empty() {
1604
                reader
1605
                    .drain()
1606
                    .await
1607
                    .err_tip(|| "Failed to drain in RedisStore::update")?;
1608
                // Zero-digest keys are special -- we don't need to do anything with it.
1609
                return Ok(0);
1610
            }
1611
        }
1612
1613
        // The temp key expires on its own. Deleting it as soon as the upload
1614
        // cannot publish frees the memory now rather than at the end of its
1615
        // lifetime, which is the difference between a bounded and a full
1616
        // Redis when uploads fail or are abandoned in numbers.
1617
        let (connection, _uuid) = self.connection_manager.get_connection().await?;
1618
        let guard = TempKeyGuard {
1619
            connection,
1620
            temp_key: temp_key.clone(),
1621
            armed: true,
1622
        };
1623
        match self
1624
            .update_through_temp_key(&key, &final_key, &temp_key, reader, upload_size)
1625
            .await
1626
        {
1627
            Ok(len) => {
1628
                guard.disarm();
1629
                Ok(len)
1630
            }
1631
            Err(err) => {
1632
                // On the connection the upload ended on, which after a
1633
                // failover is a newer one than the guard was given.
1634
                match self.connection_manager.get_connection().await {
1635
                    Ok((connection, _uuid)) => guard.delete_with(connection).await,
1636
                    Err(_) => guard.delete().await,
1637
                }
1638
                Err(err)
1639
            }
1640
        }
1641
30
    }
1642
1643
    async fn get_part(
1644
        self: Pin<&Self>,
1645
        key: StoreKey<'_>,
1646
        writer: &mut DropCloserWriteHalf,
1647
        offset: u64,
1648
        length: Option<u64>,
1649
16
    ) -> Result<(), Error> {
1650
        let offset = isize::try_from(offset).err_tip(|| "Could not convert offset to isize")?;
1651
        let length = length
1652
4
            .map(|v| usize::try_from(v).err_tip(|| "Could not convert length to usize"))
1653
            .transpose()?;
1654
1655
        // To follow RBE spec we need to consider any digest's with
1656
        // zero size to be existing.
1657
        if is_zero_digest(key.borrow()) {
1658
            return writer
1659
                .send_eof()
1660
                .err_tip(|| "Failed to send zero EOF in redis store get_part");
1661
        }
1662
1663
        let encoded_key = self.encode_key(&key);
1664
        let encoded_key = encoded_key.as_ref();
1665
1666
        // N.B. the `-1`'s you see here are because redis GETRANGE is inclusive at both the start and end, so when we
1667
        // do math with indices we change them to be exclusive at the end.
1668
1669
        // We want to read the data at the key from `offset` to `offset + length`.
1670
        let data_start = offset;
1671
        let data_end = data_start
1672
4
            .saturating_add(length.map_or(isize::MAX, |l| isize::try_from(l).unwrap_or(isize::MAX)))
1673
            .saturating_sub(1);
1674
1675
        // And we don't ever want to read more than `read_chunk_size` bytes at a time, so we'll need to iterate.
1676
        let mut chunk_start = data_start;
1677
        let mut chunk_end = cmp::min(
1678
            data_start.saturating_add(self.read_chunk_size.try_into().unwrap_or(isize::MAX)) - 1,
1679
            data_end,
1680
        );
1681
1682
        let mut client = self.get_client().await?;
1683
        let mut bytes_read: u64 = 0;
1684
        loop {
1685
            // getrange is position-based and idempotent, so re-resolve the
1686
            // master and retry on a transient failover without re-sending
1687
            // already-written chunks.
1688
            let chunk: Bytes = {
1689
                let mut attempt: u32 = 0;
1690
                loop {
1691
                    attempt += 1;
1692
                    match client
1693
                        .connection_manager
1694
                        .getrange(encoded_key, chunk_start, chunk_end)
1695
                        .await
1696
                    {
1697
                        Ok(v) => break v,
1698
                        Err(err)
1699
                            if attempt < MAX_REDIS_RETRY_ATTEMPTS
1700
                                && is_retryable_redis_error(&err) =>
1701
                        {
1702
                            client.reconnect(&self.connection_manager).await?;
1703
                            sleep(Duration::from_secs_f32(DEFAULT_RETRY_DELAY)).await;
1704
                        }
1705
                        Err(err) => {
1706
                            return Err(
1707
                                Error::from(err).append("In RedisStore::get_part::getrange")
1708
                            );
1709
                        }
1710
                    }
1711
                }
1712
            };
1713
1714
            let didnt_receive_full_chunk = chunk.len() < self.read_chunk_size;
1715
            let reached_end_of_data = chunk_end == data_end;
1716
1717
            if didnt_receive_full_chunk || reached_end_of_data {
1718
                if !chunk.is_empty() {
1719
                    writer
1720
                        .send(chunk)
1721
                        .await
1722
                        .err_tip(|| "Failed to write data in RedisStore::get_part")?;
1723
                } else if bytes_read > 0 {
1724
                    // GETRANGE answers the same for a value that ends on a
1725
                    // chunk boundary and for a key removed between two
1726
                    // chunks (eviction under allkeys-lru). Handing the second
1727
                    // on as EOF is a truncated blob that every store above
1728
                    // caches as complete, so ask how long the value is now
1729
                    // and refuse anything but the end we stopped at.
1730
                    let value_len = self.strlen(&mut client, encoded_key).await?;
1731
                    let read_end = u64::try_from(data_start)
1732
                        .unwrap_or(0)
1733
                        .saturating_add(bytes_read);
1734
                    if value_len != read_end {
1735
                        return Err(make_err!(
1736
                            Code::NotFound,
1737
                            "Data for {key:?} changed under the read in the Redis store: read {bytes_read} bytes from offset {data_start}, the key now holds {value_len}"
1738
                        ));
1739
                    }
1740
                }
1741
1742
                break; // No more data to read.
1743
            }
1744
1745
            // We received a full chunk's worth of data, so write it...
1746
            bytes_read += chunk.len() as u64;
1747
            writer
1748
                .send(chunk)
1749
                .await
1750
                .err_tip(|| "Failed to write data in RedisStore::get_part")?;
1751
1752
            // ...and go grab the next chunk.
1753
            chunk_start = chunk_end + 1;
1754
            chunk_end = cmp::min(
1755
                chunk_start.saturating_add(self.read_chunk_size.try_into().unwrap_or(isize::MAX))
1756
                    - 1,
1757
                data_end,
1758
            );
1759
        }
1760
1761
        // If we didn't write any data, check if the key exists, if not return a NotFound error.
1762
        // This is required by spec.
1763
        if writer.get_bytes_written() == 0 {
1764
            // We're supposed to read 0 bytes, so just check if the key exists.
1765
            let exists: bool = {
1766
                let mut attempt: u32 = 0;
1767
                loop {
1768
                    attempt += 1;
1769
                    match client.connection_manager.exists(encoded_key).await {
1770
                        Ok(v) => break v,
1771
                        Err(err)
1772
                            if attempt < MAX_REDIS_RETRY_ATTEMPTS
1773
                                && is_retryable_redis_error(&err) =>
1774
                        {
1775
                            client.reconnect(&self.connection_manager).await?;
1776
                            sleep(Duration::from_secs_f32(DEFAULT_RETRY_DELAY)).await;
1777
                        }
1778
                        Err(err) => {
1779
                            return Err(
1780
                                Error::from(err).append("In RedisStore::get_part::zero_exists")
1781
                            );
1782
                        }
1783
                    }
1784
                }
1785
            };
1786
1787
            if !exists {
1788
                return Err(make_err!(
1789
                    Code::NotFound,
1790
                    "Data not found in Redis store for digest: {key:?}"
1791
                ));
1792
            }
1793
            // The key exists and holds nothing, yet the digest names bytes
1794
            // from this offset on: that is the empty value a failed upload
1795
            // left, not the blob. NotFound sends the client back to upload it.
1796
            if let StoreKey::Digest(digest) = key.borrow()
1797
                && digest.size_bytes() > u64::try_from(data_start).unwrap_or(u64::MAX)
1798
0
                && length.is_none_or(|len| len > 0)
1799
            {
1800
                return Err(make_err!(
1801
                    Code::NotFound,
1802
                    "Redis holds an empty value for {key:?}, which names {} bytes; treating it as missing so the client uploads it again",
1803
                    digest.size_bytes()
1804
                ));
1805
            }
1806
        }
1807
1808
        writer
1809
            .send_eof()
1810
            .err_tip(|| "Failed to write EOF in redis store get_part")
1811
16
    }
1812
1813
16
    fn inner_store(&self, _digest: Option<StoreKey>) -> &dyn StoreDriver {
1814
16
        self
1815
16
    }
1816
1817
0
    fn as_any(&self) -> &(dyn core::any::Any + Sync + Send) {
1818
0
        self
1819
0
    }
1820
1821
0
    fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send> {
1822
0
        self
1823
0
    }
1824
1825
0
    fn register_health(self: Arc<Self>, registry: &mut HealthRegistryBuilder) {
1826
0
        registry.register_indicator(self);
1827
0
    }
1828
1829
9
    fn register_remove_callback(self: Arc<Self>, callback: RemoveCallback) -> Result<(), Error> {
1830
9
        debug!(?callback, "New callback");
1831
9
        background_spawn!("remove_callback_subscribe", async move {
1832
9
            self.remove_callbacks.lock().await.push(callback);
1833
            // The subscription init is retried until it succeeds: this
1834
            // function already told its caller Ok(()) (e.g.
1835
            // ExistenceCacheStore, which relies on eviction callbacks for
1836
            // invalidation), so a transient Redis error here (e.g. -LOADING
1837
            // during a restart/failover) must not permanently disable remove
1838
            // callbacks. Previously a one-shot `get_or_try_init` failure was
1839
            // only logged, the OnceCell stayed empty, and nothing ever
1840
            // retried, so the `__key*__:*` subscription was silently never
1841
            // established.
1842
9
            let mut retry_delay = Duration::from_millis(50);
1843
            loop {
1844
9
                let local_self = self.clone();
1845
9
                let init_result = self
1846
9
                    .has_remove_callback_subscribe
1847
9
                    .get_or_try_init(|| async move {
1848
                        // The notify-keyspace-events check is advisory: we log
1849
                        // misconfiguration loudly, but a failure to *read* the
1850
                        // config must not block the subscription itself.
1851
9
                        let cfg_result = async {
1852
9
                            let mut client = local_self.get_client().await
?0
;
1853
9
                            redis::cmd("CONFIG").arg("GET").arg("notify-keyspace-events").to_owned().query_async::<Vec<(String,String)>>(&mut client.connection_manager).await.map_err(|e| 
Error::from1
(
e1
).
append1
("Parsing notify-keyspace-events"))
1854
9
                        }.await;
1855
9
                        match &cfg_result {
1856
8
                            Ok(cfg) => {
1857
8
                                if cfg.len() != 1 {
1858
0
                                    warn!(?cfg, "Got multiple items for CONFIG GET, expected one");
1859
8
                                } else if let Some((_, events_cfg)) = cfg.first() {
1860
8
                                    if events_cfg.is_empty() {
1861
1
                                        error!("notify-keyspace-events not enabled for Redis, will fail to get remove callbacks");
1862
7
                                    } else if !events_cfg.contains('K') {
1863
1
                                        error!(notify_keyspace_events=events_cfg, "notify-keyspace-events does not contain 'K' so won't get keyspace events we need for eviction events");
1864
6
                                    } else if !events_cfg.contains('A') {
1865
1
                                        error!(notify_keyspace_events=events_cfg, "notify-keyspace-events does not contain 'A' so we won't get eviction events");
1866
5
                                    }
1867
                                    // FIXME: Redis events spec appears unreliable, so we subscribe anyways
1868
                                    // It should just need Ke as per https://redis.io/docs/latest/develop/pubsub/keyspace-notifications/
1869
                                    // but I'm yet to get reliable eviction events out of that
1870
8
                                    info!(notify_keyspace_events=events_cfg, "Attempting to subscribe to eviction events");
1871
0
                                }
1872
                            }
1873
1
                            Err(err) => {
1874
1
                                error!(?err, "Error while trying to initialise remove_callback_subscribe: could not verify notify-keyspace-events; subscribing anyway");
1875
                            }
1876
                        }
1877
9
                        local_self.connection_manager.psubscribe("__key*__:*").await
?0
;
1878
9
                        Ok::<(), Error>(())
1879
18
                    })
1880
9
                    .await;
1881
9
                match init_result {
1882
9
                    Ok(()) => break,
1883
0
                    Err(err) => {
1884
0
                        error!(
1885
                            ?err,
1886
                            "Error while trying to initialise remove_callback_subscribe; will retry"
1887
                        );
1888
0
                        sleep(retry_delay).await;
1889
0
                        retry_delay = (retry_delay * 2).min(Duration::from_secs(5));
1890
                    }
1891
                }
1892
            }
1893
9
        });
1894
9
        Ok(())
1895
9
    }
1896
}
1897
1898
#[async_trait]
1899
impl<C, M> HealthStatusIndicator for RedisStore<C, M>
1900
where
1901
    C: ConnectionLike + Clone + Send + Sync + Unpin + 'static,
1902
    M: RedisManager<C> + Send + Sync + Unpin + 'static,
1903
{
1904
1
    fn get_name(&self) -> &'static str {
1905
1
        "RedisStore"
1906
1
    }
1907
1908
    /// Lightweight health check: just `PING` the master, bounded by a
1909
    /// short physical timeout. The default `StoreDriver::check_health`
1910
    /// performs a full `update_oneshot` + `has` + `get_part_unchunked`
1911
    /// roundtrip, which queues behind real production traffic on the
1912
    /// same connection-permit semaphore and Redis master. When the
1913
    /// store is even moderately loaded that easily exceeds the
1914
    /// `HealthServer` per-indicator budget (default 5 s), each
1915
    /// RedisStore-backed indicator (AC, small-blob CAS, scheduler)
1916
    /// reports `HealthStatus::Timeout`, and `/status` returns 503 —
1917
    /// surfaced as a readiness-probe failure that sheds traffic from
1918
    /// an otherwise-functional pod. A `PING` proves the connection
1919
    /// is reachable and the master is accepting commands; that is
1920
    /// the only invariant a kubelet probe needs.
1921
1
    async fn check_health(&self, _namespace: Cow<'static, str>) -> HealthStatus {
1922
        let mut client = match self.get_client().await {
1923
            Ok(c) => c,
1924
            Err(e) => {
1925
                return HealthStatus::new_failed(
1926
                    self,
1927
                    format!("RedisStore::check_health: failed to acquire connection: {e}").into(),
1928
                );
1929
            }
1930
        };
1931
1932
        // Hold the `ClientWithPermit` for the duration of the call so
1933
        // its `Drop` releases the semaphore permit on exit. We just
1934
        // need a `&mut` to the connection manager underneath.
1935
1
        let ping = async {
1936
1
            redis::cmd("PING")
1937
1
                .query_async::<()>(&mut client.connection_manager)
1938
1
                .await
1939
1
        };
1940
        let retry_reason = match timeout(self.health_check_timeout, ping).await {
1941
            Ok(Ok(())) => {
1942
                return HealthStatus::new_ok(self, "RedisStore::check_health: PING ok".into());
1943
            }
1944
            // A PING that errors connection-wise or times out means the handle
1945
            // points at a master that went away (a Sentinel failover). Without
1946
            // re-resolving here, a store with no other traffic stays wedged on
1947
            // the dead handle and reports unhealthy forever — shedding readiness
1948
            // traffic from an otherwise-recovered pod until it is restarted.
1949
            Ok(Err(e)) if is_retryable_redis_error(&e) => format!("PING errored: {e}"),
1950
            Ok(Err(e)) => {
1951
                return HealthStatus::new_failed(
1952
                    self,
1953
                    format!("RedisStore::check_health: PING errored: {e}").into(),
1954
                );
1955
            }
1956
            Err(_) => format!(
1957
                "PING exceeded {}ms timeout",
1958
                self.health_check_timeout.as_millis()
1959
            ),
1960
        };
1961
1962
        // The reconnect (re-resolving the master via Sentinel) is logged by
1963
        // `StandardRedisManager::reconnect`; `retry_reason` is surfaced in the
1964
        // failure message below if the second PING still doesn't come back.
1965
        if let Err(e) = client.reconnect(&self.connection_manager).await {
1966
            return HealthStatus::new_failed(
1967
                self,
1968
                format!("RedisStore::check_health: {retry_reason}; reconnect failed: {e}").into(),
1969
            );
1970
        }
1971
1
        let ping = async {
1972
1
            redis::cmd("PING")
1973
1
                .query_async::<()>(&mut client.connection_manager)
1974
1
                .await
1975
1
        };
1976
        match timeout(self.health_check_timeout, ping).await {
1977
            Ok(Ok(())) => HealthStatus::new_ok(
1978
                self,
1979
                "RedisStore::check_health: PING ok after re-resolving master".into(),
1980
            ),
1981
            Ok(Err(e)) => HealthStatus::new_failed(
1982
                self,
1983
                format!("RedisStore::check_health: PING still errored after reconnect: {e}").into(),
1984
            ),
1985
            Err(_) => HealthStatus::new_failed(
1986
                self,
1987
                format!(
1988
                    "RedisStore::check_health: PING still exceeded {}ms timeout after reconnect",
1989
                    self.health_check_timeout.as_millis()
1990
                )
1991
                .into(),
1992
            ),
1993
        }
1994
1
    }
1995
}
1996
1997
// -------------------------------------------------------------------
1998
// Below this line are specific to the redis scheduler implementation.
1999
// -------------------------------------------------------------------
2000
2001
/// The time in milliseconds that a redis cursor can be idle before it is closed.
2002
const CURSOR_IDLE_MS: u64 = 30_000;
2003
/// The name of the field in the Redis hash that stores the data.
2004
const DATA_FIELD_NAME: &str = "data";
2005
/// The name of the field in the Redis hash that stores the version.
2006
const VERSION_FIELD_NAME: &str = "version";
2007
2008
#[allow(rustdoc::broken_intra_doc_links)]
2009
/// Lua script to set a key if the version matches.
2010
/// Args:
2011
///   KEYS[1]: The key where the version is stored.
2012
///   ARGV[1]: The expected version.
2013
///   ARGV[2]: TTL in seconds, or 0 for forever
2014
///   ARGV[3]: The new data.
2015
///   ARGV[4*]: Key-value pairs of additional data to include.
2016
/// Returns:
2017
///   The new version if the version matches. nil is returned if the
2018
///   value was not set.
2019
pub const LUA_VERSION_SET_SCRIPT: &str = formatcp!(
2020
    r"
2021
local key = KEYS[1]
2022
local expected_version = tonumber(ARGV[1])
2023
local ttl = tonumber(ARGV[2])
2024
local new_data = ARGV[3]
2025
local new_version = redis.call('HINCRBY', key, '{VERSION_FIELD_NAME}', 1)
2026
local i
2027
local indexes = {{}}
2028
2029
if new_version-1 ~= expected_version then
2030
    local reverted = redis.call('HINCRBY', key, '{VERSION_FIELD_NAME}', -1)
2031
    -- HINCRBY creates the key before the version can be checked, so a
2032
    -- caller holding a stale version for a key that has since gone leaves
2033
    -- behind a hash with nothing in it but a zeroed version. It has no
2034
    -- data, no expiry and never gains either, yet it still matches the
2035
    -- index prefix, so it sits in the index as an empty document and
2036
    -- crowds every search that reads it. Only ever removes a key this
2037
    -- call brought into existence: a real record always carries data.
2038
    if reverted == 0 and redis.call('HEXISTS', key, '{DATA_FIELD_NAME}') == 0 then
2039
        redis.call('DEL', key)
2040
    end
2041
    return {{ 0, new_version-1 }}
2042
end
2043
-- Skip first 3 argvs, as they are known inputs.
2044
-- Remember: Lua is 1-indexed.
2045
for i=4, #ARGV do
2046
    indexes[i-3] = ARGV[i]
2047
end
2048
2049
-- In testing we witnessed redis sometimes not update our FT indexes
2050
-- resulting in stale data. It appears if we delete our keys then insert
2051
-- them again it works and reduces risk significantly.
2052
redis.call('DEL', key)
2053
redis.call('HSET', key, '{DATA_FIELD_NAME}', new_data, '{VERSION_FIELD_NAME}', new_version, unpack(indexes))
2054
2055
if ttl ~= 0 then
2056
    redis.call('EXPIRE', key, ttl)
2057
end
2058
return {{ 1, new_version }}
2059
"
2060
);
2061
2062
/// How long a freshly created index is given to finish its background
2063
/// scan before a read goes ahead regardless, and how often it is asked.
2064
const INDEX_READY_TIMEOUT: Duration = Duration::from_secs(5);
2065
const INDEX_READY_POLL: Duration = Duration::from_millis(50);
2066
2067
/// `FT.CREATE` of an index that exists: `RediSearch` answers an `Index`
2068
/// extension error saying so, which a create that races another node gets.
2069
3
fn is_index_already_exists(err: &RedisError) -> bool {
2070
3
    err.kind() == redis::ErrorKind::Extension
2071
2
        && err.code() == Some("Index")
2072
1
        && err
2073
1
            .detail()
2074
1
            .is_some_and(|d| d.to_ascii_lowercase().contains("already exists"))
2075
3
}
2076
2077
/// Polls `FT.INFO` until the index reports `indexing 0`, the timeout
2078
/// passes, or the call fails; none of those stops the caller, which reads
2079
/// the index either way.
2080
1
async fn wait_for_index_ready<C>(connection_manager: C, index: &str)
2081
1
where
2082
1
    C: ConnectionLike + Send + Clone,
2083
1
{
2084
1
    wait_for_index_ready_within(connection_manager, index, INDEX_READY_TIMEOUT).await;
2085
1
}
2086
2087
1
async fn wait_for_index_ready_within<C>(connection_manager: C, index: &str, timeout: Duration)
2088
1
where
2089
1
    C: ConnectionLike + Send + Clone,
2090
1
{
2091
1
    let started = Instant::now();
2092
    loop {
2093
1
        match ft_info_indexing(connection_manager.clone(), index).await {
2094
1
            Ok(false) => return,
2095
0
            Ok(true) => {}
2096
0
            Err(err) => {
2097
0
                warn!(
2098
                    ?err,
2099
                    index, "Could not read FT.INFO after creating the index; reading it now"
2100
                );
2101
0
                return;
2102
            }
2103
        }
2104
0
        if started.elapsed() >= timeout {
2105
0
            warn!(
2106
                index,
2107
                "Index still backfilling after {timeout:?}; reading it now"
2108
            );
2109
0
            return;
2110
0
        }
2111
0
        sleep(INDEX_READY_POLL).await;
2112
    }
2113
1
}
2114
2115
/// This is the output of the calculations below hardcoded into the executable.
2116
const FINGERPRINT_CREATE_INDEX_HEX: &str = "3e762c15";
2117
2118
#[cfg(test)]
2119
mod test {
2120
    use super::FINGERPRINT_CREATE_INDEX_HEX;
2121
2122
    /// String of the `FT.CREATE` command used to create the index template.
2123
    const CREATE_INDEX_TEMPLATE: &str = "FT.CREATE {} ON HASH PREFIX 1 {} NOOFFSETS NOHL NOFIELDS NOFREQS SCHEMA {} TAG CASESENSITIVE SORTABLE";
2124
2125
    /// Compile-time fingerprint of the `FT.CREATE` command used to create the
2126
    /// index template. This is a simple CRC32 checksum of the command string.
2127
    /// We don't care about it actually being a valid CRC32 checksum, just that
2128
    /// it's a unique identifier with a low chance of collision.
2129
1
    const fn fingerprint_create_index_template() -> u32 {
2130
        const POLY: u32 = 0xEDB8_8320;
2131
        const DATA: &[u8] = CREATE_INDEX_TEMPLATE.as_bytes();
2132
1
        let mut crc = 0xFFFF_FFFF;
2133
1
        let mut i = 0;
2134
102
        while i < DATA.len() {
2135
101
            let byte = DATA[i];
2136
101
            crc ^= byte as u32;
2137
2138
101
            let mut j = 0;
2139
909
            while j < 8 {
2140
808
                crc = if crc & 1 != 0 {
2141
386
                    (crc >> 1) ^ POLY
2142
                } else {
2143
422
                    crc >> 1
2144
                };
2145
808
                j += 1;
2146
            }
2147
101
            i += 1;
2148
        }
2149
1
        crc
2150
1
    }
2151
2152
    /// Verify that our calculation always evaluates to this fixed value.
2153
    #[test]
2154
1
    fn test_fingerprint_value() {
2155
1
        assert_eq!(
2156
1
            format!("{:08x}", &fingerprint_create_index_template()),
2157
            FINGERPRINT_CREATE_INDEX_HEX,
2158
        );
2159
1
    }
2160
}
2161
2162
/// Get the name of the index to create for the given field.
2163
/// This will add some prefix data to the name to try and ensure
2164
/// if the index definition changes, the name will get a new name.
2165
macro_rules! get_index_name {
2166
    ($prefix:expr, $field:expr, $maybe_sort:expr) => {
2167
        format_args!(
2168
            "{}_{}_{}_{}",
2169
            $prefix,
2170
            $field,
2171
            $maybe_sort.unwrap_or(""),
2172
            FINGERPRINT_CREATE_INDEX_HEX
2173
        )
2174
    };
2175
}
2176
2177
/// Try to sanitize a string to be used as a Redis key.
2178
/// We don't actually modify the string, just check if it's valid.
2179
16.9k
const fn try_sanitize(s: &str) -> bool {
2180
    // Note: We cannot use for loops or iterators here because they are not const.
2181
    // Allowing us to use a const function here gives the compiler the ability to
2182
    // optimize this function away entirely in the case where the input is constant.
2183
16.9k
    let chars = s.as_bytes();
2184
16.9k
    let mut i: usize = 0;
2185
16.9k
    let len = s.len();
2186
    loop {
2187
856k
        if i >= len {
2188
16.9k
            break;
2189
839k
        }
2190
839k
        let c = chars[i];
2191
839k
        if !c.is_ascii_alphanumeric() && 
c != b'_'39.6k
{
2192
0
            return false;
2193
839k
        }
2194
839k
        i += 1;
2195
    }
2196
16.9k
    true
2197
16.9k
}
2198
2199
/// An individual subscription to a key in Redis.
2200
#[derive(Debug)]
2201
pub struct RedisSubscription {
2202
    receiver: Option<tokio::sync::watch::Receiver<String>>,
2203
    weak_subscribed_keys: Weak<RwLock<StringPatriciaMap<RedisSubscriptionPublisher>>>,
2204
}
2205
2206
impl SchedulerSubscription for RedisSubscription {
2207
    /// Wait for the subscription key to change.
2208
52.9k
    async fn changed(&mut self) -> Result<(), Error> 
{20.9k
2209
20.9k
        let receiver = self
2210
20.9k
            .receiver
2211
20.9k
            .as_mut()
2212
20.9k
            .ok_or_else(|| 
make_err!0
(
Code::Internal0
, "In RedisSubscription::changed::as_mut"))
?0
;
2213
20.9k
        receiver.changed().await.
map_err16.8k
(|err|
{0
2214
0
            Error::from_std_err(Code::Internal, &err)
2215
0
                .append("In RedisSubscription::changed::changed")
2216
0
        })
2217
16.8k
    }
2218
}
2219
2220
// If the subscription is dropped, we need to possibly remove the key from the
2221
// subscribed keys map.
2222
impl Drop for RedisSubscription {
2223
5.70k
    fn drop(&mut self) {
2224
5.70k
        let Some(receiver) = self.receiver.take() else {
2225
0
            warn!("RedisSubscription has already been dropped, nothing to do.");
2226
0
            return; // Already dropped, nothing to do.
2227
        };
2228
5.70k
        let key = receiver.borrow().clone();
2229
5.70k
        let Some(
subscribed_keys5.68k
) = self.weak_subscribed_keys.upgrade() else {
2230
22
            return; // Parent dropped — nothing to do.
2231
        };
2232
5.68k
        let mut subscribed_keys = subscribed_keys.write();
2233
5.68k
        let Some(publisher) = subscribed_keys.get(&key) else {
2234
0
            warn!(
2235
                %key,
2236
                "RedisSubscription::drop: key absent from subscribed_keys under write lock — \
2237
                 indicates an unexpected removal path",
2238
            );
2239
0
            return;
2240
        };
2241
        // Count includes our own (still-alive) receiver. If we are the
2242
        // sole subscriber, remove the publisher entry.
2243
5.68k
        if publisher.receiver_count() == 1 {
2244
5.47k
            subscribed_keys.remove(&key);
2245
5.47k
        
}203
2246
5.68k
        drop(receiver);
2247
5.70k
    }
2248
}
2249
2250
/// A publisher for a key in Redis.
2251
#[derive(Debug)]
2252
struct RedisSubscriptionPublisher {
2253
    sender: Mutex<tokio::sync::watch::Sender<String>>,
2254
}
2255
2256
impl RedisSubscriptionPublisher {
2257
5.50k
    fn new(
2258
5.50k
        key: String,
2259
5.50k
        weak_subscribed_keys: Weak<RwLock<StringPatriciaMap<Self>>>,
2260
5.50k
    ) -> (Self, RedisSubscription) {
2261
5.50k
        let (sender, receiver) = tokio::sync::watch::channel(key);
2262
5.50k
        let publisher = Self {
2263
5.50k
            sender: Mutex::new(sender),
2264
5.50k
        };
2265
5.50k
        let subscription = RedisSubscription {
2266
5.50k
            receiver: Some(receiver),
2267
5.50k
            weak_subscribed_keys,
2268
5.50k
        };
2269
5.50k
        (publisher, subscription)
2270
5.50k
    }
2271
2272
203
    fn subscribe(
2273
203
        &self,
2274
203
        weak_subscribed_keys: Weak<RwLock<StringPatriciaMap<Self>>>,
2275
203
    ) -> RedisSubscription {
2276
203
        let receiver = self.sender.lock().subscribe();
2277
203
        RedisSubscription {
2278
203
            receiver: Some(receiver),
2279
203
            weak_subscribed_keys,
2280
203
        }
2281
203
    }
2282
2283
5.68k
    fn receiver_count(&self) -> usize {
2284
5.68k
        self.sender.lock().receiver_count()
2285
5.68k
    }
2286
2287
20.2k
    fn notify(&self) {
2288
        // TODO(https://github.com/sile/patricia_tree/issues/40) When this is addressed
2289
        // we can remove the `Mutex` and use the mutable iterator directly.
2290
20.2k
        self.sender.lock().send_modify(|_| {});
2291
20.2k
    }
2292
}
2293
2294
#[derive(Debug, Clone)]
2295
pub struct RedisSubscriptionManager {
2296
    subscribed_keys: Arc<RwLock<StringPatriciaMap<RedisSubscriptionPublisher>>>,
2297
    tx_for_test: UnboundedSender<String>,
2298
    _subscription_spawn: Arc<Mutex<JoinHandleDropGuard<()>>>,
2299
}
2300
2301
impl RedisSubscriptionManager {
2302
169
    pub fn new(
2303
169
        subscriber_channel: UnboundedReceiver<PushInfo>,
2304
169
        remove_callbacks: Arc<async_lock::Mutex<Vec<RemoveCallback>>>,
2305
169
        key_prefix: String,
2306
169
    ) -> Self {
2307
169
        let subscribed_keys = Arc::new(RwLock::new(StringPatriciaMap::new()));
2308
169
        let subscribed_keys_weak = Arc::downgrade(&subscribed_keys);
2309
169
        let (tx_for_test, mut rx_for_test) = unbounded_channel();
2310
169
        let mut local_subscriber_channel = UnboundedReceiverStream::new(subscriber_channel);
2311
        Self {
2312
169
            subscribed_keys,
2313
169
            tx_for_test,
2314
169
            _subscription_spawn: Arc::new(Mutex::new(spawn!(
2315
                "redis_subscribe_spawn",
2316
132
                async move {
2317
132
                    debug!("running subscribe loop");
2318
                    loop {
2319
                        loop {
2320
46.7k
                            let 
key46.6k
= select! {
2321
46.7k
                                
value46.6k
= rx_for_test.recv() => {
2322
46.6k
                                    let Some(value) = value else {
2323
0
                                        unreachable!("Channel should never close");
2324
                                    };
2325
46.6k
                                    value
2326
                                },
2327
46.7k
                                
maybe_push_info32
= local_subscriber_channel.next() => {
2328
32
                                    if let Some(
push_info13
) = maybe_push_info {
2329
13
                                        match push_info.kind {
2330
6
                                            redis::PushKind::PMessage => {},
2331
                                            redis::PushKind::PSubscribe => {
2332
1
                                                trace!(?push_info, "PSubscribe, ignore");
2333
1
                                                continue;
2334
                                            }
2335
                                            _ => {
2336
6
                                                warn!(?push_info, "Other push_info message, discarded");
2337
6
                                                continue;
2338
                                            },
2339
                                        }
2340
6
                                        if push_info.data.len() != 3 {
2341
0
                                            error!(?push_info, "Expected exactly 3 values on subscriber channel (pattern, channel, value)");
2342
0
                                            continue;
2343
6
                                        }
2344
6
                                        let value = match push_info.data.last().unwrap() {
2345
0
                                            Value::SimpleString(s) => {
2346
0
                                                s.clone()
2347
                                            }
2348
6
                                            Value::BulkString(v) => {
2349
6
                                                String::from_utf8(v.clone()).expect("String message")
2350
                                            }
2351
0
                                            other => {
2352
0
                                                error!(?other, "Received non-string message in RedisSubscriptionManager");
2353
0
                                                continue;
2354
                                            }
2355
                                        };
2356
                                        // Redis reports maxmemory eviction as
2357
                                        // "evicted" and TTL expiry as "expired".
2358
                                        // Both mean the key is gone, so both have
2359
                                        // to invalidate anything caching its
2360
                                        // existence; treating only one as a removal
2361
                                        // leaves an ExistenceCacheStore claiming to
2362
                                        // hold a key that Redis has already dropped.
2363
6
                                        if value == "evicted" || 
value == "expired"3
{
2364
5
                                            trace!(?push_info, %value, "Key removal event");
2365
5
                                            let removed_key = if let Some(key) = push_info.data.get(1) {
2366
5
                                                if let Value::BulkString(s) = key {
2367
5
                                                    String::from_utf8(s.clone()).expect("String message")
2368
                                                } else {
2369
0
                                                    error!(?push_info, "Removed key wasn't bulk-string");
2370
0
                                                    continue;
2371
                                                }
2372
                                            } else {
2373
0
                                                error!(?push_info, "No key in removal event");
2374
0
                                                continue;
2375
                                            };
2376
5
                                            trace!(?removed_key, "Removed key");
2377
5
                                            let Some((_prefix, internal_key)) = removed_key.split_once(':') else {
2378
0
                                                error!(?removed_key, "Removed key doesn't contain a colon");
2379
0
                                                continue;
2380
                                            };
2381
2382
5
                                            let store_key = match decode_key(&key_prefix, Cow::from(internal_key)) {
2383
5
                                                Ok(k) => k.into_owned(),
2384
0
                                                Err(err) => {
2385
0
                                                    error!(%err, internal_key, "Bad redis key");
2386
0
                                                    continue;
2387
                                                }
2388
                                            };
2389
5
                                            let locked_remove_callbacks = remove_callbacks.lock().await;
2390
5
                                            let mut callbacks: FuturesUnordered<_> =
2391
5
                                                locked_remove_callbacks.iter()
2392
5
                                                .map(|callback| callback.callback(store_key.borrow()))
2393
5
                                                .collect();
2394
10
                                            while callbacks.next().await.is_some() 
{}5
2395
5
                                            continue
2396
1
                                        }
2397
1
                                        value
2398
                                    } else {
2399
19
                                        error!("Error receiving message in RedisSubscriptionManager from subscriber_channel");
2400
19
                                        break;
2401
                                    }
2402
                                }
2403
                            };
2404
46.6k
                            trace!(key, "New subscription manager key");
2405
46.6k
                            let Some(subscribed_keys) = subscribed_keys_weak.upgrade() else {
2406
0
                                warn!(
2407
                                    "It appears our parent has been dropped, exiting RedisSubscriptionManager spawn"
2408
                                );
2409
0
                                return;
2410
                            };
2411
46.6k
                            let subscribed_keys_mux = subscribed_keys.read();
2412
46.6k
                            subscribed_keys_mux
2413
46.6k
                                .common_prefix_values(&*key)
2414
46.6k
                                .for_each(RedisSubscriptionPublisher::notify);
2415
                        }
2416
                        // Sleep for a small amount of time to ensure we don't reconnect too quickly.
2417
19
                        sleep(Duration::from_secs(1)).await;
2418
                        // If we reconnect or lag behind we might have had dirty keys, so we need to
2419
                        // flag all of them as changed.
2420
2
                        let Some(subscribed_keys) = subscribed_keys_weak.upgrade() else {
2421
0
                            warn!(
2422
                                "It appears our parent has been dropped, exiting RedisSubscriptionManager spawn"
2423
                            );
2424
0
                            return;
2425
                        };
2426
2
                        let subscribed_keys_mux = subscribed_keys.read();
2427
                        // Just in case also get a new receiver.
2428
2
                        for 
publisher0
in subscribed_keys_mux.values() {
2429
0
                            publisher.notify();
2430
0
                        }
2431
                    }
2432
0
                }
2433
            ))),
2434
        }
2435
169
    }
2436
}
2437
2438
impl SubscriptionManagerNotify for RedisSubscriptionManager {
2439
46.6k
    fn notify_for_test(&self, value: String) {
2440
46.6k
        self.tx_for_test.send(value).unwrap();
2441
46.6k
    }
2442
}
2443
2444
impl SchedulerSubscriptionManager for RedisSubscriptionManager {
2445
    type Subscription = RedisSubscription;
2446
2447
5.70k
    fn subscribe<K>(&self, key: K) -> Result<Self::Subscription, Error>
2448
5.70k
    where
2449
5.70k
        K: SchedulerStoreKeyProvider,
2450
    {
2451
5.70k
        let weak_subscribed_keys = Arc::downgrade(&self.subscribed_keys);
2452
5.70k
        let mut subscribed_keys = self.subscribed_keys.write();
2453
5.70k
        let key = key.get_key();
2454
5.70k
        let key_str = key.as_str();
2455
5.70k
        let mut subscription = if let Some(
publisher203
) = subscribed_keys.get(&key_str) {
2456
203
            publisher.subscribe(weak_subscribed_keys)
2457
        } else {
2458
5.50k
            let (publisher, subscription) =
2459
5.50k
                RedisSubscriptionPublisher::new(key_str.to_string(), weak_subscribed_keys);
2460
5.50k
            subscribed_keys.insert(key_str, publisher);
2461
5.50k
            subscription
2462
        };
2463
5.70k
        subscription
2464
5.70k
            .receiver
2465
5.70k
            .as_mut()
2466
5.70k
            .ok_or_else(|| 
{0
2467
0
                make_err!(
2468
0
                    Code::Internal,
2469
                    "Receiver should be set in RedisSubscriptionManager::subscribe"
2470
                )
2471
0
            })?
2472
5.70k
            .mark_changed();
2473
2474
5.70k
        Ok(subscription)
2475
5.70k
    }
2476
2477
5.20k
    fn is_reliable() -> bool {
2478
5.20k
        false
2479
5.20k
    }
2480
}
2481
2482
/// How the scheduler's index for `K` is created: never `TEMPORARY`, since
2483
/// `RediSearch` 2.10 never collects a temporary index and every update of an
2484
/// indexed hash leaves a dead entry behind for good. A persistent index is
2485
/// collected. An index an older release created temporary must not be
2486
/// dropped the ordinary way, which deletes its hashes: see
2487
/// `redis_utils::index_migration`.
2488
3
fn index_definition<K: SchedulerIndexProvider>() -> (FtCreateOptions, Vec<SearchSchema>) {
2489
3
    let mut schema = vec![SearchSchema {
2490
3
        field_name: K::INDEX_NAME.into(),
2491
3
        sortable: false,
2492
3
    }];
2493
3
    if let Some(sort_key) = K::MAYBE_SORT_KEY {
2494
3
        schema.push(SearchSchema {
2495
3
            field_name: sort_key.into(),
2496
3
            sortable: true,
2497
3
        });
2498
3
    
}0
2499
3
    let create_options = FtCreateOptions {
2500
3
        prefixes: vec![K::KEY_PREFIX.into()],
2501
3
        nohl: true,
2502
3
        nofields: true,
2503
3
        nofreqs: true,
2504
3
        nooffsets: true,
2505
3
        temporary: None,
2506
3
    };
2507
3
    (create_options, schema)
2508
3
}
2509
2510
/// How long a settled index name is trusted before it is checked again. A
2511
/// failed check is trusted as long, so a server that refuses the check
2512
/// (some managed Redis refuse `FT.DEBUG`) is asked once a minute, not on
2513
/// every read.
2514
const INDEX_MIGRATION_RECHECK: Duration = Duration::from_mins(1);
2515
2516
/// How long an answer that holds only while a replacement backfills is
2517
/// trusted, so concurrent reads do not each repeat the check.
2518
const INDEX_MIGRATION_POLL: Duration = Duration::from_secs(1);
2519
2520
/// How long a read waits for a replacement index to finish backfilling when
2521
/// there is no original left to read meanwhile. Reads normally stay on the
2522
/// original while the replacement fills, so this is only hit if the
2523
/// original was removed some other way.
2524
const MIGRATION_READY_TIMEOUT: Duration = Duration::from_mins(10);
2525
2526
impl<C, M> RedisStore<C, M>
2527
where
2528
    C: Clone + ConnectionLike + Sync + Send + 'static,
2529
    M: RedisManager<C> + Sync + Send + 'static,
2530
{
2531
16.9k
    fn original_index_name<K: SchedulerIndexProvider>() -> String {
2532
16.9k
        format!(
2533
            "{}",
2534
16.9k
            get_index_name!(K::KEY_PREFIX, K::INDEX_NAME, K::MAYBE_SORT_KEY)
2535
        )
2536
16.9k
    }
2537
2538
    /// The index a read of `K` uses, advancing the migration of an inherited
2539
    /// temporary index by one step when `migrate_temporary_indexes` is set.
2540
    /// Any failure reads the original, which is what reads did before.
2541
16.9k
    async fn read_index_name<K: SchedulerIndexProvider>(&self) -> String {
2542
16.9k
        let original = Self::original_index_name::<K>();
2543
16.9k
        if !self.migrate_temporary_indexes {
2544
16.9k
            return original;
2545
0
        }
2546
0
        if let Some((name, until)) = self.index_names.lock().get(&original)
2547
0
            && Instant::now() < *until
2548
        {
2549
0
            return name.clone();
2550
0
        }
2551
        // One reader checks; the rest wait here and take the answer it
2552
        // cached, instead of a herd of FT.DEBUG/FT.INFO calls (and, in
2553
        // Step::WaitForReplacement, a herd of ready polls).
2554
0
        let _gate = self.index_migration_gate.lock().await;
2555
0
        if let Some((name, until)) = self.index_names.lock().get(&original)
2556
0
            && Instant::now() < *until
2557
        {
2558
0
            return name.clone();
2559
0
        }
2560
0
        let (name, trust) = match self.migration_step::<K>(&original).await {
2561
0
            Ok(decided) => decided,
2562
0
            Err(err) => {
2563
0
                warn!(index = %original, ?err, "Could not check the index for migration; reading it");
2564
0
                (original.clone(), INDEX_MIGRATION_RECHECK)
2565
            }
2566
        };
2567
0
        self.index_names
2568
0
            .lock()
2569
0
            .insert(original, (name.clone(), Instant::now() + trust));
2570
0
        name
2571
16.9k
    }
2572
2573
    /// Drops the decided name for `K`, so the next read checks again. Used
2574
    /// when the index a read was given has gone, which happens to a read
2575
    /// that was handed the original just before another process retired it.
2576
0
    fn forget_index_name<K: SchedulerIndexProvider>(&self) {
2577
0
        self.index_names
2578
0
            .lock()
2579
0
            .remove(&Self::original_index_name::<K>());
2580
0
    }
2581
2582
    /// The index a count of `K` uses. Counting never migrates: it follows
2583
    /// what reads decided.
2584
2
    fn count_index_name<K: SchedulerIndexProvider>(&self) -> String {
2585
2
        let original = Self::original_index_name::<K>();
2586
2
        self.index_names
2587
2
            .lock()
2588
2
            .get(&original)
2589
2
            .map_or(original, |(name, _)| 
name0
.
clone0
())
2590
2
    }
2591
2592
    /// One step of `redis_utils::index_migration`. Returns the index to read
2593
    /// and how long that answer holds.
2594
0
    async fn migration_step<K: SchedulerIndexProvider>(
2595
0
        &self,
2596
0
        original: &str,
2597
0
    ) -> Result<(String, Duration), Error> {
2598
0
        let replacement = migrated_name(original);
2599
0
        let (mut connection, _) = self.connection_manager.get_connection().await?;
2600
0
        let original_kind = index_kind(&mut connection, original).await?;
2601
0
        let replacement_state = match index_kind(&mut connection, &replacement).await? {
2602
0
            IndexKind::Missing => None,
2603
            IndexKind::Persistent => {
2604
                // Both counts in one MULTI/EXEC: a key written or deleted
2605
                // while checking moves both or neither, so replacement_state
2606
                // compares them strictly, with no tolerance a short scan
2607
                // could hide inside.
2608
0
                let (original_docs, info) = if original_kind == IndexKind::Missing {
2609
0
                    (None, ft_info(connection.clone(), &replacement).await?)
2610
                } else {
2611
0
                    let (original_info, replacement_info) =
2612
0
                        ft_info_pair(connection.clone(), original, &replacement).await?;
2613
0
                    (Some(original_info.num_docs), replacement_info)
2614
                };
2615
0
                let state = index_migration::replacement_state(info, original_docs);
2616
0
                if state == Replacement::Incomplete {
2617
0
                    error!(
2618
                        %original,
2619
                        %replacement,
2620
                        replacement_docs = info.num_docs,
2621
                        ?original_docs,
2622
                        hash_indexing_failures = ?info.hash_indexing_failures,
2623
                        "The replacement index does not hold every key; reads stay on the \
2624
                         original. Check Redis memory, then drop the replacement with \
2625
                         `FT.DROPINDEX {replacement} _FORCEKEEPDOCS` to build it again"
2626
                    );
2627
0
                }
2628
0
                Some(state)
2629
            }
2630
            IndexKind::Temporary(_) => {
2631
0
                return Err(make_err!(
2632
0
                    Code::Internal,
2633
0
                    "Replacement index {replacement} is temporary; it was not created by this code"
2634
0
                ));
2635
            }
2636
        };
2637
0
        match index_migration::plan(original_kind, replacement_state) {
2638
            Step::UseOriginal => Ok((
2639
0
                original.to_string(),
2640
0
                if original_kind == IndexKind::Persistent {
2641
0
                    INDEX_MIGRATION_RECHECK
2642
                } else {
2643
0
                    INDEX_MIGRATION_POLL
2644
                },
2645
            )),
2646
            Step::CreateReplacement => {
2647
0
                let (create_options, schema) = index_definition::<K>();
2648
0
                match ft_create(
2649
0
                    connection.clone(),
2650
0
                    replacement.clone(),
2651
0
                    create_options,
2652
0
                    schema,
2653
                )
2654
0
                .await
2655
                {
2656
                    Ok(()) => {
2657
0
                        info!(%original, %replacement, "Building a persistent index to replace a temporary one");
2658
                    }
2659
0
                    Err(err) if is_index_already_exists(&err) => {}
2660
0
                    Err(err) if is_out_of_memory(&err) => {
2661
0
                        error!(
2662
                            %original,
2663
                            %replacement,
2664
                            "Redis is at maxmemory, so the persistent index that replaces the \
2665
                             temporary one cannot be built; reads stay on the temporary index. \
2666
                             Raise maxmemory, or restart Redis: a restart rebuilds the temporary \
2667
                             index without its dead entries, which frees most of the memory"
2668
                        );
2669
0
                        return Ok((original.to_string(), INDEX_MIGRATION_RECHECK));
2670
                    }
2671
0
                    Err(err) => return Err(err.into()),
2672
                }
2673
0
                Ok((original.to_string(), INDEX_MIGRATION_POLL))
2674
            }
2675
0
            Step::WaitOnOriginal => Ok((original.to_string(), INDEX_MIGRATION_POLL)),
2676
0
            Step::KeepOriginal => Ok((original.to_string(), INDEX_MIGRATION_RECHECK)),
2677
            Step::WaitForReplacement => {
2678
0
                wait_for_index_ready_within(connection, &replacement, MIGRATION_READY_TIMEOUT)
2679
0
                    .await;
2680
0
                Ok((replacement, INDEX_MIGRATION_POLL))
2681
            }
2682
0
            Step::UseReplacement { retire_original } => {
2683
0
                if retire_original {
2684
0
                    drop_keeping_documents(&mut connection, original).await?;
2685
0
                    info!(%original, %replacement, "Retired the temporary index, keeping its keys");
2686
0
                }
2687
0
                Ok((replacement, INDEX_MIGRATION_RECHECK))
2688
            }
2689
        }
2690
0
    }
2691
2692
    /// Opens one aggregate read over `K`'s index for
2693
    /// `search_by_index_prefix`: resolves which index to read, runs
2694
    /// `FT.AGGREGATE` on it, and recovers a stale resolved name, a moved
2695
    /// master and a missing index. The returned stream pages the cursor; an
2696
    /// error from a later page ends it.
2697
16.9k
    async fn open_index_rows<K: SchedulerIndexProvider + Send>(
2698
16.9k
        &self,
2699
16.9k
        index_value: &str,
2700
16.9k
    ) -> Result<Pin<Box<dyn Stream<Item = Result<Value, RedisError>> + Send>>, Error> {
2701
16.9k
        let index_name = self.read_index_name::<K>().await;
2702
16.9k
        let 
run_ft_aggregate16.9k
= |connection_manager: C, index_name: String| async {
2703
16.9k
            ft_aggregate(
2704
16.9k
                connection_manager,
2705
16.9k
                index_name,
2706
16.9k
                if index_value.is_empty() {
2707
2
                    "*".to_string()
2708
                } else {
2709
16.9k
                    format!("@{}:{{ {} }}", K::INDEX_NAME, index_value)
2710
                },
2711
                FtAggregateOptions {
2712
16.9k
                    load: vec![DATA_FIELD_NAME.into(), VERSION_FIELD_NAME.into()],
2713
16.9k
                    cursor: FtAggregateCursor {
2714
16.9k
                        count: self.max_count_per_cursor,
2715
16.9k
                        max_idle: CURSOR_IDLE_MS,
2716
16.9k
                    },
2717
16.9k
                    sort_by: K::MAYBE_SORT_KEY.map_or_else(Vec::new, |v| 
vec!5.73k
[
format!5.73k
("@{v}")]),
2718
                    sort_desc: K::SORT_DESCENDING,
2719
                },
2720
            )
2721
16.9k
            .await
2722
33.8k
        };
2723
16.9k
        let run_ft_create = |connection_manager: C, index_name: String| async 
{3
2724
3
            let (create_options, schema) = index_definition::<K>();
2725
3
            ft_create(connection_manager, index_name, create_options, schema).await
2726
6
        };
2727
2728
16.9k
        let (connection_manager, connect_id) = self.connection_manager.get_connection().await
?0
;
2729
16.9k
        let 
stream16.9k
= match run_ft_aggregate(connection_manager.clone(), index_name.clone()).await {
2730
            // A demoted master answers READONLY and a dead/old master drops the
2731
            // connection or times the command out. Both mean the master moved
2732
            // (Sentinel failover) — re-resolve it and retry rather than letting
2733
            // the scheduler's matching loop spin on a stale handle. (A missing
2734
            // index is not retryable here; it falls through to the create path
2735
            // below, which creates the index and retries the aggregate inline.)
2736
11
            Err(
err8
) if is_retryable_redis_error(&err
)8
=> {
2737
3
                let (connection_manager, _connect_id) =
2738
8
                    self.connection_manager.reconnect(connect_id).await
?5
;
2739
3
                run_ft_aggregate(connection_manager, index_name.clone())
2740
3
                    .await
2741
3
                    .err_tip(|| 
{0
2742
0
                        format!(
2743
                            "Error with reconnected ft_aggregate in RedisStore::search_by_index_prefix({index_name})"
2744
                        )
2745
0
                    })
2746
            }
2747
            Err(_) => {
2748
                // The index this read was given may have just been retired
2749
                // by another process finishing the migration. Creating it
2750
                // again would bring back the index the migration removed and
2751
                // read it before it is full; ask again which index to read.
2752
3
                let moved = if self.migrate_temporary_indexes {
2753
0
                    self.forget_index_name::<K>();
2754
0
                    let current = self.read_index_name::<K>().await;
2755
0
                    (current != index_name).then_some(current)
2756
                } else {
2757
3
                    None
2758
                };
2759
                // A failure on the re-resolved name falls through to the
2760
                // create path below only when that name is the original,
2761
                // which the ordinary path owns and may recreate — the
2762
                // behavior every failure had before the migration existed.
2763
                // A replacement is never created here: it would come back
2764
                // empty and be read before it is full. `index_name` is
2765
                // rebound so the create path works on whatever index the
2766
                // migration settled on.
2767
3
                let (index_name, resolved) = match moved {
2768
0
                    Some(current) => {
2769
0
                        let result = run_ft_aggregate(connection_manager.clone(), current.clone())
2770
0
                            .await
2771
0
                            .err_tip(|| {
2772
0
                                format!(
2773
                                    "Error with re-resolved ft_aggregate in RedisStore::search_by_index_prefix({current})"
2774
                                )
2775
0
                            });
2776
0
                        let recreate =
2777
0
                            result.is_err() && current == Self::original_index_name::<K>();
2778
0
                        (current, (!recreate).then_some(result))
2779
                    }
2780
3
                    None => (index_name, None),
2781
                };
2782
3
                if let Some(
result0
) = resolved {
2783
0
                    result
2784
                } else {
2785
3
                    let (connection_manager, result) =
2786
3
                        match run_ft_create(connection_manager.clone(), index_name.clone()).await {
2787
3
                            Err(
err0
) if is_retryable_redis_error(&err
)0
=> {
2788
0
                                let (connection_manager, _connect_id) =
2789
0
                                    self.connection_manager.reconnect(connect_id).await?;
2790
                                (
2791
0
                                    connection_manager.clone(),
2792
0
                                    run_ft_create(connection_manager, index_name.clone()).await,
2793
                                )
2794
                            }
2795
3
                            result => (connection_manager, result),
2796
                        };
2797
2798
                    // RediSearch returns ErrorKind::Extension with code "Index"
2799
                    // and detail along the lines of "Index already exists" when
2800
                    // FT.CREATE races with another node.
2801
3
                    let create_result = result.or_else(|e| {
2802
3
                        if is_index_already_exists(&e) {
2803
1
                            Ok(())
2804
                        } else {
2805
2
                            Err(e).err_tip(|| {
2806
2
                                format!(
2807
                                    "Error with ft_create in RedisStore::search_by_index_prefix({index_name})"
2808
                                )
2809
2
                            })
2810
                        }
2811
3
                    });
2812
2813
                    // FT.CREATE returns before the index holds the keys that
2814
                    // already exist; RediSearch scans them in the background.
2815
                    // An aggregate in that window reads a partial index, and
2816
                    // for the scheduler's queue that is a queue with actions
2817
                    // missing, so wait for the scan to finish first. The wait
2818
                    // is bounded; past it the aggregate runs anyway and the next
2819
                    // pass re-reads.
2820
3
                    if create_result.is_ok() {
2821
1
                        wait_for_index_ready(connection_manager.clone(), &index_name).await;
2822
2
                    }
2823
2824
3
                    let run_result = run_ft_aggregate(connection_manager, index_name.clone())
2825
3
                        .await
2826
3
                        .err_tip(|| {
2827
3
                            format!(
2828
                                "Error with second ft_aggregate in RedisStore::search_by_index_prefix({index_name})"
2829
                            )
2830
3
                        });
2831
2832
                    // Creating the index will race which is ok. If it fails to create, we only
2833
                    // error if the second ft_aggregate call fails and fails to create.
2834
3
                    run_result.or_else(move |e| create_result.merge(Err(e)))
2835
                }
2836
            }
2837
16.9k
            Ok(stream) => Ok(stream),
2838
3
        }?;
2839
16.9k
        Ok(Box::pin(stream))
2840
16.9k
    }
2841
}
2842
2843
impl<C, M> SchedulerStore for RedisStore<C, M>
2844
where
2845
    C: Clone + ConnectionLike + Sync + Send + 'static,
2846
    M: RedisManager<C> + Sync + Send + 'static,
2847
{
2848
    type SubscriptionManager = RedisSubscriptionManager;
2849
2850
5.38k
    fn subscription_manager(
2851
5.38k
        &self,
2852
5.38k
    ) -> impl Future<Output = Result<Arc<RedisSubscriptionManager>, Error>> {
2853
5.38k
        std::future::ready(if self.pub_sub_channel.is_none() {
2854
0
            Err(make_input_err!(
2855
0
                "RedisStore must have a pubsub for Redis Scheduler if using subscriptions"
2856
0
            ))
2857
        } else {
2858
5.38k
            Ok(self.subscription_manager.clone())
2859
        })
2860
5.38k
    }
2861
2862
48.7k
    async fn update_data<T>(&self, data: T, expiry: Option<Duration>) -> Result<Option<i64>, Error>
2863
48.7k
    where
2864
48.7k
        T: SchedulerStoreDataProvider
2865
48.7k
            + SchedulerStoreKeyProvider
2866
48.7k
            + SchedulerCurrentVersionProvider
2867
48.7k
            + Send,
2868
48.7k
    {
2869
48.7k
        let key = data.get_key();
2870
48.7k
        let redis_key = self.encode_key(&key);
2871
48.7k
        let 
mut client46.7k
= self.get_client().await
?0
;
2872
46.7k
        let maybe_index = data.get_indexes().err_tip(|| 
{0
2873
0
            format!("Err getting index in RedisStore::update_data::versioned for {redis_key}")
2874
0
        })?;
2875
46.7k
        if <T as SchedulerStoreKeyProvider>::Versioned::VALUE {
2876
7.91k
            let current_version = data.current_version();
2877
7.91k
            let data = data.try_into_bytes().err_tip(|| 
{0
2878
0
                format!("Could not convert value to bytes in RedisStore::update_data::versioned for {redis_key}")
2879
0
            })?;
2880
7.91k
            let mut script = self.connection_manager.update_script(redis_key.as_ref());
2881
7.91k
            let mut script_invocation = script
2882
7.91k
                .arg(format!("{current_version}"))
2883
7.91k
                .arg(expiry.unwrap_or(Duration::ZERO).as_secs())
2884
7.91k
                .arg(data.to_vec());
2885
23.4k
            for (name, value) in 
maybe_index7.91k
{
2886
23.4k
                script_invocation = script_invocation.arg(name).arg(value.to_vec());
2887
23.4k
            }
2888
7.91k
            let start = Instant::now();
2889
7.91k
            let (
success7.89k
,
new_version7.89k
):
(bool, i64)7.89k
= match script_invocation
2890
7.91k
                .invoke_async(&mut client.connection_manager)
2891
7.91k
                .await
2892
            {
2893
7.89k
                Ok(v) => v,
2894
16
                Err(
err8
) if is_retryable_redis_error(&err
)8
=> {
2895
8
                    client.reconnect(&self.connection_manager).await
?1
;
2896
7
                    script_invocation
2897
7
                        .invoke_async(&mut client.connection_manager)
2898
7
                        .await
2899
7
                        .err_tip(|| 
format!5
("(after reconnect) In RedisStore::update_data::versioned for {key:?}"))
?5
2900
                }
2901
8
                Err(err) => {
2902
8
                    let mut error: Error = err.into();
2903
8
                    error
2904
8
                        .messages
2905
8
                        .push(format!("In RedisStore::update_data::versioned for {key:?}"));
2906
8
                    return Err(error);
2907
                }
2908
            };
2909
2910
7.89k
            let elapsed = start.elapsed();
2911
2912
7.89k
            if elapsed > Duration::from_millis(100) {
2913
628
                warn!(
2914
                    %redis_key,
2915
                    ?elapsed,
2916
                    "Slow Redis version-set operation"
2917
                );
2918
7.26k
            }
2919
7.89k
            if !success {
2920
10
                warn!(
2921
                    %redis_key,
2922
                    %key,
2923
                    %current_version,
2924
                    %new_version,
2925
5
                    caller = core::any::type_name::<T>(),
2926
                    "Redis version conflict - optimistic lock failed"
2927
                );
2928
10
                return Ok(None);
2929
7.88k
            }
2930
7.88k
            trace!(
2931
                %redis_key,
2932
                %key,
2933
                old_version = %current_version,
2934
                %new_version,
2935
                "Updated redis key to new version"
2936
            );
2937
            // If we have a publish channel configured, send a notice that the key has been set.
2938
7.88k
            if let Some(
pub_sub_channel7.88k
) = &self.pub_sub_channel {
2939
7.88k
                return Ok(client
2940
7.88k
                    .connection_manager
2941
7.88k
                    .publish(pub_sub_channel, redis_key.as_ref())
2942
7.88k
                    .await
?3
);
2943
2
            }
2944
2
            Ok(Some(new_version))
2945
        } else {
2946
38.8k
            let data = data.try_into_bytes().err_tip(|| 
{0
2947
0
                format!("Could not convert value to bytes in RedisStore::update_data::noversion for {redis_key}")
2948
0
            })?;
2949
38.8k
            let mut fields: Vec<(String, _)> = vec![];
2950
38.8k
            fields.push((DATA_FIELD_NAME.into(), data.to_vec()));
2951
38.8k
            for (
name1.31k
,
value1.31k
) in maybe_index {
2952
1.31k
                fields.push((name.into(), value.to_vec()));
2953
1.31k
            }
2954
38.8k
            match client
2955
38.8k
                .connection_manager
2956
38.8k
                .hset_multiple::<_, _, _, ()>(redis_key.as_ref(), &fields)
2957
38.8k
                .await
2958
            {
2959
38.8k
                Ok(_v) => {
2960
38.8k
                    if let Some(expiry_v) = expiry {
2961
38.8k
                        let seconds =
2962
38.8k
                            TryInto::<i64>::try_into(expiry_v.as_secs()).err_tip(|| 
{0
2963
0
                                format!("Expiry seconds doesn't map to i64: {expiry_v:#?}")
2964
0
                            })?;
2965
38.8k
                        let 
expiry_result38.7k
:
u838.7k
= client
2966
38.8k
                            .connection_manager
2967
38.8k
                            .expire(redis_key.as_ref(), seconds)
2968
38.8k
                            .await
2969
38.7k
                            .err_tip(|| 
{1
2970
1
                                format!(
2971
                                    "In RedisStore::update_data::noversion (expiry) for {redis_key}"
2972
                                )
2973
1
                            })?;
2974
38.7k
                        if expiry_result != 1 {
2975
1
                            warn!(%redis_key, seconds, "Wasn't able to set expiry for Redis key");
2976
38.7k
                        }
2977
0
                    }
2978
                }
2979
19
                Err(err) if is_retryable_redis_error(&err) => {
2980
19
                    client.reconnect(&self.connection_manager).await
?12
;
2981
7
                    client
2982
7
                        .connection_manager
2983
7
                        .hset_multiple::<_, _, _, ()>(redis_key.as_ref(), &fields)
2984
7
                        .await
2985
7
                        .err_tip(|| 
format!4
("(after reconnect) In RedisStore::update_data::noversion (hset) for {redis_key}"))
?4
;
2986
3
                    if let Some(expiry_v) = expiry {
2987
3
                        let seconds =
2988
3
                            TryInto::<i64>::try_into(expiry_v.as_secs()).err_tip(|| 
{0
2989
0
                                format!("Expiry seconds doesn't map to i64: {expiry_v:#?}")
2990
0
                            })?;
2991
3
                        let 
expiry_result1
:
u81
= client.connection_manager.expire(redis_key.as_ref(), seconds).await
2992
3
                        .err_tip(|| 
format!2
("(after reconnect) In RedisStore::update_data::noversion (expiry) for {redis_key}"))
?2
;
2993
1
                        if expiry_result != 1 {
2994
0
                            warn!(%redis_key, seconds, "Wasn't able to set expiry for Redis key");
2995
1
                        }
2996
0
                    }
2997
                }
2998
0
                Err(err) => {
2999
0
                    let mut error: Error = err.into();
3000
0
                    error.messages.push(format!(
3001
                        "In RedisStore::update_data::noversion for {redis_key}"
3002
                    ));
3003
0
                    return Err(error);
3004
                }
3005
            }
3006
            // If we have a publish channel configured, send a notice that the key has been set.
3007
38.7k
            if let Some(
pub_sub_channel38.7k
) = &self.pub_sub_channel {
3008
38.7k
                return Ok(client
3009
38.7k
                    .connection_manager
3010
38.7k
                    .publish(pub_sub_channel, redis_key.as_ref())
3011
38.7k
                    .await
?0
);
3012
3
            }
3013
3
            Ok(Some(0)) // Always use "0" version since this is not a versioned request.
3014
        }
3015
46.6k
    }
3016
3017
16.9k
    async fn search_by_index_prefix<K>(
3018
16.9k
        &self,
3019
16.9k
        index: K,
3020
16.9k
    ) -> Result<
3021
16.9k
        impl Stream<Item = Result<<K as SchedulerStoreDecodeTo>::DecodeOutput, Error>> + Send,
3022
16.9k
        Error,
3023
16.9k
    >
3024
16.9k
    where
3025
16.9k
        K: SchedulerIndexProvider + SchedulerStoreDecodeTo + Send,
3026
16.9k
    {
3027
16.9k
        let index_value = index.index_value().into_owned();
3028
16.9k
        try_sanitize(index_value.as_str())
3029
16.9k
            .then_some(())
3030
16.9k
            .err_tip(|| 
{0
3031
0
                format!("In RedisStore::search_by_index_prefix::try_sanitize - {index_value:?}")
3032
0
            })?;
3033
16.9k
        let 
rows16.9k
= self.open_index_rows::<K>(&index_value).await
?8
;
3034
        // Retiring a migrated index closes the cursors other processes still
3035
        // hold on it, and that failure arrives on whatever page this reader
3036
        // asks for next — past open_index_rows' initial-aggregate recovery.
3037
        // Start the read over on whichever index the migration settled on,
3038
        // once: rows already seen come again, which readers of the queue
3039
        // tolerate (every row names an action whose state is checked on
3040
        // use), where erroring out here fails the caller's whole pass.
3041
16.9k
        let stream = futures::stream::unfold(
3042
16.9k
            (Some(rows), index_value, true, self),
3043
498k
            move |(mut rows, index_value, mut may_restart, store)| async move {
3044
                loop {
3045
498k
                    match rows.as_mut()
?0
.next().await {
3046
481k
                        Some(Ok(row)) => {
3047
481k
                            return Some((Ok(row), (rows, index_value, may_restart, store)));
3048
                        }
3049
0
                        Some(Err(err))
3050
0
                            if may_restart
3051
0
                                && store.migrate_temporary_indexes
3052
0
                                && is_cursor_lost(&err) =>
3053
                        {
3054
0
                            may_restart = false;
3055
0
                            info!(
3056
                                %index_value,
3057
                                ?err,
3058
                                "A page read lost its index or cursor (an index another \
3059
                                 process retired loses both); starting the read over"
3060
                            );
3061
0
                            store.forget_index_name::<K>();
3062
0
                            match store.open_index_rows::<K>(&index_value).await {
3063
0
                                Ok(reopened) => rows = Some(reopened),
3064
0
                                Err(reopen_err) => {
3065
0
                                    return Some((
3066
0
                                        Err(reopen_err.merge(err)),
3067
0
                                        (None, index_value, may_restart, store),
3068
0
                                    ));
3069
                                }
3070
                            }
3071
                        }
3072
0
                        Some(Err(err)) => {
3073
                            return Some((
3074
0
                                Err(Error::from(err)).err_tip(
3075
                                    || "Error in stream of in RedisStore::search_by_index_prefix",
3076
                                ),
3077
0
                                (None, index_value, may_restart, store),
3078
                            ));
3079
                        }
3080
16.9k
                        None => return None,
3081
                    }
3082
                }
3083
997k
            },
3084
        );
3085
3086
481k
        Ok(
stream16.9k
.
filter_map16.9k
(|result| async move {
3087
481k
            let raw_redis_map = match result {
3088
481k
                Ok(v) => v,
3089
0
                Err(e) => return Some(Err(e)),
3090
            };
3091
3092
481k
            if matches!(raw_redis_map, Value::Int(_)) {
3093
0
                return None;
3094
481k
            }
3095
3096
481k
            let Some(redis_map) = raw_redis_map.as_sequence() else {
3097
0
                return Some(Err(Error::new(
3098
0
                    Code::Internal,
3099
0
                    format!("Non-array from ft_aggregate: {raw_redis_map:?}"),
3100
0
                )));
3101
            };
3102
481k
            let mut redis_map_iter = redis_map.iter();
3103
481k
            let mut bytes_data: Option<Bytes> = None;
3104
481k
            let mut version: Option<i64> = None;
3105
1.44M
            while let Some(
key962k
) = redis_map_iter.next() {
3106
962k
                let value = redis_map_iter.next().unwrap();
3107
962k
                let Value::BulkString(k) = key else {
3108
0
                    return Some(Err(Error::new(
3109
0
                        Code::Internal,
3110
0
                        format!("Non-BulkString key from ft_aggregate: {key:?}"),
3111
0
                    )));
3112
                };
3113
962k
                let Ok(str_key) = str::from_utf8(k) else {
3114
0
                    return Some(Err(Error::new(
3115
0
                        Code::Internal,
3116
0
                        format!("Non-utf8 key from ft_aggregate: {key:?}"),
3117
0
                    )));
3118
                };
3119
962k
                let Value::BulkString(v) = value else {
3120
0
                    return Some(Err(Error::new(
3121
0
                        Code::Internal,
3122
0
                        format!("Non-BulkString value from ft_aggregate: {key:?}"),
3123
0
                    )));
3124
                };
3125
962k
                match str_key {
3126
962k
                    DATA_FIELD_NAME => {
3127
481k
                        bytes_data = Some(v.clone().into());
3128
481k
                    }
3129
480k
                    VERSION_FIELD_NAME => {
3130
480k
                        let Ok(str_v) = str::from_utf8(v) else {
3131
0
                            return Some(Err(Error::new(
3132
0
                                Code::Internal,
3133
0
                                format!("Non-utf8 version value from ft_aggregate: {v:?}"),
3134
0
                            )));
3135
                        };
3136
480k
                        let Ok(raw_version) = str_v.parse::<i64>() else {
3137
0
                            return Some(Err(Error::new(
3138
0
                                Code::Internal,
3139
0
                                format!("Non-integer version value from ft_aggregate: {str_v:?}"),
3140
0
                            )));
3141
                        };
3142
480k
                        version = Some(raw_version);
3143
                    }
3144
1
                    other => {
3145
1
                        if K::MAYBE_SORT_KEY == Some(other) {
3146
1
                            // ignore sort keys
3147
1
                        } else {
3148
0
                            return Some(Err(Error::new(
3149
0
                                Code::Internal,
3150
0
                                format!("Extra keys from ft_aggregate: {other}"),
3151
0
                            )));
3152
                        }
3153
                    }
3154
                }
3155
            }
3156
481k
            let Some(found_bytes_data) = bytes_data else {
3157
0
                return Some(Err(Error::new(
3158
0
                    Code::Internal,
3159
0
                    format!("Missing '{DATA_FIELD_NAME}' in ft_aggregate, got: {raw_redis_map:?}"),
3160
0
                )));
3161
            };
3162
            Some(
3163
481k
                K::decode(version.unwrap_or(0), found_bytes_data)
3164
481k
                    .err_tip(|| "In RedisStore::search_by_index_prefix::decode"),
3165
            )
3166
963k
        }))
3167
16.9k
    }
3168
3169
2
    async fn count_by_index_prefix<K>(&self, index: K) -> Result<u64, Error>
3170
2
    where
3171
2
        K: SchedulerIndexProvider + Send,
3172
2
    {
3173
2
        let index_value = index.index_value();
3174
2
        try_sanitize(index_value.as_ref())
3175
2
            .then_some(())
3176
2
            .err_tip(|| 
{0
3177
0
                format!("In RedisStore::count_by_index_prefix::try_sanitize - {index_value:?}")
3178
0
            })?;
3179
2
        let index_name = self.count_index_name::<K>();
3180
2
        let query = if index_value.is_empty() {
3181
0
            "*".to_string()
3182
        } else {
3183
2
            format!("@{}:{{ {} }}", K::INDEX_NAME, index_value)
3184
        };
3185
3186
2
        let (connection_manager, connect_id) = self.connection_manager.get_connection().await
?0
;
3187
        // Same failover handling as search_by_index_prefix: a demoted master
3188
        // answers READONLY and a dead one drops the connection, both meaning the
3189
        // master moved. Unlike that path this never creates the index; counting
3190
        // is a read-only observer and the matching loop owns index creation.
3191
2
        match ft_search_count(connection_manager, index_name.clone(), query.clone()).await {
3192
0
            Err(err) if is_retryable_redis_error(&err) => {
3193
0
                let (connection_manager, _connect_id) =
3194
0
                    self.connection_manager.reconnect(connect_id).await?;
3195
0
                ft_search_count(connection_manager, index_name.clone(), query)
3196
0
                    .await
3197
0
                    .map_err(Error::from)
3198
0
                    .err_tip(|| {
3199
0
                        format!(
3200
                            "Error with reconnected ft_search_count in RedisStore::count_by_index_prefix({index_name})"
3201
                        )
3202
0
                    })
3203
            }
3204
2
            result => result.map_err(Error::from).err_tip(|| 
{0
3205
0
                format!(
3206
                    "Error with ft_search_count in RedisStore::count_by_index_prefix({index_name})"
3207
                )
3208
0
            }),
3209
        }
3210
2
    }
3211
3212
640k
    async fn get_and_decode<K>(
3213
640k
        &self,
3214
640k
        key: K,
3215
640k
    ) -> Result<Option<<K as SchedulerStoreDecodeTo>::DecodeOutput>, Error>
3216
640k
    where
3217
640k
        K: SchedulerStoreKeyProvider + SchedulerStoreDecodeTo + Send,
3218
640k
    {
3219
640k
        let key = key.get_key();
3220
640k
        let key = self.encode_key(&key);
3221
640k
        let 
mut client627k
= self.get_client().await
?0
;
3222
        // hmget is idempotent, so re-resolve the master and retry on a transient
3223
        // failover (matching the read paths in get_part/list) instead of failing
3224
        // a scheduler-state read while the master is moving.
3225
609k
        let results: Vec<Value> = {
3226
627k
            let mut attempt: u32 = 0;
3227
            loop {
3228
627k
                attempt += 1;
3229
627k
                match client
3230
627k
                    .connection_manager
3231
627k
                    .hmget::<_, Vec<String>, Vec<Value>>(
3232
627k
                        key.as_ref(),
3233
627k
                        vec![VERSION_FIELD_NAME.into(), DATA_FIELD_NAME.into()],
3234
                    )
3235
627k
                    .await
3236
                {
3237
609k
                    Ok(v) => break v,
3238
212
                    Err(err)
3239
212
                        if attempt < MAX_REDIS_RETRY_ATTEMPTS && is_retryable_redis_error(&err) =>
3240
                    {
3241
212
                        client.reconnect(&self.connection_manager).await
?211
;
3242
1
                        sleep(Duration::from_secs_f32(DEFAULT_RETRY_DELAY)).await;
3243
                    }
3244
0
                    Err(err) => {
3245
0
                        return Err(Error::from(err).append(format!(
3246
0
                            "In RedisStore::get_without_version::notversioned {key}"
3247
0
                        )));
3248
                    }
3249
                }
3250
            }
3251
        };
3252
609k
        let Some(Value::BulkString(
data106k
)) = results.get(1) else {
3253
503k
            return Ok(None);
3254
        };
3255
        #[allow(clippy::get_first)]
3256
106k
        let version = if let Some(raw_v) = results.get(0) {
3257
106k
            match raw_v {
3258
0
                Value::Int(v) => *v,
3259
68.6k
                Value::BulkString(v) => i64::from_str(str::from_utf8(v).expect("utf-8 bulkstring"))
3260
68.6k
                    .expect("integer bulkstring"),
3261
38.0k
                Value::Nil => 0,
3262
                _ => {
3263
0
                    warn!(?raw_v, "Non-integer version!");
3264
0
                    0
3265
                }
3266
            }
3267
        } else {
3268
0
            0
3269
        };
3270
        Ok(Some(
3271
106k
            K::decode(version, Bytes::from(data.clone())).err_tip(|| 
{0
3272
0
                format!("In RedisStore::get_with_version::notversioned::decode {key}")
3273
0
            })?,
3274
        ))
3275
610k
    }
3276
}
3277
3278
#[cfg(test)]
3279
// This module hand-rolls a minimal RESP3 server plus a dedicated
3280
// current-thread runtime so the psubscribe/reconnect interleaving is exact and
3281
// timing-independent; that legitimately needs the otherwise-disallowed tokio
3282
// runtime/spawn primitives.
3283
#[expect(
3284
    clippy::disallowed_methods,
3285
    reason = "deterministic in-test RESP server needs a hand-built runtime and raw spawns"
3286
)]
3287
#[expect(
3288
    clippy::doc_markdown,
3289
    reason = "choreography docs reference bare Redis/RESP identifiers (PSUBSCRIBE, Sha1) by name"
3290
)]
3291
#[expect(
3292
    trivial_casts,
3293
    reason = "explicit Pin<Box<dyn Future>> coercion for the connect closure's return type"
3294
)]
3295
mod psubscribe_reconnect_race_tests {
3296
    use core::time::Duration;
3297
    use std::collections::HashSet;
3298
    use std::sync::Arc;
3299
3300
    use redis::aio::{ConnectionManager, ConnectionManagerConfig};
3301
    use tokio::io::{AsyncReadExt, AsyncWriteExt};
3302
    use tokio::net::{TcpListener, TcpStream};
3303
    use tokio::sync::Notify;
3304
3305
    use super::{RedisManager, StandardRedisManager};
3306
3307
    /// Reads one CRLF-terminated line from the stream.
3308
48
    async fn read_line(stream: &mut TcpStream) -> Option<String> {
3309
48
        let mut line = Vec::new();
3310
48
        let mut byte = [0u8; 1];
3311
        loop {
3312
196
            if stream.read_exact(&mut byte).await.
is_err194
() {
3313
0
                return None;
3314
194
            }
3315
194
            if byte[0] == b'\n' {
3316
46
                break;
3317
148
            }
3318
148
            if byte[0] != b'\r' {
3319
102
                line.push(byte[0]);
3320
102
            
}46
3321
        }
3322
46
        Some(String::from_utf8_lossy(&line).into_owned())
3323
46
    }
3324
3325
    /// Parses one RESP command (array of bulk strings) from the client.
3326
14
    async fn parse_command(stream: &mut TcpStream) -> Option<Vec<String>> {
3327
14
        let 
header12
= read_line(stream).await
?0
;
3328
12
        if !header.starts_with('*') {
3329
0
            return Some(vec![header]);
3330
12
        }
3331
12
        let count: usize = header[1..].parse().ok()
?0
;
3332
12
        let mut parts = Vec::with_capacity(count);
3333
12
        for _ in 0..count {
3334
34
            let len_line = read_line(stream).await
?0
;
3335
34
            let len: usize = len_line.strip_prefix('$')
?0
.parse().ok()
?0
;
3336
34
            let mut data = vec![0u8; len + 2]; // include trailing CRLF
3337
34
            stream.read_exact(&mut data).await.ok()
?0
;
3338
34
            parts.push(String::from_utf8_lossy(&data[..len]).into_owned());
3339
        }
3340
12
        Some(parts)
3341
12
    }
3342
3343
    struct FakeServerState {
3344
        /// Sha1 to return for SCRIPT LOAD (must match the client's own hash).
3345
        script_hash: String,
3346
        /// Patterns each connection (by accept order) was PSUBSCRIBEd to.
3347
        patterns_per_conn: parking_lot::Mutex<Vec<Vec<String>>>,
3348
        /// Signalled when conn #1 (the reconnect connection) receives its
3349
        /// first PSUBSCRIBE (the replay of the pre-existing pattern).
3350
        replay_psubscribe_arrived: Notify,
3351
        /// The server holds conn #1's first PSUBSCRIBE reply until this is
3352
        /// notified, keeping reconnect() parked inside its replay loop.
3353
        release_replay_reply: Notify,
3354
    }
3355
3356
2
    async fn handle_conn(mut stream: TcpStream, conn_idx: usize, state: Arc<FakeServerState>) {
3357
2
        let mut first_psubscribe_on_this_conn = true;
3358
14
        while let Some(
cmd12
) = parse_command(&mut stream).await {
3359
12
            let name = cmd
3360
12
                .first()
3361
12
                .map(|c| c.to_ascii_uppercase())
3362
12
                .unwrap_or_default();
3363
12
            match name.as_str() {
3364
12
                "HELLO" => {
3365
2
                    stream
3366
2
                        .write_all(
3367
2
                            b"%3\r\n$6\r\nserver\r\n$5\r\nredis\r\n$7\r\nversion\r\n$5\r\n7.4.0\r\n$5\r\nproto\r\n:3\r\n",
3368
2
                        )
3369
2
                        .await
3370
2
                        .unwrap();
3371
                }
3372
10
                "SCRIPT" => {
3373
2
                    let reply =
3374
2
                        format!("${}\r\n{}\r\n", state.script_hash.len(), state.script_hash);
3375
2
                    stream.write_all(reply.as_bytes()).await.unwrap();
3376
                }
3377
8
                "PSUBSCRIBE" => {
3378
4
                    let pattern = cmd.get(1).cloned().unwrap_or_default();
3379
4
                    state.patterns_per_conn.lock()[conn_idx].push(pattern.clone());
3380
4
                    if conn_idx == 1 && 
first_psubscribe_on_this_conn2
{
3381
1
                        first_psubscribe_on_this_conn = false;
3382
                        // Tell the test that reconnect() is now parked awaiting
3383
                        // this reply inside its replay loop, then hold the
3384
                        // reply until the test releases it.
3385
1
                        state.replay_psubscribe_arrived.notify_one();
3386
1
                        state.release_replay_reply.notified().await;
3387
3
                    }
3388
4
                    let reply = format!(
3389
                        ">3\r\n$10\r\npsubscribe\r\n${}\r\n{}\r\n:1\r\n",
3390
4
                        pattern.len(),
3391
                        pattern
3392
                    );
3393
4
                    stream.write_all(reply.as_bytes()).await.unwrap();
3394
                }
3395
                _ => {
3396
4
                    stream.write_all(b"+OK\r\n").await.unwrap();
3397
                }
3398
            }
3399
        }
3400
0
    }
3401
3402
    /// Deterministic repro: a `psubscribe` that interleaves with a `reconnect`
3403
    /// whose subscription snapshot was taken before the insert lands only on
3404
    /// the old (about-to-be-replaced) connection. The new live connection is
3405
    /// never subscribed, yet the pattern is recorded as active in
3406
    /// `self.subscriptions`, so notifications are silently lost until an
3407
    /// unrelated future reconnect replays the set.
3408
    ///
3409
    /// Choreography (all real await points, no timing races):
3410
    ///   1. Manager connects (conn #0) and psubscribes "pre".
3411
    ///   2. Task B calls reconnect(): connects conn #1, snapshots
3412
    ///      subscriptions = {"pre"}, and parks in the replay loop because the
3413
    ///      fake server withholds conn #1's PSUBSCRIBE "pre" reply.
3414
    ///   3. The test (task A) calls psubscribe("pat"): get_connection still
3415
    ///      returns conn #0 (reconnect has not published yet), the pattern is
3416
    ///      inserted into `subscriptions`, and PSUBSCRIBE succeeds on conn #0.
3417
    ///   4. The server releases the reply; reconnect publishes conn #1.
3418
    ///   5. Conn #1 — now the live connection — was never subscribed to "pat".
3419
    #[test]
3420
1
    fn psubscribe_lost_across_reconnect() {
3421
1
        let runtime = tokio::runtime::Builder::new_current_thread()
3422
1
            .enable_all()
3423
1
            .build()
3424
1
            .unwrap();
3425
1
        runtime.block_on(async {
3426
1
            let state = Arc::new(FakeServerState {
3427
1
                script_hash: redis::Script::new(super::LUA_VERSION_SET_SCRIPT)
3428
1
                    .get_hash()
3429
1
                    .to_string(),
3430
1
                patterns_per_conn: parking_lot::Mutex::new(Vec::new()),
3431
1
                replay_psubscribe_arrived: Notify::new(),
3432
1
                release_replay_reply: Notify::new(),
3433
1
            });
3434
3435
1
            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
3436
1
            let port = listener.local_addr().unwrap().port();
3437
            {
3438
1
                let state = state.clone();
3439
1
                tokio::spawn(async move {
3440
1
                    let mut conn_idx = 0usize;
3441
                    loop {
3442
3
                        let (
stream2
, _) = listener.accept().await.
unwrap2
();
3443
2
                        state.patterns_per_conn.lock().push(Vec::new());
3444
2
                        tokio::spawn(handle_conn(stream, conn_idx, state.clone()));
3445
2
                        conn_idx += 1;
3446
                    }
3447
                });
3448
            }
3449
3450
            // Keep the push receiver alive for the whole test.
3451
1
            let (push_tx, _push_rx) = tokio::sync::mpsc::unbounded_channel();
3452
1
            let url = format!("redis://127.0.0.1:{port}/?protocol=resp3");
3453
2
            let 
connect_func1
=
Box::new1
(move || {
3454
2
                let url = url.clone();
3455
2
                let push_tx = push_tx.clone();
3456
2
                Box::pin(async move {
3457
2
                    let client = redis::Client::open(url.as_str()).unwrap();
3458
2
                    let config = ConnectionManagerConfig::new()
3459
2
                        .set_number_of_retries(0)
3460
2
                        .set_connection_timeout(Some(Duration::from_secs(5)))
3461
2
                        .set_response_timeout(Some(Duration::from_secs(5)))
3462
2
                        .set_push_sender(push_tx);
3463
2
                    ConnectionManager::new_with_config(client, config)
3464
2
                        .await
3465
2
                        .map_err(Into::into)
3466
2
                }) as core::pin::Pin<Box<_>>
3467
2
            });
3468
3469
1
            let manager = Arc::new(StandardRedisManager::new(connect_func).await.unwrap());
3470
3471
            // Step 1: a pre-existing subscription so reconnect's replay loop
3472
            // has an await point we can park it on.
3473
1
            manager.psubscribe("pre").await.unwrap();
3474
1
            let (_conn, uuid0) = manager.get_connection().await.unwrap();
3475
3476
            // Step 2: start reconnect; it will connect conn #1, snapshot
3477
            // subscriptions (= {"pre"}), and park replaying "pre".
3478
1
            let reconnect_task = {
3479
1
                let manager = manager.clone();
3480
1
                tokio::spawn(async move { manager.reconnect(uuid0).await })
3481
            };
3482
1
            tokio::time::timeout(
3483
1
                Duration::from_secs(5),
3484
1
                state.replay_psubscribe_arrived.notified(),
3485
1
            )
3486
1
            .await
3487
1
            .expect("reconnect never reached its subscription replay loop");
3488
3489
            // Step 3: concurrent psubscribe while reconnect is parked
3490
            // post-snapshot, pre-publish. It succeeds — on the old connection.
3491
1
            manager.psubscribe("pat").await.unwrap();
3492
3493
            // Step 4: let reconnect finish and publish conn #1.
3494
1
            state.release_replay_reply.notify_one();
3495
1
            let (_new_conn, new_uuid) = reconnect_task.await.unwrap().unwrap();
3496
1
            assert_ne!(
3497
                uuid0, new_uuid,
3498
                "reconnect must have published a new connection"
3499
            );
3500
3501
            // The manager believes "pat" is an active subscription...
3502
1
            let recorded: HashSet<String> = manager.subscriptions.lock().clone();
3503
1
            assert!(
3504
1
                recorded.contains("pat"),
3505
                "sanity: pattern recorded as active"
3506
            );
3507
3508
            // ...but the live connection (conn #1) was never subscribed to it.
3509
1
            let conn1_patterns = state.patterns_per_conn.lock()[1].clone();
3510
1
            assert!(
3511
1
                conn1_patterns.contains(&"pat".to_string()),
3512
                "RACE REPRODUCED: pattern \"pat\" is recorded as an active \
3513
                 subscription but the live post-reconnect connection was only \
3514
                 subscribed to {conn1_patterns:?}; keyspace notifications for \
3515
                 \"pat\" are silently lost until the next reconnect",
3516
            );
3517
1
        });
3518
1
    }
3519
}