Skip to main content

redis_server_wrapper/
server.rs

1//! Type-safe wrapper for `redis-server` with builder pattern.
2
3use std::collections::HashMap;
4use std::fs;
5use std::path::PathBuf;
6use std::time::Duration;
7
8use tokio::process::Command;
9
10use crate::cli::RedisCli;
11use crate::error::{Error, Result};
12
13/// Full configuration snapshot for a single `redis-server` process.
14///
15/// This struct is populated by the [`RedisServer`] builder and passed to
16/// [`RedisServer::start`]. You rarely need to construct it directly; use the
17/// builder instead.
18///
19/// # Example
20///
21/// ```no_run
22/// use redis_server_wrapper::RedisServer;
23///
24/// # async fn example() {
25/// let server = RedisServer::new()
26///     .port(6400)
27///     .bind("127.0.0.1")
28///     .save(false)
29///     .start()
30///     .await
31///     .unwrap();
32///
33/// assert!(server.is_alive().await);
34/// // Stopped automatically on Drop.
35/// # }
36/// ```
37#[derive(Debug, Clone)]
38pub struct RedisServerConfig {
39    // -- network --
40    /// TCP port the server listens on (default: `6379`).
41    pub port: u16,
42    /// IP address to bind (default: `"127.0.0.1"`).
43    pub bind: String,
44    /// Whether protected mode is enabled (default: `false`).
45    pub protected_mode: bool,
46    /// TCP backlog queue length, if set.
47    pub tcp_backlog: Option<u32>,
48    /// Unix domain socket path, if set.
49    pub unixsocket: Option<PathBuf>,
50    /// Unix socket file permissions (e.g. `700`), if set.
51    pub unixsocketperm: Option<u32>,
52    /// Idle client timeout in seconds (`0` = disabled), if set.
53    pub timeout: Option<u32>,
54    /// TCP keepalive interval in seconds, if set.
55    pub tcp_keepalive: Option<u32>,
56
57    // -- tls --
58    /// TLS listening port, if set.
59    pub tls_port: Option<u16>,
60    /// Path to the TLS certificate file, if set.
61    pub tls_cert_file: Option<PathBuf>,
62    /// Path to the TLS private key file, if set.
63    pub tls_key_file: Option<PathBuf>,
64    /// Passphrase for the TLS private key file, if set.
65    pub tls_key_file_pass: Option<String>,
66    /// Path to the TLS CA certificate file, if set.
67    pub tls_ca_cert_file: Option<PathBuf>,
68    /// Path to a directory containing TLS CA certificates, if set.
69    pub tls_ca_cert_dir: Option<PathBuf>,
70    /// Whether TLS client authentication is required, if set.
71    pub tls_auth_clients: Option<bool>,
72    /// Path to the TLS client certificate file (for outgoing connections), if set.
73    pub tls_client_cert_file: Option<PathBuf>,
74    /// Path to the TLS client private key file (for outgoing connections), if set.
75    pub tls_client_key_file: Option<PathBuf>,
76    /// Passphrase for the TLS client private key file, if set.
77    pub tls_client_key_file_pass: Option<String>,
78    /// Path to the DH parameters file for DHE ciphers, if set.
79    pub tls_dh_params_file: Option<PathBuf>,
80    /// Allowed TLS 1.2 ciphers (OpenSSL cipher list format), if set.
81    pub tls_ciphers: Option<String>,
82    /// Allowed TLS 1.3 ciphersuites (colon-separated), if set.
83    pub tls_ciphersuites: Option<String>,
84    /// Allowed TLS protocol versions (e.g. `"TLSv1.2 TLSv1.3"`), if set.
85    pub tls_protocols: Option<String>,
86    /// Whether the server prefers its own cipher order, if set.
87    pub tls_prefer_server_ciphers: Option<bool>,
88    /// Whether TLS session caching is enabled, if set.
89    pub tls_session_caching: Option<bool>,
90    /// Number of entries in the TLS session cache, if set.
91    pub tls_session_cache_size: Option<u32>,
92    /// Timeout in seconds for cached TLS sessions, if set.
93    pub tls_session_cache_timeout: Option<u32>,
94    /// Whether replication traffic uses TLS, if set.
95    pub tls_replication: Option<bool>,
96    /// Whether cluster bus communication uses TLS, if set.
97    pub tls_cluster: Option<bool>,
98
99    // -- general --
100    /// Whether the server daemonizes itself (default: `true`).
101    pub daemonize: bool,
102    /// Working directory for data files (default: a sub-directory of `$TMPDIR`).
103    pub dir: PathBuf,
104    /// Path to the log file, if set. Defaults to `redis.log` inside the node directory.
105    pub logfile: Option<String>,
106    /// Server log verbosity (default: [`LogLevel::Notice`]).
107    pub loglevel: LogLevel,
108    /// Number of databases, if set (Redis default: `16`).
109    pub databases: Option<u32>,
110
111    // -- memory --
112    /// Maximum memory limit (e.g. `"256mb"`), if set.
113    pub maxmemory: Option<String>,
114    /// Eviction policy when `maxmemory` is reached, if set.
115    pub maxmemory_policy: Option<String>,
116    /// Number of keys sampled per eviction round, if set (Redis default: `5`).
117    pub maxmemory_samples: Option<u32>,
118    /// Per-client memory limit (e.g. `"0"` = disabled), if set.
119    pub maxmemory_clients: Option<String>,
120    /// Eviction processing effort (1-100), if set (Redis default: `10`).
121    pub maxmemory_eviction_tenacity: Option<u32>,
122    /// Maximum number of simultaneous client connections, if set.
123    pub maxclients: Option<u32>,
124    /// Logarithmic factor for the LFU frequency counter, if set (Redis default: `10`).
125    pub lfu_log_factor: Option<u32>,
126    /// LFU counter decay time in minutes, if set (Redis default: `1`).
127    pub lfu_decay_time: Option<u32>,
128    /// Effort spent on active key expiration (1-100), if set (Redis default: `10`).
129    pub active_expire_effort: Option<u32>,
130
131    // -- lazyfree --
132    /// Whether eviction uses background deletion, if set.
133    pub lazyfree_lazy_eviction: Option<bool>,
134    /// Whether expired-key deletion uses background threads, if set.
135    pub lazyfree_lazy_expire: Option<bool>,
136    /// Whether implicit `DEL` commands (e.g. `RENAME`) use background deletion, if set.
137    pub lazyfree_lazy_server_del: Option<bool>,
138    /// Whether explicit `DEL` behaves like `UNLINK`, if set.
139    pub lazyfree_lazy_user_del: Option<bool>,
140    /// Whether `FLUSHDB`/`FLUSHALL` default to `ASYNC`, if set.
141    pub lazyfree_lazy_user_flush: Option<bool>,
142
143    // -- persistence --
144    /// RDB save policy (default: [`SavePolicy::Disabled`]).
145    pub save: SavePolicy,
146    /// Whether AOF persistence is enabled (default: `false`).
147    pub appendonly: bool,
148    /// AOF fsync policy, if set.
149    pub appendfsync: Option<AppendFsync>,
150    /// AOF filename, if set (Redis default: `"appendonly.aof"`).
151    pub appendfilename: Option<String>,
152    /// AOF directory name, if set (Redis default: `"appendonlydir"`).
153    pub appenddirname: Option<PathBuf>,
154    /// Whether the AOF file uses an RDB preamble, if set.
155    pub aof_use_rdb_preamble: Option<bool>,
156    /// Whether truncated AOF files are loaded, if set.
157    pub aof_load_truncated: Option<bool>,
158    /// Maximum allowed size of a corrupt AOF tail, if set (e.g. `"32mb"`).
159    pub aof_load_corrupt_tail_max_size: Option<String>,
160    /// Whether AOF rewrite performs incremental fsync, if set.
161    pub aof_rewrite_incremental_fsync: Option<bool>,
162    /// Whether timestamps are recorded in the AOF file, if set.
163    pub aof_timestamp_enabled: Option<bool>,
164    /// Trigger an AOF rewrite when the file grows by this percentage, if set.
165    pub auto_aof_rewrite_percentage: Option<u32>,
166    /// Minimum AOF size before an automatic rewrite is triggered, if set (e.g. `"64mb"`).
167    pub auto_aof_rewrite_min_size: Option<String>,
168    /// Whether fsync is suppressed during AOF rewrites, if set.
169    pub no_appendfsync_on_rewrite: Option<bool>,
170
171    // -- replication --
172    /// Master host and port to replicate from, if set.
173    pub replicaof: Option<(String, u16)>,
174    /// Password for authenticating with a master, if set.
175    pub masterauth: Option<String>,
176    /// Username for authenticating with a master, if set.
177    pub masteruser: Option<String>,
178    /// Replication backlog size (e.g. `"1mb"`), if set.
179    pub repl_backlog_size: Option<String>,
180    /// Seconds before the backlog is freed when no replicas are connected, if set.
181    pub repl_backlog_ttl: Option<u32>,
182    /// Whether TCP_NODELAY is disabled on the replication socket, if set.
183    pub repl_disable_tcp_nodelay: Option<bool>,
184    /// Diskless load policy for replicas, if set.
185    pub repl_diskless_load: Option<ReplDisklessLoad>,
186    /// Whether the master sends RDB to replicas via diskless transfer, if set.
187    pub repl_diskless_sync: Option<bool>,
188    /// Delay in seconds before starting a diskless sync, if set.
189    pub repl_diskless_sync_delay: Option<u32>,
190    /// Maximum number of replicas to wait for before starting a diskless sync, if set.
191    pub repl_diskless_sync_max_replicas: Option<u32>,
192    /// Interval in seconds between PING commands sent to the master, if set.
193    pub repl_ping_replica_period: Option<u32>,
194    /// Replication timeout in seconds, if set.
195    pub repl_timeout: Option<u32>,
196    /// IP address a replica announces to the master, if set.
197    pub replica_announce_ip: Option<String>,
198    /// Port a replica announces to the master, if set.
199    pub replica_announce_port: Option<u16>,
200    /// Whether the replica is announced to clients, if set.
201    pub replica_announced: Option<bool>,
202    /// Buffer limit for full synchronization on replicas (e.g. `"256mb"`), if set.
203    pub replica_full_sync_buffer_limit: Option<String>,
204    /// Whether replicas ignore disk-write errors, if set.
205    pub replica_ignore_disk_write_errors: Option<bool>,
206    /// Whether replicas ignore the maxmemory setting, if set.
207    pub replica_ignore_maxmemory: Option<bool>,
208    /// Whether replicas perform a lazy flush during full sync, if set.
209    pub replica_lazy_flush: Option<bool>,
210    /// Replica priority for Sentinel promotion, if set.
211    pub replica_priority: Option<u32>,
212    /// Whether the replica is read-only, if set.
213    pub replica_read_only: Option<bool>,
214    /// Whether the replica serves stale data while syncing, if set.
215    pub replica_serve_stale_data: Option<bool>,
216    /// Minimum number of replicas that must acknowledge writes, if set.
217    pub min_replicas_to_write: Option<u32>,
218    /// Maximum replication lag (in seconds) for a replica to count toward `min-replicas-to-write`, if set.
219    pub min_replicas_max_lag: Option<u32>,
220
221    // -- security --
222    /// `requirepass` password for client connections, if set.
223    pub password: Option<String>,
224    /// Path to an ACL file, if set.
225    pub acl_file: Option<PathBuf>,
226
227    // -- cluster --
228    /// Whether Redis Cluster mode is enabled (default: `false`).
229    pub cluster_enabled: bool,
230    /// Cluster node timeout in milliseconds, if set.
231    pub cluster_node_timeout: Option<u64>,
232    /// Path to the cluster config file, if set. Overrides the auto-generated default.
233    pub cluster_config_file: Option<PathBuf>,
234    /// Whether full hash slot coverage is required for the cluster to accept writes, if set.
235    pub cluster_require_full_coverage: Option<bool>,
236    /// Whether reads are allowed when the cluster is down, if set.
237    pub cluster_allow_reads_when_down: Option<bool>,
238    /// Whether pubsub shard channels are allowed when the cluster is down, if set.
239    pub cluster_allow_pubsubshard_when_down: Option<bool>,
240    /// Whether automatic replica migration is allowed, if set.
241    pub cluster_allow_replica_migration: Option<bool>,
242    /// Minimum number of replicas a master must have before one can migrate, if set.
243    pub cluster_migration_barrier: Option<u32>,
244    /// Whether this replica will never attempt a failover, if set.
245    pub cluster_replica_no_failover: Option<bool>,
246    /// Factor multiplied by node timeout to determine replica validity, if set.
247    pub cluster_replica_validity_factor: Option<u32>,
248    /// IP address this node announces to the cluster bus, if set.
249    pub cluster_announce_ip: Option<String>,
250    /// Client port this node announces to the cluster, if set.
251    pub cluster_announce_port: Option<u16>,
252    /// Cluster bus port this node announces, if set.
253    pub cluster_announce_bus_port: Option<u16>,
254    /// TLS port this node announces to the cluster, if set.
255    pub cluster_announce_tls_port: Option<u16>,
256    /// Hostname this node announces to the cluster, if set.
257    pub cluster_announce_hostname: Option<String>,
258    /// Human-readable node name announced to the cluster, if set.
259    pub cluster_announce_human_nodename: Option<String>,
260    /// Dedicated cluster bus port, if set (0 = auto, default offset +10000).
261    pub cluster_port: Option<u16>,
262    /// Preferred endpoint type for cluster redirections, if set (e.g. `"ip"`, `"hostname"`).
263    pub cluster_preferred_endpoint_type: Option<String>,
264    /// Send buffer limit in bytes for cluster bus links, if set.
265    pub cluster_link_sendbuf_limit: Option<u64>,
266    /// Compatibility sample ratio percentage, if set.
267    pub cluster_compatibility_sample_ratio: Option<u32>,
268    /// Maximum lag in bytes before slot migration handoff, if set.
269    pub cluster_slot_migration_handoff_max_lag_bytes: Option<u64>,
270    /// Write pause timeout in milliseconds during slot migration, if set.
271    pub cluster_slot_migration_write_pause_timeout: Option<u64>,
272    /// Whether per-slot statistics are enabled, if set.
273    pub cluster_slot_stats_enabled: Option<bool>,
274
275    // -- data structures --
276    /// Maximum number of entries in a hash before converting from listpack to hash table, if set.
277    pub hash_max_listpack_entries: Option<u32>,
278    /// Maximum size of a hash entry value before converting from listpack to hash table, if set.
279    pub hash_max_listpack_value: Option<u32>,
280    /// Maximum listpack size for list entries (positive = element count, negative = byte limit), if set.
281    pub list_max_listpack_size: Option<i32>,
282    /// Number of list quicklist nodes at each end that are not compressed, if set.
283    pub list_compress_depth: Option<u32>,
284    /// Maximum number of integer entries in a set before converting from intset to hash table, if set.
285    pub set_max_intset_entries: Option<u32>,
286    /// Maximum number of entries in a set before converting from listpack to hash table, if set.
287    pub set_max_listpack_entries: Option<u32>,
288    /// Maximum size of a set entry value before converting from listpack to hash table, if set.
289    pub set_max_listpack_value: Option<u32>,
290    /// Maximum number of entries in a sorted set before converting from listpack to skiplist, if set.
291    pub zset_max_listpack_entries: Option<u32>,
292    /// Maximum size of a sorted set entry value before converting from listpack to skiplist, if set.
293    pub zset_max_listpack_value: Option<u32>,
294    /// Maximum number of bytes used by the sparse representation of a HyperLogLog, if set.
295    pub hll_sparse_max_bytes: Option<u32>,
296    /// Maximum number of bytes in a single stream listpack node, if set.
297    pub stream_node_max_bytes: Option<u32>,
298    /// Maximum number of entries in a single stream listpack node, if set.
299    pub stream_node_max_entries: Option<u32>,
300    /// Duration in milliseconds for stream ID de-duplication, if set.
301    pub stream_idmp_duration: Option<u64>,
302    /// Maximum number of entries tracked for stream ID de-duplication, if set.
303    pub stream_idmp_maxsize: Option<u64>,
304
305    // -- modules --
306    /// List of Redis module paths to load at startup, each with its load-time arguments.
307    pub loadmodule: Vec<(PathBuf, Vec<String>)>,
308
309    // -- advanced --
310    /// Server tick frequency in Hz, if set (Redis default: `10`).
311    pub hz: Option<u32>,
312    /// Number of I/O threads, if set.
313    pub io_threads: Option<u32>,
314    /// Whether I/O threads also handle reads, if set.
315    pub io_threads_do_reads: Option<bool>,
316    /// Keyspace notification event mask (e.g. `"KEA"`), if set.
317    pub notify_keyspace_events: Option<String>,
318
319    // -- slow log --
320    /// Log queries slower than this many microseconds (`0` = log everything, `-1` = disabled).
321    pub slowlog_log_slower_than: Option<i64>,
322    /// Maximum number of entries in the slow log.
323    pub slowlog_max_len: Option<u32>,
324
325    // -- latency tracking --
326    /// Latency monitor threshold in milliseconds (`0` = disabled).
327    pub latency_monitor_threshold: Option<u64>,
328    /// Enable the extended latency tracking system.
329    pub latency_tracking: Option<bool>,
330    /// Percentiles reported by the latency tracking system (e.g. `"50 99 99.9"`).
331    pub latency_tracking_info_percentiles: Option<String>,
332
333    // -- active defragmentation --
334    /// Enable active defragmentation.
335    pub activedefrag: Option<bool>,
336    /// Minimum amount of fragmentation waste to start defragmentation.
337    pub active_defrag_ignore_bytes: Option<String>,
338    /// Minimum percentage of fragmentation to start defragmentation.
339    pub active_defrag_threshold_lower: Option<u32>,
340    /// Maximum percentage of fragmentation at which we use maximum effort.
341    pub active_defrag_threshold_upper: Option<u32>,
342    /// Minimal effort for defragmentation as a percentage of CPU time.
343    pub active_defrag_cycle_min: Option<u32>,
344    /// Maximum effort for defragmentation as a percentage of CPU time.
345    pub active_defrag_cycle_max: Option<u32>,
346    /// Maximum number of set/hash/zset/list fields processed per defrag scan step.
347    pub active_defrag_max_scan_fields: Option<u32>,
348
349    // -- logging and process --
350    /// Enable logging to syslog.
351    pub syslog_enabled: Option<bool>,
352    /// Syslog identity string.
353    pub syslog_ident: Option<String>,
354    /// Syslog facility (e.g. `"local0"`).
355    pub syslog_facility: Option<String>,
356    /// Supervision mode (`"upstart"`, `"systemd"`, `"auto"`, or `"no"`).
357    pub supervised: Option<String>,
358    /// Show the Redis logo on startup.
359    pub always_show_logo: Option<bool>,
360    /// Set the process title.
361    pub set_proc_title: Option<bool>,
362    /// Template for the process title.
363    pub proc_title_template: Option<String>,
364
365    // -- security and ACL --
366    /// Default pub/sub permissions for ACL users (`"allchannels"` or `"resetchannels"`).
367    pub acl_pubsub_default: Option<String>,
368    /// Maximum length of the ACL log.
369    pub acllog_max_len: Option<u32>,
370    /// Enable the DEBUG command (`"yes"`, `"local"`, or `"no"`).
371    pub enable_debug_command: Option<String>,
372    /// Enable the MODULE command (`"yes"`, `"local"`, or `"no"`).
373    pub enable_module_command: Option<String>,
374    /// Allow CONFIG SET to modify protected configs.
375    pub enable_protected_configs: Option<String>,
376    /// Rename a command (command, new-name). Empty new-name disables the command.
377    pub rename_command: Vec<(String, String)>,
378    /// Sanitize dump payload on restore (`"yes"`, `"no"`, or `"clients"`).
379    pub sanitize_dump_payload: Option<String>,
380    /// Hide user data from log messages.
381    pub hide_user_data_from_log: Option<bool>,
382
383    // -- networking (additional) --
384    /// Source address for outgoing connections.
385    pub bind_source_addr: Option<String>,
386    /// Busy reply threshold in milliseconds.
387    pub busy_reply_threshold: Option<u64>,
388    /// Client output buffer limits (e.g. `"normal 0 0 0"`, `"replica 256mb 64mb 60"`).
389    pub client_output_buffer_limit: Vec<String>,
390    /// Maximum size of a single client query buffer.
391    pub client_query_buffer_limit: Option<String>,
392    /// Maximum size of a single protocol bulk request.
393    pub proto_max_bulk_len: Option<String>,
394    /// Maximum number of new connections per event loop cycle.
395    pub max_new_connections_per_cycle: Option<u32>,
396    /// Maximum number of new TLS connections per event loop cycle.
397    pub max_new_tls_connections_per_cycle: Option<u32>,
398    /// Socket mark ID for outgoing connections.
399    pub socket_mark_id: Option<u32>,
400
401    // -- RDB (additional) --
402    /// RDB dump filename.
403    pub dbfilename: Option<String>,
404    /// Enable RDB compression.
405    pub rdbcompression: Option<bool>,
406    /// Enable RDB checksum.
407    pub rdbchecksum: Option<bool>,
408    /// Incremental fsync during RDB save.
409    pub rdb_save_incremental_fsync: Option<bool>,
410    /// Delete RDB sync files used by diskless replication.
411    pub rdb_del_sync_files: Option<bool>,
412    /// Stop accepting writes when bgsave fails.
413    pub stop_writes_on_bgsave_error: Option<bool>,
414
415    // -- shutdown --
416    /// Shutdown behavior on SIGINT (e.g. `"default"`, `"save"`, `"nosave"`, `"now"`, `"force"`).
417    pub shutdown_on_sigint: Option<String>,
418    /// Shutdown behavior on SIGTERM.
419    pub shutdown_on_sigterm: Option<String>,
420    /// Maximum seconds to wait during shutdown for lagging replicas.
421    pub shutdown_timeout: Option<u32>,
422
423    // -- other --
424    /// Enable active rehashing.
425    pub activerehashing: Option<bool>,
426    /// Enable crash log on crash.
427    pub crash_log_enabled: Option<bool>,
428    /// Enable crash memory check on crash.
429    pub crash_memcheck_enabled: Option<bool>,
430    /// Disable transparent huge pages.
431    pub disable_thp: Option<bool>,
432    /// Enable dynamic Hz adjustment.
433    pub dynamic_hz: Option<bool>,
434    /// Ignore specific warnings (e.g. `"ARM64-COW-BUG"`).
435    pub ignore_warnings: Option<String>,
436    /// Include another config file.
437    pub include: Vec<PathBuf>,
438    /// Enable jemalloc background thread.
439    pub jemalloc_bg_thread: Option<bool>,
440    /// Locale collation setting.
441    pub locale_collate: Option<String>,
442    /// Lua script time limit in milliseconds.
443    pub lua_time_limit: Option<u64>,
444    /// OOM score adjustment mode (`"yes"`, `"no"`, or `"absolute"`).
445    pub oom_score_adj: Option<String>,
446    /// OOM score adjustment values (e.g. `"0 200 800"`).
447    pub oom_score_adj_values: Option<String>,
448    /// Propagation error behavior (`"panic"` or `"ignore"`).
449    pub propagation_error_behavior: Option<String>,
450    /// Maximum number of keys in the tracking table.
451    pub tracking_table_max_keys: Option<u64>,
452
453    // -- catch-all for anything not covered above --
454    /// Arbitrary key/value directives forwarded verbatim to the config file.
455    pub extra: HashMap<String, String>,
456
457    // -- binary paths --
458    /// Path to the `redis-server` binary (default: auto-detected).
459    pub redis_server_bin: String,
460    /// Path to the `redis-cli` binary (default: `"redis-cli"`).
461    pub redis_cli_bin: String,
462
463    // -- stack --
464    /// When `true`, skip automatic Redis Stack module detection and loading.
465    pub no_stack_modules: bool,
466}
467
468/// AOF fsync policy.
469#[derive(Debug, Clone, Copy)]
470pub enum AppendFsync {
471    /// Fsync after every write operation.
472    Always,
473    /// Fsync once per second (Redis default).
474    Everysec,
475    /// Let the OS decide when to flush.
476    No,
477}
478
479impl std::fmt::Display for AppendFsync {
480    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
481        match self {
482            AppendFsync::Always => f.write_str("always"),
483            AppendFsync::Everysec => f.write_str("everysec"),
484            AppendFsync::No => f.write_str("no"),
485        }
486    }
487}
488
489/// Diskless load policy for replicas.
490///
491/// Controls how a replica loads the RDB payload received from a master during
492/// diskless replication.
493#[derive(Debug, Clone, Copy)]
494pub enum ReplDisklessLoad {
495    /// Never load the RDB directly from the socket (write to disk first).
496    Disabled,
497    /// Load directly from the socket only when the current dataset is empty.
498    OnEmptyDb,
499    /// Load directly from the socket, swapping the dataset atomically.
500    Swapdb,
501}
502
503impl std::fmt::Display for ReplDisklessLoad {
504    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
505        match self {
506            ReplDisklessLoad::Disabled => f.write_str("disabled"),
507            ReplDisklessLoad::OnEmptyDb => f.write_str("on-empty-db"),
508            ReplDisklessLoad::Swapdb => f.write_str("swapdb"),
509        }
510    }
511}
512
513/// RDB save policy.
514///
515/// Controls whether and how the `save` directive is emitted in the Redis
516/// configuration file.
517#[derive(Debug, Clone, Default)]
518pub enum SavePolicy {
519    /// Emit `save ""` to disable RDB snapshots entirely.
520    #[default]
521    Disabled,
522    /// Omit the `save` directive and let Redis use its built-in defaults.
523    Default,
524    /// Emit one `save <seconds> <changes>` line for each pair.
525    Custom(Vec<(u64, u64)>),
526}
527
528/// Redis log level.
529#[derive(Debug, Clone, Copy)]
530pub enum LogLevel {
531    /// Very verbose output, useful for diagnosing Redis internals.
532    Debug,
533    /// Slightly less verbose than `Debug`.
534    Verbose,
535    /// Informational messages only (default).
536    Notice,
537    /// Only critical events are logged.
538    Warning,
539}
540
541impl std::fmt::Display for LogLevel {
542    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
543        match self {
544            LogLevel::Debug => f.write_str("debug"),
545            LogLevel::Verbose => f.write_str("verbose"),
546            LogLevel::Notice => f.write_str("notice"),
547            LogLevel::Warning => f.write_str("warning"),
548        }
549    }
550}
551
552impl Default for RedisServerConfig {
553    fn default() -> Self {
554        Self {
555            port: 6379,
556            bind: "127.0.0.1".into(),
557            protected_mode: false,
558            tcp_backlog: None,
559            unixsocket: None,
560            unixsocketperm: None,
561            timeout: None,
562            tcp_keepalive: None,
563            tls_port: None,
564            tls_cert_file: None,
565            tls_key_file: None,
566            tls_key_file_pass: None,
567            tls_ca_cert_file: None,
568            tls_ca_cert_dir: None,
569            tls_auth_clients: None,
570            tls_client_cert_file: None,
571            tls_client_key_file: None,
572            tls_client_key_file_pass: None,
573            tls_dh_params_file: None,
574            tls_ciphers: None,
575            tls_ciphersuites: None,
576            tls_protocols: None,
577            tls_prefer_server_ciphers: None,
578            tls_session_caching: None,
579            tls_session_cache_size: None,
580            tls_session_cache_timeout: None,
581            tls_replication: None,
582            tls_cluster: None,
583            daemonize: true,
584            dir: std::env::temp_dir().join("redis-server-wrapper"),
585            logfile: None,
586            loglevel: LogLevel::Notice,
587            databases: None,
588            maxmemory: None,
589            maxmemory_policy: None,
590            maxmemory_samples: None,
591            maxmemory_clients: None,
592            maxmemory_eviction_tenacity: None,
593            maxclients: None,
594            lfu_log_factor: None,
595            lfu_decay_time: None,
596            active_expire_effort: None,
597            lazyfree_lazy_eviction: None,
598            lazyfree_lazy_expire: None,
599            lazyfree_lazy_server_del: None,
600            lazyfree_lazy_user_del: None,
601            lazyfree_lazy_user_flush: None,
602            save: SavePolicy::Disabled,
603            appendonly: false,
604            appendfsync: None,
605            appendfilename: None,
606            appenddirname: None,
607            aof_use_rdb_preamble: None,
608            aof_load_truncated: None,
609            aof_load_corrupt_tail_max_size: None,
610            aof_rewrite_incremental_fsync: None,
611            aof_timestamp_enabled: None,
612            auto_aof_rewrite_percentage: None,
613            auto_aof_rewrite_min_size: None,
614            no_appendfsync_on_rewrite: None,
615            replicaof: None,
616            masterauth: None,
617            masteruser: None,
618            repl_backlog_size: None,
619            repl_backlog_ttl: None,
620            repl_disable_tcp_nodelay: None,
621            repl_diskless_load: None,
622            repl_diskless_sync: None,
623            repl_diskless_sync_delay: None,
624            repl_diskless_sync_max_replicas: None,
625            repl_ping_replica_period: None,
626            repl_timeout: None,
627            replica_announce_ip: None,
628            replica_announce_port: None,
629            replica_announced: None,
630            replica_full_sync_buffer_limit: None,
631            replica_ignore_disk_write_errors: None,
632            replica_ignore_maxmemory: None,
633            replica_lazy_flush: None,
634            replica_priority: None,
635            replica_read_only: None,
636            replica_serve_stale_data: None,
637            min_replicas_to_write: None,
638            min_replicas_max_lag: None,
639            password: None,
640            acl_file: None,
641            cluster_enabled: false,
642            cluster_node_timeout: None,
643            cluster_config_file: None,
644            cluster_require_full_coverage: None,
645            cluster_allow_reads_when_down: None,
646            cluster_allow_pubsubshard_when_down: None,
647            cluster_allow_replica_migration: None,
648            cluster_migration_barrier: None,
649            cluster_replica_no_failover: None,
650            cluster_replica_validity_factor: None,
651            cluster_announce_ip: None,
652            cluster_announce_port: None,
653            cluster_announce_bus_port: None,
654            cluster_announce_tls_port: None,
655            cluster_announce_hostname: None,
656            cluster_announce_human_nodename: None,
657            cluster_port: None,
658            cluster_preferred_endpoint_type: None,
659            cluster_link_sendbuf_limit: None,
660            cluster_compatibility_sample_ratio: None,
661            cluster_slot_migration_handoff_max_lag_bytes: None,
662            cluster_slot_migration_write_pause_timeout: None,
663            cluster_slot_stats_enabled: None,
664            hash_max_listpack_entries: None,
665            hash_max_listpack_value: None,
666            list_max_listpack_size: None,
667            list_compress_depth: None,
668            set_max_intset_entries: None,
669            set_max_listpack_entries: None,
670            set_max_listpack_value: None,
671            zset_max_listpack_entries: None,
672            zset_max_listpack_value: None,
673            hll_sparse_max_bytes: None,
674            stream_node_max_bytes: None,
675            stream_node_max_entries: None,
676            stream_idmp_duration: None,
677            stream_idmp_maxsize: None,
678            loadmodule: Vec::new(),
679            hz: None,
680            io_threads: None,
681            io_threads_do_reads: None,
682            notify_keyspace_events: None,
683            slowlog_log_slower_than: None,
684            slowlog_max_len: None,
685            latency_monitor_threshold: None,
686            latency_tracking: None,
687            latency_tracking_info_percentiles: None,
688            activedefrag: None,
689            active_defrag_ignore_bytes: None,
690            active_defrag_threshold_lower: None,
691            active_defrag_threshold_upper: None,
692            active_defrag_cycle_min: None,
693            active_defrag_cycle_max: None,
694            active_defrag_max_scan_fields: None,
695            syslog_enabled: None,
696            syslog_ident: None,
697            syslog_facility: None,
698            supervised: None,
699            always_show_logo: None,
700            set_proc_title: None,
701            proc_title_template: None,
702            acl_pubsub_default: None,
703            acllog_max_len: None,
704            enable_debug_command: None,
705            enable_module_command: None,
706            enable_protected_configs: None,
707            rename_command: Vec::new(),
708            sanitize_dump_payload: None,
709            hide_user_data_from_log: None,
710            bind_source_addr: None,
711            busy_reply_threshold: None,
712            client_output_buffer_limit: Vec::new(),
713            client_query_buffer_limit: None,
714            proto_max_bulk_len: None,
715            max_new_connections_per_cycle: None,
716            max_new_tls_connections_per_cycle: None,
717            socket_mark_id: None,
718            dbfilename: None,
719            rdbcompression: None,
720            rdbchecksum: None,
721            rdb_save_incremental_fsync: None,
722            rdb_del_sync_files: None,
723            stop_writes_on_bgsave_error: None,
724            shutdown_on_sigint: None,
725            shutdown_on_sigterm: None,
726            shutdown_timeout: None,
727            activerehashing: None,
728            crash_log_enabled: None,
729            crash_memcheck_enabled: None,
730            disable_thp: None,
731            dynamic_hz: None,
732            ignore_warnings: None,
733            include: Vec::new(),
734            jemalloc_bg_thread: None,
735            locale_collate: None,
736            lua_time_limit: None,
737            oom_score_adj: None,
738            oom_score_adj_values: None,
739            propagation_error_behavior: None,
740            tracking_table_max_keys: None,
741            extra: HashMap::new(),
742            redis_server_bin: crate::stack::detect_server_bin(),
743            redis_cli_bin: "redis-cli".into(),
744            no_stack_modules: false,
745        }
746    }
747}
748
749/// Builder for a Redis server.
750pub struct RedisServer {
751    config: RedisServerConfig,
752}
753
754impl RedisServer {
755    /// Create a new builder with default settings.
756    pub fn new() -> Self {
757        Self {
758            config: RedisServerConfig::default(),
759        }
760    }
761
762    // -- network --
763
764    /// Set the listening port (default: 6379).
765    pub fn port(mut self, port: u16) -> Self {
766        self.config.port = port;
767        self
768    }
769
770    /// Set the bind address (default: `127.0.0.1`).
771    pub fn bind(mut self, bind: impl Into<String>) -> Self {
772        self.config.bind = bind.into();
773        self
774    }
775
776    /// Enable or disable protected mode (default: off).
777    pub fn protected_mode(mut self, protected: bool) -> Self {
778        self.config.protected_mode = protected;
779        self
780    }
781
782    /// Set the TCP backlog queue length.
783    pub fn tcp_backlog(mut self, backlog: u32) -> Self {
784        self.config.tcp_backlog = Some(backlog);
785        self
786    }
787
788    /// Set a Unix socket path for connections.
789    pub fn unixsocket(mut self, path: impl Into<PathBuf>) -> Self {
790        self.config.unixsocket = Some(path.into());
791        self
792    }
793
794    /// Set Unix socket permissions (e.g. `700`).
795    pub fn unixsocketperm(mut self, perm: u32) -> Self {
796        self.config.unixsocketperm = Some(perm);
797        self
798    }
799
800    /// Close idle client connections after this many seconds (0 = disabled).
801    pub fn timeout(mut self, seconds: u32) -> Self {
802        self.config.timeout = Some(seconds);
803        self
804    }
805
806    /// Set TCP keepalive interval in seconds.
807    pub fn tcp_keepalive(mut self, seconds: u32) -> Self {
808        self.config.tcp_keepalive = Some(seconds);
809        self
810    }
811
812    // -- tls --
813
814    /// Set TLS listening port.
815    pub fn tls_port(mut self, port: u16) -> Self {
816        self.config.tls_port = Some(port);
817        self
818    }
819
820    /// Set the TLS certificate file path.
821    pub fn tls_cert_file(mut self, path: impl Into<PathBuf>) -> Self {
822        self.config.tls_cert_file = Some(path.into());
823        self
824    }
825
826    /// Set the TLS private key file path.
827    pub fn tls_key_file(mut self, path: impl Into<PathBuf>) -> Self {
828        self.config.tls_key_file = Some(path.into());
829        self
830    }
831
832    /// Set the TLS CA certificate file path.
833    pub fn tls_ca_cert_file(mut self, path: impl Into<PathBuf>) -> Self {
834        self.config.tls_ca_cert_file = Some(path.into());
835        self
836    }
837
838    /// Require TLS client authentication.
839    pub fn tls_auth_clients(mut self, require: bool) -> Self {
840        self.config.tls_auth_clients = Some(require);
841        self
842    }
843
844    /// Set the passphrase for the TLS private key file.
845    pub fn tls_key_file_pass(mut self, pass: impl Into<String>) -> Self {
846        self.config.tls_key_file_pass = Some(pass.into());
847        self
848    }
849
850    /// Set the TLS CA certificate directory path.
851    pub fn tls_ca_cert_dir(mut self, path: impl Into<PathBuf>) -> Self {
852        self.config.tls_ca_cert_dir = Some(path.into());
853        self
854    }
855
856    /// Set the TLS client certificate file path (for outgoing connections).
857    pub fn tls_client_cert_file(mut self, path: impl Into<PathBuf>) -> Self {
858        self.config.tls_client_cert_file = Some(path.into());
859        self
860    }
861
862    /// Set the TLS client private key file path (for outgoing connections).
863    pub fn tls_client_key_file(mut self, path: impl Into<PathBuf>) -> Self {
864        self.config.tls_client_key_file = Some(path.into());
865        self
866    }
867
868    /// Set the passphrase for the TLS client private key file.
869    pub fn tls_client_key_file_pass(mut self, pass: impl Into<String>) -> Self {
870        self.config.tls_client_key_file_pass = Some(pass.into());
871        self
872    }
873
874    /// Set the DH parameters file path for DHE ciphers.
875    pub fn tls_dh_params_file(mut self, path: impl Into<PathBuf>) -> Self {
876        self.config.tls_dh_params_file = Some(path.into());
877        self
878    }
879
880    /// Set the allowed TLS 1.2 ciphers (OpenSSL cipher list format).
881    pub fn tls_ciphers(mut self, ciphers: impl Into<String>) -> Self {
882        self.config.tls_ciphers = Some(ciphers.into());
883        self
884    }
885
886    /// Set the allowed TLS 1.3 ciphersuites (colon-separated).
887    pub fn tls_ciphersuites(mut self, suites: impl Into<String>) -> Self {
888        self.config.tls_ciphersuites = Some(suites.into());
889        self
890    }
891
892    /// Set the allowed TLS protocol versions (e.g. `"TLSv1.2 TLSv1.3"`).
893    pub fn tls_protocols(mut self, protocols: impl Into<String>) -> Self {
894        self.config.tls_protocols = Some(protocols.into());
895        self
896    }
897
898    /// Prefer the server's cipher order over the client's.
899    pub fn tls_prefer_server_ciphers(mut self, prefer: bool) -> Self {
900        self.config.tls_prefer_server_ciphers = Some(prefer);
901        self
902    }
903
904    /// Enable or disable TLS session caching.
905    pub fn tls_session_caching(mut self, enable: bool) -> Self {
906        self.config.tls_session_caching = Some(enable);
907        self
908    }
909
910    /// Set the number of entries in the TLS session cache.
911    pub fn tls_session_cache_size(mut self, size: u32) -> Self {
912        self.config.tls_session_cache_size = Some(size);
913        self
914    }
915
916    /// Set the timeout in seconds for cached TLS sessions.
917    pub fn tls_session_cache_timeout(mut self, seconds: u32) -> Self {
918        self.config.tls_session_cache_timeout = Some(seconds);
919        self
920    }
921
922    /// Enable TLS for replication traffic.
923    pub fn tls_replication(mut self, enable: bool) -> Self {
924        self.config.tls_replication = Some(enable);
925        self
926    }
927
928    /// Enable TLS for cluster bus communication.
929    pub fn tls_cluster(mut self, enable: bool) -> Self {
930        self.config.tls_cluster = Some(enable);
931        self
932    }
933
934    // -- general --
935
936    /// Set the working directory for data files.
937    pub fn dir(mut self, dir: impl Into<PathBuf>) -> Self {
938        self.config.dir = dir.into();
939        self
940    }
941
942    /// Set the log level (default: [`LogLevel::Notice`]).
943    pub fn loglevel(mut self, level: LogLevel) -> Self {
944        self.config.loglevel = level;
945        self
946    }
947
948    /// Set the log file path. Defaults to `redis.log` inside the node directory.
949    pub fn logfile(mut self, path: impl Into<String>) -> Self {
950        self.config.logfile = Some(path.into());
951        self
952    }
953
954    /// Set the number of databases (default: 16).
955    pub fn databases(mut self, n: u32) -> Self {
956        self.config.databases = Some(n);
957        self
958    }
959
960    // -- memory --
961
962    /// Set the maximum memory limit (e.g. `"256mb"`, `"1gb"`).
963    pub fn maxmemory(mut self, limit: impl Into<String>) -> Self {
964        self.config.maxmemory = Some(limit.into());
965        self
966    }
967
968    /// Set the eviction policy when maxmemory is reached.
969    pub fn maxmemory_policy(mut self, policy: impl Into<String>) -> Self {
970        self.config.maxmemory_policy = Some(policy.into());
971        self
972    }
973
974    /// Set the number of keys sampled per eviction round (Redis default: 5).
975    pub fn maxmemory_samples(mut self, n: u32) -> Self {
976        self.config.maxmemory_samples = Some(n);
977        self
978    }
979
980    /// Set per-client memory limit (e.g. `"0"` to disable).
981    pub fn maxmemory_clients(mut self, limit: impl Into<String>) -> Self {
982        self.config.maxmemory_clients = Some(limit.into());
983        self
984    }
985
986    /// Set eviction processing effort (1-100, Redis default: 10).
987    pub fn maxmemory_eviction_tenacity(mut self, tenacity: u32) -> Self {
988        self.config.maxmemory_eviction_tenacity = Some(tenacity);
989        self
990    }
991
992    /// Set the maximum number of simultaneous client connections.
993    pub fn maxclients(mut self, n: u32) -> Self {
994        self.config.maxclients = Some(n);
995        self
996    }
997
998    /// Set the logarithmic factor for the LFU frequency counter (Redis default: 10).
999    pub fn lfu_log_factor(mut self, factor: u32) -> Self {
1000        self.config.lfu_log_factor = Some(factor);
1001        self
1002    }
1003
1004    /// Set the LFU counter decay time in minutes (Redis default: 1).
1005    pub fn lfu_decay_time(mut self, minutes: u32) -> Self {
1006        self.config.lfu_decay_time = Some(minutes);
1007        self
1008    }
1009
1010    /// Set the effort spent on active key expiration (1-100, Redis default: 10).
1011    pub fn active_expire_effort(mut self, effort: u32) -> Self {
1012        self.config.active_expire_effort = Some(effort);
1013        self
1014    }
1015
1016    // -- lazyfree --
1017
1018    /// Enable or disable background deletion during eviction.
1019    pub fn lazyfree_lazy_eviction(mut self, enable: bool) -> Self {
1020        self.config.lazyfree_lazy_eviction = Some(enable);
1021        self
1022    }
1023
1024    /// Enable or disable background deletion of expired keys.
1025    pub fn lazyfree_lazy_expire(mut self, enable: bool) -> Self {
1026        self.config.lazyfree_lazy_expire = Some(enable);
1027        self
1028    }
1029
1030    /// Enable or disable background deletion for implicit `DEL` (e.g. `RENAME`).
1031    pub fn lazyfree_lazy_server_del(mut self, enable: bool) -> Self {
1032        self.config.lazyfree_lazy_server_del = Some(enable);
1033        self
1034    }
1035
1036    /// Make explicit `DEL` behave like `UNLINK` (background deletion).
1037    pub fn lazyfree_lazy_user_del(mut self, enable: bool) -> Self {
1038        self.config.lazyfree_lazy_user_del = Some(enable);
1039        self
1040    }
1041
1042    /// Make `FLUSHDB`/`FLUSHALL` default to `ASYNC`.
1043    pub fn lazyfree_lazy_user_flush(mut self, enable: bool) -> Self {
1044        self.config.lazyfree_lazy_user_flush = Some(enable);
1045        self
1046    }
1047
1048    // -- persistence --
1049
1050    /// Enable or disable RDB snapshots (default: off).
1051    ///
1052    /// `true` omits the `save` directive (Redis built-in defaults apply).
1053    /// `false` emits `save ""` to disable RDB entirely.
1054    pub fn save(mut self, save: bool) -> Self {
1055        self.config.save = if save {
1056            SavePolicy::Default
1057        } else {
1058            SavePolicy::Disabled
1059        };
1060        self
1061    }
1062
1063    /// Set a custom RDB save schedule.
1064    ///
1065    /// Each `(seconds, changes)` pair emits a `save <seconds> <changes>` line.
1066    pub fn save_schedule(mut self, schedule: Vec<(u64, u64)>) -> Self {
1067        self.config.save = SavePolicy::Custom(schedule);
1068        self
1069    }
1070
1071    /// Enable or disable AOF persistence.
1072    pub fn appendonly(mut self, appendonly: bool) -> Self {
1073        self.config.appendonly = appendonly;
1074        self
1075    }
1076
1077    /// Set the AOF fsync policy.
1078    pub fn appendfsync(mut self, policy: AppendFsync) -> Self {
1079        self.config.appendfsync = Some(policy);
1080        self
1081    }
1082
1083    /// Set the AOF filename.
1084    pub fn appendfilename(mut self, name: impl Into<String>) -> Self {
1085        self.config.appendfilename = Some(name.into());
1086        self
1087    }
1088
1089    /// Set the AOF directory name.
1090    pub fn appenddirname(mut self, name: impl Into<PathBuf>) -> Self {
1091        self.config.appenddirname = Some(name.into());
1092        self
1093    }
1094
1095    /// Enable or disable the RDB preamble in AOF files.
1096    pub fn aof_use_rdb_preamble(mut self, enable: bool) -> Self {
1097        self.config.aof_use_rdb_preamble = Some(enable);
1098        self
1099    }
1100
1101    /// Control whether truncated AOF files are loaded.
1102    pub fn aof_load_truncated(mut self, enable: bool) -> Self {
1103        self.config.aof_load_truncated = Some(enable);
1104        self
1105    }
1106
1107    /// Set the maximum allowed size of a corrupt AOF tail (e.g. `"32mb"`).
1108    pub fn aof_load_corrupt_tail_max_size(mut self, size: impl Into<String>) -> Self {
1109        self.config.aof_load_corrupt_tail_max_size = Some(size.into());
1110        self
1111    }
1112
1113    /// Enable or disable incremental fsync during AOF rewrites.
1114    pub fn aof_rewrite_incremental_fsync(mut self, enable: bool) -> Self {
1115        self.config.aof_rewrite_incremental_fsync = Some(enable);
1116        self
1117    }
1118
1119    /// Enable or disable timestamps in the AOF file.
1120    pub fn aof_timestamp_enabled(mut self, enable: bool) -> Self {
1121        self.config.aof_timestamp_enabled = Some(enable);
1122        self
1123    }
1124
1125    /// Set the percentage growth that triggers an automatic AOF rewrite.
1126    pub fn auto_aof_rewrite_percentage(mut self, pct: u32) -> Self {
1127        self.config.auto_aof_rewrite_percentage = Some(pct);
1128        self
1129    }
1130
1131    /// Set the minimum AOF size before an automatic rewrite is triggered (e.g. `"64mb"`).
1132    pub fn auto_aof_rewrite_min_size(mut self, size: impl Into<String>) -> Self {
1133        self.config.auto_aof_rewrite_min_size = Some(size.into());
1134        self
1135    }
1136
1137    /// Control whether fsync is suppressed during AOF rewrites.
1138    pub fn no_appendfsync_on_rewrite(mut self, enable: bool) -> Self {
1139        self.config.no_appendfsync_on_rewrite = Some(enable);
1140        self
1141    }
1142
1143    // -- replication --
1144
1145    /// Configure this server as a replica of the given master.
1146    pub fn replicaof(mut self, host: impl Into<String>, port: u16) -> Self {
1147        self.config.replicaof = Some((host.into(), port));
1148        self
1149    }
1150
1151    /// Set the password for authenticating with a master.
1152    pub fn masterauth(mut self, password: impl Into<String>) -> Self {
1153        self.config.masterauth = Some(password.into());
1154        self
1155    }
1156
1157    /// Set the username for authenticating with a master (ACL-based auth).
1158    pub fn masteruser(mut self, user: impl Into<String>) -> Self {
1159        self.config.masteruser = Some(user.into());
1160        self
1161    }
1162
1163    /// Set the replication backlog size (e.g. `"1mb"`).
1164    pub fn repl_backlog_size(mut self, size: impl Into<String>) -> Self {
1165        self.config.repl_backlog_size = Some(size.into());
1166        self
1167    }
1168
1169    /// Set seconds before the backlog is freed when no replicas are connected.
1170    pub fn repl_backlog_ttl(mut self, seconds: u32) -> Self {
1171        self.config.repl_backlog_ttl = Some(seconds);
1172        self
1173    }
1174
1175    /// Disable TCP_NODELAY on the replication socket.
1176    pub fn repl_disable_tcp_nodelay(mut self, disable: bool) -> Self {
1177        self.config.repl_disable_tcp_nodelay = Some(disable);
1178        self
1179    }
1180
1181    /// Set the diskless load policy for replicas.
1182    pub fn repl_diskless_load(mut self, policy: ReplDisklessLoad) -> Self {
1183        self.config.repl_diskless_load = Some(policy);
1184        self
1185    }
1186
1187    /// Enable or disable diskless sync from master to replicas.
1188    pub fn repl_diskless_sync(mut self, enable: bool) -> Self {
1189        self.config.repl_diskless_sync = Some(enable);
1190        self
1191    }
1192
1193    /// Set the delay in seconds before starting a diskless sync.
1194    pub fn repl_diskless_sync_delay(mut self, seconds: u32) -> Self {
1195        self.config.repl_diskless_sync_delay = Some(seconds);
1196        self
1197    }
1198
1199    /// Set the maximum number of replicas to wait for before starting a diskless sync.
1200    pub fn repl_diskless_sync_max_replicas(mut self, n: u32) -> Self {
1201        self.config.repl_diskless_sync_max_replicas = Some(n);
1202        self
1203    }
1204
1205    /// Set the interval in seconds between PING commands sent to the master.
1206    pub fn repl_ping_replica_period(mut self, seconds: u32) -> Self {
1207        self.config.repl_ping_replica_period = Some(seconds);
1208        self
1209    }
1210
1211    /// Set the replication timeout in seconds.
1212    pub fn repl_timeout(mut self, seconds: u32) -> Self {
1213        self.config.repl_timeout = Some(seconds);
1214        self
1215    }
1216
1217    /// Set the IP address a replica announces to the master.
1218    pub fn replica_announce_ip(mut self, ip: impl Into<String>) -> Self {
1219        self.config.replica_announce_ip = Some(ip.into());
1220        self
1221    }
1222
1223    /// Set the port a replica announces to the master.
1224    pub fn replica_announce_port(mut self, port: u16) -> Self {
1225        self.config.replica_announce_port = Some(port);
1226        self
1227    }
1228
1229    /// Control whether the replica is announced to clients.
1230    pub fn replica_announced(mut self, announced: bool) -> Self {
1231        self.config.replica_announced = Some(announced);
1232        self
1233    }
1234
1235    /// Set the buffer limit for full synchronization on replicas (e.g. `"256mb"`).
1236    pub fn replica_full_sync_buffer_limit(mut self, size: impl Into<String>) -> Self {
1237        self.config.replica_full_sync_buffer_limit = Some(size.into());
1238        self
1239    }
1240
1241    /// Control whether replicas ignore disk-write errors.
1242    pub fn replica_ignore_disk_write_errors(mut self, ignore: bool) -> Self {
1243        self.config.replica_ignore_disk_write_errors = Some(ignore);
1244        self
1245    }
1246
1247    /// Control whether replicas ignore the maxmemory setting.
1248    pub fn replica_ignore_maxmemory(mut self, ignore: bool) -> Self {
1249        self.config.replica_ignore_maxmemory = Some(ignore);
1250        self
1251    }
1252
1253    /// Enable or disable lazy flush on replicas during full sync.
1254    pub fn replica_lazy_flush(mut self, enable: bool) -> Self {
1255        self.config.replica_lazy_flush = Some(enable);
1256        self
1257    }
1258
1259    /// Set the replica priority for Sentinel promotion.
1260    pub fn replica_priority(mut self, priority: u32) -> Self {
1261        self.config.replica_priority = Some(priority);
1262        self
1263    }
1264
1265    /// Control whether the replica is read-only.
1266    pub fn replica_read_only(mut self, read_only: bool) -> Self {
1267        self.config.replica_read_only = Some(read_only);
1268        self
1269    }
1270
1271    /// Control whether the replica serves stale data while syncing.
1272    pub fn replica_serve_stale_data(mut self, serve: bool) -> Self {
1273        self.config.replica_serve_stale_data = Some(serve);
1274        self
1275    }
1276
1277    /// Set the minimum number of replicas that must acknowledge writes.
1278    pub fn min_replicas_to_write(mut self, n: u32) -> Self {
1279        self.config.min_replicas_to_write = Some(n);
1280        self
1281    }
1282
1283    /// Set the maximum replication lag (in seconds) for a replica to count toward `min-replicas-to-write`.
1284    pub fn min_replicas_max_lag(mut self, seconds: u32) -> Self {
1285        self.config.min_replicas_max_lag = Some(seconds);
1286        self
1287    }
1288
1289    // -- security --
1290
1291    /// Set a `requirepass` password for client connections.
1292    pub fn password(mut self, password: impl Into<String>) -> Self {
1293        self.config.password = Some(password.into());
1294        self
1295    }
1296
1297    /// Set the path to an ACL file.
1298    pub fn acl_file(mut self, path: impl Into<PathBuf>) -> Self {
1299        self.config.acl_file = Some(path.into());
1300        self
1301    }
1302
1303    // -- cluster --
1304
1305    /// Enable Redis Cluster mode.
1306    pub fn cluster_enabled(mut self, enabled: bool) -> Self {
1307        self.config.cluster_enabled = enabled;
1308        self
1309    }
1310
1311    /// Set the cluster node timeout in milliseconds.
1312    pub fn cluster_node_timeout(mut self, ms: u64) -> Self {
1313        self.config.cluster_node_timeout = Some(ms);
1314        self
1315    }
1316
1317    /// Set a custom cluster config file path (overrides auto-generated default).
1318    pub fn cluster_config_file(mut self, path: impl Into<PathBuf>) -> Self {
1319        self.config.cluster_config_file = Some(path.into());
1320        self
1321    }
1322
1323    /// Require full hash slot coverage for the cluster to accept writes.
1324    pub fn cluster_require_full_coverage(mut self, require: bool) -> Self {
1325        self.config.cluster_require_full_coverage = Some(require);
1326        self
1327    }
1328
1329    /// Allow reads when the cluster is down.
1330    pub fn cluster_allow_reads_when_down(mut self, allow: bool) -> Self {
1331        self.config.cluster_allow_reads_when_down = Some(allow);
1332        self
1333    }
1334
1335    /// Allow pubsub shard channels when the cluster is down.
1336    pub fn cluster_allow_pubsubshard_when_down(mut self, allow: bool) -> Self {
1337        self.config.cluster_allow_pubsubshard_when_down = Some(allow);
1338        self
1339    }
1340
1341    /// Allow automatic replica migration between masters.
1342    pub fn cluster_allow_replica_migration(mut self, allow: bool) -> Self {
1343        self.config.cluster_allow_replica_migration = Some(allow);
1344        self
1345    }
1346
1347    /// Set the minimum number of replicas a master must retain before one can migrate.
1348    pub fn cluster_migration_barrier(mut self, barrier: u32) -> Self {
1349        self.config.cluster_migration_barrier = Some(barrier);
1350        self
1351    }
1352
1353    /// Prevent this replica from ever attempting a failover.
1354    pub fn cluster_replica_no_failover(mut self, no_failover: bool) -> Self {
1355        self.config.cluster_replica_no_failover = Some(no_failover);
1356        self
1357    }
1358
1359    /// Set the replica validity factor (multiplied by node timeout).
1360    pub fn cluster_replica_validity_factor(mut self, factor: u32) -> Self {
1361        self.config.cluster_replica_validity_factor = Some(factor);
1362        self
1363    }
1364
1365    /// Set the IP address this node announces to the cluster bus.
1366    pub fn cluster_announce_ip(mut self, ip: impl Into<String>) -> Self {
1367        self.config.cluster_announce_ip = Some(ip.into());
1368        self
1369    }
1370
1371    /// Set the client port this node announces to the cluster.
1372    pub fn cluster_announce_port(mut self, port: u16) -> Self {
1373        self.config.cluster_announce_port = Some(port);
1374        self
1375    }
1376
1377    /// Set the cluster bus port this node announces.
1378    pub fn cluster_announce_bus_port(mut self, port: u16) -> Self {
1379        self.config.cluster_announce_bus_port = Some(port);
1380        self
1381    }
1382
1383    /// Set the TLS port this node announces to the cluster.
1384    pub fn cluster_announce_tls_port(mut self, port: u16) -> Self {
1385        self.config.cluster_announce_tls_port = Some(port);
1386        self
1387    }
1388
1389    /// Set the hostname this node announces to the cluster.
1390    pub fn cluster_announce_hostname(mut self, hostname: impl Into<String>) -> Self {
1391        self.config.cluster_announce_hostname = Some(hostname.into());
1392        self
1393    }
1394
1395    /// Set the human-readable node name announced to the cluster.
1396    pub fn cluster_announce_human_nodename(mut self, name: impl Into<String>) -> Self {
1397        self.config.cluster_announce_human_nodename = Some(name.into());
1398        self
1399    }
1400
1401    /// Set the dedicated cluster bus port (0 = auto with +10000 offset).
1402    pub fn cluster_port(mut self, port: u16) -> Self {
1403        self.config.cluster_port = Some(port);
1404        self
1405    }
1406
1407    /// Set the preferred endpoint type for cluster redirections (e.g. `"ip"`, `"hostname"`).
1408    pub fn cluster_preferred_endpoint_type(mut self, endpoint_type: impl Into<String>) -> Self {
1409        self.config.cluster_preferred_endpoint_type = Some(endpoint_type.into());
1410        self
1411    }
1412
1413    /// Set the send buffer limit in bytes for cluster bus links.
1414    pub fn cluster_link_sendbuf_limit(mut self, limit: u64) -> Self {
1415        self.config.cluster_link_sendbuf_limit = Some(limit);
1416        self
1417    }
1418
1419    /// Set the compatibility sample ratio percentage.
1420    pub fn cluster_compatibility_sample_ratio(mut self, ratio: u32) -> Self {
1421        self.config.cluster_compatibility_sample_ratio = Some(ratio);
1422        self
1423    }
1424
1425    /// Set the maximum lag in bytes before slot migration handoff.
1426    pub fn cluster_slot_migration_handoff_max_lag_bytes(mut self, bytes: u64) -> Self {
1427        self.config.cluster_slot_migration_handoff_max_lag_bytes = Some(bytes);
1428        self
1429    }
1430
1431    /// Set the write pause timeout in milliseconds during slot migration.
1432    pub fn cluster_slot_migration_write_pause_timeout(mut self, ms: u64) -> Self {
1433        self.config.cluster_slot_migration_write_pause_timeout = Some(ms);
1434        self
1435    }
1436
1437    /// Enable per-slot statistics collection.
1438    pub fn cluster_slot_stats_enabled(mut self, enable: bool) -> Self {
1439        self.config.cluster_slot_stats_enabled = Some(enable);
1440        self
1441    }
1442
1443    // -- data structures --
1444
1445    /// Set the maximum number of entries in a hash before converting from listpack to hash table.
1446    pub fn hash_max_listpack_entries(mut self, n: u32) -> Self {
1447        self.config.hash_max_listpack_entries = Some(n);
1448        self
1449    }
1450
1451    /// Set the maximum size of a hash entry value before converting from listpack to hash table.
1452    pub fn hash_max_listpack_value(mut self, n: u32) -> Self {
1453        self.config.hash_max_listpack_value = Some(n);
1454        self
1455    }
1456
1457    /// Set the maximum listpack size for list entries.
1458    ///
1459    /// Positive values limit the number of elements per listpack node.
1460    /// Negative values set a byte-size limit: -1 = 4KB, -2 = 8KB, -3 = 16KB, -4 = 32KB, -5 = 64KB.
1461    pub fn list_max_listpack_size(mut self, n: i32) -> Self {
1462        self.config.list_max_listpack_size = Some(n);
1463        self
1464    }
1465
1466    /// Set the number of quicklist nodes at each end of the list that are not compressed.
1467    ///
1468    /// `0` disables compression. `1` means the head and tail are uncompressed, etc.
1469    pub fn list_compress_depth(mut self, n: u32) -> Self {
1470        self.config.list_compress_depth = Some(n);
1471        self
1472    }
1473
1474    /// Set the maximum number of integer entries in a set before converting from intset to hash table.
1475    pub fn set_max_intset_entries(mut self, n: u32) -> Self {
1476        self.config.set_max_intset_entries = Some(n);
1477        self
1478    }
1479
1480    /// Set the maximum number of entries in a set before converting from listpack to hash table.
1481    pub fn set_max_listpack_entries(mut self, n: u32) -> Self {
1482        self.config.set_max_listpack_entries = Some(n);
1483        self
1484    }
1485
1486    /// Set the maximum size of a set entry value before converting from listpack to hash table.
1487    pub fn set_max_listpack_value(mut self, n: u32) -> Self {
1488        self.config.set_max_listpack_value = Some(n);
1489        self
1490    }
1491
1492    /// Set the maximum number of entries in a sorted set before converting from listpack to skiplist.
1493    pub fn zset_max_listpack_entries(mut self, n: u32) -> Self {
1494        self.config.zset_max_listpack_entries = Some(n);
1495        self
1496    }
1497
1498    /// Set the maximum size of a sorted set entry value before converting from listpack to skiplist.
1499    pub fn zset_max_listpack_value(mut self, n: u32) -> Self {
1500        self.config.zset_max_listpack_value = Some(n);
1501        self
1502    }
1503
1504    /// Set the maximum number of bytes for the sparse representation of a HyperLogLog.
1505    pub fn hll_sparse_max_bytes(mut self, n: u32) -> Self {
1506        self.config.hll_sparse_max_bytes = Some(n);
1507        self
1508    }
1509
1510    /// Set the maximum number of bytes in a single stream listpack node.
1511    pub fn stream_node_max_bytes(mut self, n: u32) -> Self {
1512        self.config.stream_node_max_bytes = Some(n);
1513        self
1514    }
1515
1516    /// Set the maximum number of entries in a single stream listpack node.
1517    pub fn stream_node_max_entries(mut self, n: u32) -> Self {
1518        self.config.stream_node_max_entries = Some(n);
1519        self
1520    }
1521
1522    /// Set the duration in milliseconds for stream ID de-duplication.
1523    pub fn stream_idmp_duration(mut self, ms: u64) -> Self {
1524        self.config.stream_idmp_duration = Some(ms);
1525        self
1526    }
1527
1528    /// Set the maximum number of entries tracked for stream ID de-duplication.
1529    pub fn stream_idmp_maxsize(mut self, n: u64) -> Self {
1530        self.config.stream_idmp_maxsize = Some(n);
1531        self
1532    }
1533
1534    // -- modules --
1535
1536    /// Load a Redis module at startup.
1537    pub fn loadmodule(mut self, path: impl Into<PathBuf>) -> Self {
1538        self.config.loadmodule.push((path.into(), Vec::new()));
1539        self
1540    }
1541
1542    /// Load a Redis module at startup with load-time arguments.
1543    ///
1544    /// Renders `loadmodule "<path>" arg1 arg2 ...`. The path is quoted; the
1545    /// arguments are appended unquoted and space-separated, matching how Redis
1546    /// parses module arguments.
1547    pub fn loadmodule_with_args(
1548        mut self,
1549        path: impl Into<PathBuf>,
1550        args: impl IntoIterator<Item = impl Into<String>>,
1551    ) -> Self {
1552        self.config
1553            .loadmodule
1554            .push((path.into(), args.into_iter().map(Into::into).collect()));
1555        self
1556    }
1557
1558    // -- advanced --
1559
1560    /// Set the server tick frequency in Hz (default: 10).
1561    pub fn hz(mut self, hz: u32) -> Self {
1562        self.config.hz = Some(hz);
1563        self
1564    }
1565
1566    /// Set the number of I/O threads.
1567    pub fn io_threads(mut self, n: u32) -> Self {
1568        self.config.io_threads = Some(n);
1569        self
1570    }
1571
1572    /// Enable I/O threads for reads as well as writes.
1573    pub fn io_threads_do_reads(mut self, enable: bool) -> Self {
1574        self.config.io_threads_do_reads = Some(enable);
1575        self
1576    }
1577
1578    /// Set keyspace notification events (e.g. `"KEA"`).
1579    pub fn notify_keyspace_events(mut self, events: impl Into<String>) -> Self {
1580        self.config.notify_keyspace_events = Some(events.into());
1581        self
1582    }
1583
1584    // -- slow log --
1585
1586    /// Set the slow log threshold in microseconds (`0` = log everything, `-1` = disabled).
1587    pub fn slowlog_log_slower_than(mut self, us: i64) -> Self {
1588        self.config.slowlog_log_slower_than = Some(us);
1589        self
1590    }
1591
1592    /// Set the maximum number of slow log entries.
1593    pub fn slowlog_max_len(mut self, n: u32) -> Self {
1594        self.config.slowlog_max_len = Some(n);
1595        self
1596    }
1597
1598    // -- latency tracking --
1599
1600    /// Set the latency monitor threshold in milliseconds (`0` = disabled).
1601    pub fn latency_monitor_threshold(mut self, ms: u64) -> Self {
1602        self.config.latency_monitor_threshold = Some(ms);
1603        self
1604    }
1605
1606    /// Enable or disable the extended latency tracking system.
1607    pub fn latency_tracking(mut self, enable: bool) -> Self {
1608        self.config.latency_tracking = Some(enable);
1609        self
1610    }
1611
1612    /// Set percentiles reported by the latency tracking system (e.g. `"50 99 99.9"`).
1613    pub fn latency_tracking_info_percentiles(mut self, percentiles: impl Into<String>) -> Self {
1614        self.config.latency_tracking_info_percentiles = Some(percentiles.into());
1615        self
1616    }
1617
1618    // -- active defragmentation --
1619
1620    /// Enable or disable active defragmentation.
1621    pub fn activedefrag(mut self, enable: bool) -> Self {
1622        self.config.activedefrag = Some(enable);
1623        self
1624    }
1625
1626    /// Set the minimum fragmentation waste to start defragmentation (e.g. `"100mb"`).
1627    pub fn active_defrag_ignore_bytes(mut self, bytes: impl Into<String>) -> Self {
1628        self.config.active_defrag_ignore_bytes = Some(bytes.into());
1629        self
1630    }
1631
1632    /// Set the minimum fragmentation percentage to start defragmentation.
1633    pub fn active_defrag_threshold_lower(mut self, pct: u32) -> Self {
1634        self.config.active_defrag_threshold_lower = Some(pct);
1635        self
1636    }
1637
1638    /// Set the fragmentation percentage at which maximum effort is used.
1639    pub fn active_defrag_threshold_upper(mut self, pct: u32) -> Self {
1640        self.config.active_defrag_threshold_upper = Some(pct);
1641        self
1642    }
1643
1644    /// Set the minimal CPU effort for defragmentation (percentage).
1645    pub fn active_defrag_cycle_min(mut self, pct: u32) -> Self {
1646        self.config.active_defrag_cycle_min = Some(pct);
1647        self
1648    }
1649
1650    /// Set the maximum CPU effort for defragmentation (percentage).
1651    pub fn active_defrag_cycle_max(mut self, pct: u32) -> Self {
1652        self.config.active_defrag_cycle_max = Some(pct);
1653        self
1654    }
1655
1656    /// Set the maximum fields processed per defrag scan step.
1657    pub fn active_defrag_max_scan_fields(mut self, n: u32) -> Self {
1658        self.config.active_defrag_max_scan_fields = Some(n);
1659        self
1660    }
1661
1662    // -- logging and process --
1663
1664    /// Enable logging to syslog.
1665    pub fn syslog_enabled(mut self, enable: bool) -> Self {
1666        self.config.syslog_enabled = Some(enable);
1667        self
1668    }
1669
1670    /// Set the syslog identity string.
1671    pub fn syslog_ident(mut self, ident: impl Into<String>) -> Self {
1672        self.config.syslog_ident = Some(ident.into());
1673        self
1674    }
1675
1676    /// Set the syslog facility (e.g. `"local0"`).
1677    pub fn syslog_facility(mut self, facility: impl Into<String>) -> Self {
1678        self.config.syslog_facility = Some(facility.into());
1679        self
1680    }
1681
1682    /// Set the supervision mode (`"upstart"`, `"systemd"`, `"auto"`, or `"no"`).
1683    pub fn supervised(mut self, mode: impl Into<String>) -> Self {
1684        self.config.supervised = Some(mode.into());
1685        self
1686    }
1687
1688    /// Show the Redis logo on startup.
1689    pub fn always_show_logo(mut self, enable: bool) -> Self {
1690        self.config.always_show_logo = Some(enable);
1691        self
1692    }
1693
1694    /// Enable setting the process title.
1695    pub fn set_proc_title(mut self, enable: bool) -> Self {
1696        self.config.set_proc_title = Some(enable);
1697        self
1698    }
1699
1700    /// Set the process title template.
1701    pub fn proc_title_template(mut self, template: impl Into<String>) -> Self {
1702        self.config.proc_title_template = Some(template.into());
1703        self
1704    }
1705
1706    // -- security and ACL --
1707
1708    /// Set the default pub/sub ACL permissions (`"allchannels"` or `"resetchannels"`).
1709    pub fn acl_pubsub_default(mut self, default: impl Into<String>) -> Self {
1710        self.config.acl_pubsub_default = Some(default.into());
1711        self
1712    }
1713
1714    /// Set the maximum length of the ACL log.
1715    pub fn acllog_max_len(mut self, n: u32) -> Self {
1716        self.config.acllog_max_len = Some(n);
1717        self
1718    }
1719
1720    /// Enable the DEBUG command (`"yes"`, `"local"`, or `"no"`).
1721    pub fn enable_debug_command(mut self, mode: impl Into<String>) -> Self {
1722        self.config.enable_debug_command = Some(mode.into());
1723        self
1724    }
1725
1726    /// Enable the MODULE command (`"yes"`, `"local"`, or `"no"`).
1727    pub fn enable_module_command(mut self, mode: impl Into<String>) -> Self {
1728        self.config.enable_module_command = Some(mode.into());
1729        self
1730    }
1731
1732    /// Allow CONFIG SET to modify protected configs (`"yes"`, `"local"`, or `"no"`).
1733    pub fn enable_protected_configs(mut self, mode: impl Into<String>) -> Self {
1734        self.config.enable_protected_configs = Some(mode.into());
1735        self
1736    }
1737
1738    /// Rename a command. Pass an empty new name to disable the command entirely.
1739    pub fn rename_command(
1740        mut self,
1741        command: impl Into<String>,
1742        new_name: impl Into<String>,
1743    ) -> Self {
1744        self.config
1745            .rename_command
1746            .push((command.into(), new_name.into()));
1747        self
1748    }
1749
1750    /// Set dump payload sanitization mode (`"yes"`, `"no"`, or `"clients"`).
1751    pub fn sanitize_dump_payload(mut self, mode: impl Into<String>) -> Self {
1752        self.config.sanitize_dump_payload = Some(mode.into());
1753        self
1754    }
1755
1756    /// Hide user data from log messages.
1757    pub fn hide_user_data_from_log(mut self, enable: bool) -> Self {
1758        self.config.hide_user_data_from_log = Some(enable);
1759        self
1760    }
1761
1762    // -- networking (additional) --
1763
1764    /// Set the source address for outgoing connections.
1765    pub fn bind_source_addr(mut self, addr: impl Into<String>) -> Self {
1766        self.config.bind_source_addr = Some(addr.into());
1767        self
1768    }
1769
1770    /// Set the busy reply threshold in milliseconds.
1771    pub fn busy_reply_threshold(mut self, ms: u64) -> Self {
1772        self.config.busy_reply_threshold = Some(ms);
1773        self
1774    }
1775
1776    /// Add a client output buffer limit (e.g. `"normal 0 0 0"` or `"replica 256mb 64mb 60"`).
1777    pub fn client_output_buffer_limit(mut self, limit: impl Into<String>) -> Self {
1778        self.config.client_output_buffer_limit.push(limit.into());
1779        self
1780    }
1781
1782    /// Set the maximum size of a single client query buffer.
1783    pub fn client_query_buffer_limit(mut self, limit: impl Into<String>) -> Self {
1784        self.config.client_query_buffer_limit = Some(limit.into());
1785        self
1786    }
1787
1788    /// Set the maximum size of a single protocol bulk request.
1789    pub fn proto_max_bulk_len(mut self, len: impl Into<String>) -> Self {
1790        self.config.proto_max_bulk_len = Some(len.into());
1791        self
1792    }
1793
1794    /// Set the maximum number of new connections per event loop cycle.
1795    pub fn max_new_connections_per_cycle(mut self, n: u32) -> Self {
1796        self.config.max_new_connections_per_cycle = Some(n);
1797        self
1798    }
1799
1800    /// Set the maximum number of new TLS connections per event loop cycle.
1801    pub fn max_new_tls_connections_per_cycle(mut self, n: u32) -> Self {
1802        self.config.max_new_tls_connections_per_cycle = Some(n);
1803        self
1804    }
1805
1806    /// Set the socket mark ID for outgoing connections.
1807    pub fn socket_mark_id(mut self, id: u32) -> Self {
1808        self.config.socket_mark_id = Some(id);
1809        self
1810    }
1811
1812    // -- RDB (additional) --
1813
1814    /// Set the RDB dump filename.
1815    pub fn dbfilename(mut self, name: impl Into<String>) -> Self {
1816        self.config.dbfilename = Some(name.into());
1817        self
1818    }
1819
1820    /// Enable or disable RDB compression.
1821    pub fn rdbcompression(mut self, enable: bool) -> Self {
1822        self.config.rdbcompression = Some(enable);
1823        self
1824    }
1825
1826    /// Enable or disable RDB checksum.
1827    pub fn rdbchecksum(mut self, enable: bool) -> Self {
1828        self.config.rdbchecksum = Some(enable);
1829        self
1830    }
1831
1832    /// Enable incremental fsync during RDB save.
1833    pub fn rdb_save_incremental_fsync(mut self, enable: bool) -> Self {
1834        self.config.rdb_save_incremental_fsync = Some(enable);
1835        self
1836    }
1837
1838    /// Delete RDB sync files used by diskless replication.
1839    pub fn rdb_del_sync_files(mut self, enable: bool) -> Self {
1840        self.config.rdb_del_sync_files = Some(enable);
1841        self
1842    }
1843
1844    /// Stop accepting writes when bgsave fails.
1845    pub fn stop_writes_on_bgsave_error(mut self, enable: bool) -> Self {
1846        self.config.stop_writes_on_bgsave_error = Some(enable);
1847        self
1848    }
1849
1850    // -- shutdown --
1851
1852    /// Set shutdown behavior on SIGINT (e.g. `"default"`, `"save"`, `"nosave"`).
1853    pub fn shutdown_on_sigint(mut self, behavior: impl Into<String>) -> Self {
1854        self.config.shutdown_on_sigint = Some(behavior.into());
1855        self
1856    }
1857
1858    /// Set shutdown behavior on SIGTERM.
1859    pub fn shutdown_on_sigterm(mut self, behavior: impl Into<String>) -> Self {
1860        self.config.shutdown_on_sigterm = Some(behavior.into());
1861        self
1862    }
1863
1864    /// Set the maximum seconds to wait during shutdown for lagging replicas.
1865    pub fn shutdown_timeout(mut self, seconds: u32) -> Self {
1866        self.config.shutdown_timeout = Some(seconds);
1867        self
1868    }
1869
1870    // -- other --
1871
1872    /// Enable or disable active rehashing.
1873    pub fn activerehashing(mut self, enable: bool) -> Self {
1874        self.config.activerehashing = Some(enable);
1875        self
1876    }
1877
1878    /// Enable crash log on crash.
1879    pub fn crash_log_enabled(mut self, enable: bool) -> Self {
1880        self.config.crash_log_enabled = Some(enable);
1881        self
1882    }
1883
1884    /// Enable crash memory check on crash.
1885    pub fn crash_memcheck_enabled(mut self, enable: bool) -> Self {
1886        self.config.crash_memcheck_enabled = Some(enable);
1887        self
1888    }
1889
1890    /// Disable transparent huge pages.
1891    pub fn disable_thp(mut self, enable: bool) -> Self {
1892        self.config.disable_thp = Some(enable);
1893        self
1894    }
1895
1896    /// Enable dynamic Hz adjustment.
1897    pub fn dynamic_hz(mut self, enable: bool) -> Self {
1898        self.config.dynamic_hz = Some(enable);
1899        self
1900    }
1901
1902    /// Ignore specific warnings (e.g. `"ARM64-COW-BUG"`).
1903    pub fn ignore_warnings(mut self, warning: impl Into<String>) -> Self {
1904        self.config.ignore_warnings = Some(warning.into());
1905        self
1906    }
1907
1908    /// Include another config file.
1909    pub fn include(mut self, path: impl Into<PathBuf>) -> Self {
1910        self.config.include.push(path.into());
1911        self
1912    }
1913
1914    /// Enable or disable jemalloc background thread.
1915    pub fn jemalloc_bg_thread(mut self, enable: bool) -> Self {
1916        self.config.jemalloc_bg_thread = Some(enable);
1917        self
1918    }
1919
1920    /// Set the locale collation setting.
1921    pub fn locale_collate(mut self, locale: impl Into<String>) -> Self {
1922        self.config.locale_collate = Some(locale.into());
1923        self
1924    }
1925
1926    /// Set the Lua script time limit in milliseconds.
1927    pub fn lua_time_limit(mut self, ms: u64) -> Self {
1928        self.config.lua_time_limit = Some(ms);
1929        self
1930    }
1931
1932    /// Set the OOM score adjustment mode (`"yes"`, `"no"`, or `"absolute"`).
1933    pub fn oom_score_adj(mut self, mode: impl Into<String>) -> Self {
1934        self.config.oom_score_adj = Some(mode.into());
1935        self
1936    }
1937
1938    /// Set the OOM score adjustment values (e.g. `"0 200 800"`).
1939    pub fn oom_score_adj_values(mut self, values: impl Into<String>) -> Self {
1940        self.config.oom_score_adj_values = Some(values.into());
1941        self
1942    }
1943
1944    /// Set the propagation error behavior (`"panic"` or `"ignore"`).
1945    pub fn propagation_error_behavior(mut self, behavior: impl Into<String>) -> Self {
1946        self.config.propagation_error_behavior = Some(behavior.into());
1947        self
1948    }
1949
1950    /// Set the maximum number of keys in the tracking table.
1951    pub fn tracking_table_max_keys(mut self, n: u64) -> Self {
1952        self.config.tracking_table_max_keys = Some(n);
1953        self
1954    }
1955
1956    // -- binary paths --
1957
1958    /// Set a custom `redis-server` binary path.
1959    pub fn redis_server_bin(mut self, bin: impl Into<String>) -> Self {
1960        self.config.redis_server_bin = bin.into();
1961        self
1962    }
1963
1964    /// Set a custom `redis-cli` binary path.
1965    pub fn redis_cli_bin(mut self, bin: impl Into<String>) -> Self {
1966        self.config.redis_cli_bin = bin.into();
1967        self
1968    }
1969
1970    /// Disable automatic Redis Stack module detection and loading.
1971    ///
1972    /// By default, if the server binary is part of a redis-stack installation,
1973    /// modules like RedisJSON, RediSearch, etc. are loaded automatically.
1974    /// Call this to suppress that behavior.
1975    pub fn no_stack_modules(mut self) -> Self {
1976        self.config.no_stack_modules = true;
1977        self
1978    }
1979
1980    /// Set an arbitrary config directive not covered by dedicated methods.
1981    pub fn extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1982        self.config.extra.insert(key.into(), value.into());
1983        self
1984    }
1985
1986    /// Start the server. Returns a handle that stops the server on Drop.
1987    ///
1988    /// Verifies that `redis-server` and `redis-cli` binaries are available
1989    /// before attempting to launch anything.
1990    pub async fn start(self) -> Result<RedisServerHandle> {
1991        if which::which(&self.config.redis_server_bin).is_err() {
1992            return Err(Error::BinaryNotFound {
1993                binary: self.config.redis_server_bin.clone(),
1994            });
1995        }
1996        if which::which(&self.config.redis_cli_bin).is_err() {
1997            return Err(Error::BinaryNotFound {
1998                binary: self.config.redis_cli_bin.clone(),
1999            });
2000        }
2001
2002        let node_dir = self.config.dir.join(format!("node-{}", self.config.port));
2003
2004        // Clean up any stale process from a previous run before (re)creating
2005        // the node directory. This handles test crashes that leave orphaned
2006        // redis-server processes behind.
2007        let stale_pidfile = node_dir.join("redis.pid");
2008        if let Some(stale_pid) = crate::process::read_pidfile(&stale_pidfile)
2009            && crate::process::pid_alive(stale_pid)
2010        {
2011            crate::process::force_kill(stale_pid);
2012        }
2013
2014        fs::create_dir_all(&node_dir)?;
2015
2016        let conf_path = node_dir.join("redis.conf");
2017        let conf_content = self.generate_config(&node_dir);
2018        fs::write(&conf_path, conf_content)?;
2019
2020        let module_args = if self.config.no_stack_modules {
2021            Vec::new()
2022        } else {
2023            crate::stack::detect_stack_modules(&self.config.redis_server_bin)
2024        };
2025        let status = Command::new(&self.config.redis_server_bin)
2026            .arg(&conf_path)
2027            .args(&module_args)
2028            .stdout(std::process::Stdio::null())
2029            .stderr(std::process::Stdio::null())
2030            .status()
2031            .await?;
2032
2033        if !status.success() {
2034            return Err(Error::ServerStart {
2035                port: self.config.port,
2036            });
2037        }
2038
2039        // The plain `port` always speaks unencrypted Redis protocol; `tls-port`
2040        // is the only listener that speaks TLS. When the plain port is
2041        // disabled (0), the server is only reachable over `tls-port`, so the
2042        // admin CLI must connect there with TLS enabled instead.
2043        let tls_only = self.config.port == 0;
2044        let admin_port = if tls_only {
2045            self.config.tls_port.unwrap_or(self.config.port)
2046        } else {
2047            self.config.port
2048        };
2049        let mut cli = RedisCli::new()
2050            .bin(&self.config.redis_cli_bin)
2051            .host(&self.config.bind)
2052            .port(admin_port);
2053        if let Some(ref pw) = self.config.password {
2054            cli = cli.password(pw);
2055        }
2056        if tls_only && self.config.tls_cert_file.is_some() && self.config.tls_key_file.is_some() {
2057            cli = cli.tls(true);
2058            if let Some(ref ca) = self.config.tls_ca_cert_file {
2059                cli = cli.cacert(ca);
2060            } else {
2061                cli = cli.insecure(true);
2062            }
2063            if let Some(ref cert) = self.config.tls_cert_file {
2064                cli = cli.cert(cert);
2065            }
2066            if let Some(ref key) = self.config.tls_key_file {
2067                cli = cli.key(key);
2068            }
2069        }
2070
2071        cli.wait_for_ready(Duration::from_secs(10)).await?;
2072
2073        let pid_path = node_dir.join("redis.pid");
2074        let pid: u32 = {
2075            let deadline = std::time::Instant::now() + std::time::Duration::from_secs(1);
2076            loop {
2077                if let Ok(s) = fs::read_to_string(&pid_path)
2078                    && let Ok(p) = s.trim().parse::<u32>()
2079                {
2080                    break p;
2081                }
2082                if std::time::Instant::now() >= deadline {
2083                    return Err(Error::ServerStart {
2084                        port: self.config.port,
2085                    });
2086                }
2087                tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2088            }
2089        };
2090
2091        Ok(RedisServerHandle {
2092            config: self.config,
2093            cli,
2094            pid,
2095            detached: false,
2096        })
2097    }
2098
2099    fn generate_config(&self, node_dir: &std::path::Path) -> String {
2100        let yn = |b: bool| if b { "yes" } else { "no" };
2101
2102        let mut conf = format!(
2103            "port {port}\n\
2104             bind {bind}\n\
2105             daemonize {daemonize}\n\
2106             pidfile \"{dir}/redis.pid\"\n\
2107             dir \"{dir}\"\n\
2108             loglevel {level}\n\
2109             protected-mode {protected}\n",
2110            port = self.config.port,
2111            bind = self.config.bind,
2112            daemonize = yn(self.config.daemonize),
2113            dir = node_dir.display(),
2114            level = self.config.loglevel,
2115            protected = yn(self.config.protected_mode),
2116        );
2117
2118        let logfile = self
2119            .config
2120            .logfile
2121            .as_deref()
2122            .map(str::to_owned)
2123            .unwrap_or_else(|| format!("{}/redis.log", node_dir.display()));
2124        conf.push_str(&format!("logfile \"{logfile}\"\n"));
2125
2126        // -- network --
2127        if let Some(backlog) = self.config.tcp_backlog {
2128            conf.push_str(&format!("tcp-backlog {backlog}\n"));
2129        }
2130        if let Some(ref path) = self.config.unixsocket {
2131            conf.push_str(&format!("unixsocket \"{}\"\n", path.display()));
2132        }
2133        if let Some(perm) = self.config.unixsocketperm {
2134            conf.push_str(&format!("unixsocketperm {perm}\n"));
2135        }
2136        if let Some(t) = self.config.timeout {
2137            conf.push_str(&format!("timeout {t}\n"));
2138        }
2139        if let Some(ka) = self.config.tcp_keepalive {
2140            conf.push_str(&format!("tcp-keepalive {ka}\n"));
2141        }
2142
2143        // -- tls --
2144        if let Some(port) = self.config.tls_port {
2145            conf.push_str(&format!("tls-port {port}\n"));
2146        }
2147        if let Some(ref path) = self.config.tls_cert_file {
2148            conf.push_str(&format!("tls-cert-file \"{}\"\n", path.display()));
2149        }
2150        if let Some(ref path) = self.config.tls_key_file {
2151            conf.push_str(&format!("tls-key-file \"{}\"\n", path.display()));
2152        }
2153        if let Some(ref pass) = self.config.tls_key_file_pass {
2154            conf.push_str(&format!("tls-key-file-pass {pass}\n"));
2155        }
2156        if let Some(ref path) = self.config.tls_ca_cert_file {
2157            conf.push_str(&format!("tls-ca-cert-file \"{}\"\n", path.display()));
2158        }
2159        if let Some(ref path) = self.config.tls_ca_cert_dir {
2160            conf.push_str(&format!("tls-ca-cert-dir \"{}\"\n", path.display()));
2161        }
2162        if let Some(auth) = self.config.tls_auth_clients {
2163            conf.push_str(&format!("tls-auth-clients {}\n", yn(auth)));
2164        }
2165        if let Some(ref path) = self.config.tls_client_cert_file {
2166            conf.push_str(&format!("tls-client-cert-file \"{}\"\n", path.display()));
2167        }
2168        if let Some(ref path) = self.config.tls_client_key_file {
2169            conf.push_str(&format!("tls-client-key-file \"{}\"\n", path.display()));
2170        }
2171        if let Some(ref pass) = self.config.tls_client_key_file_pass {
2172            conf.push_str(&format!("tls-client-key-file-pass {pass}\n"));
2173        }
2174        if let Some(ref path) = self.config.tls_dh_params_file {
2175            conf.push_str(&format!("tls-dh-params-file \"{}\"\n", path.display()));
2176        }
2177        if let Some(ref ciphers) = self.config.tls_ciphers {
2178            conf.push_str(&format!("tls-ciphers {ciphers}\n"));
2179        }
2180        if let Some(ref suites) = self.config.tls_ciphersuites {
2181            conf.push_str(&format!("tls-ciphersuites {suites}\n"));
2182        }
2183        if let Some(ref protocols) = self.config.tls_protocols {
2184            conf.push_str(&format!("tls-protocols {protocols}\n"));
2185        }
2186        if let Some(v) = self.config.tls_prefer_server_ciphers {
2187            conf.push_str(&format!("tls-prefer-server-ciphers {}\n", yn(v)));
2188        }
2189        if let Some(v) = self.config.tls_session_caching {
2190            conf.push_str(&format!("tls-session-caching {}\n", yn(v)));
2191        }
2192        if let Some(size) = self.config.tls_session_cache_size {
2193            conf.push_str(&format!("tls-session-cache-size {size}\n"));
2194        }
2195        if let Some(timeout) = self.config.tls_session_cache_timeout {
2196            conf.push_str(&format!("tls-session-cache-timeout {timeout}\n"));
2197        }
2198        if let Some(v) = self.config.tls_replication {
2199            conf.push_str(&format!("tls-replication {}\n", yn(v)));
2200        }
2201        if let Some(v) = self.config.tls_cluster {
2202            conf.push_str(&format!("tls-cluster {}\n", yn(v)));
2203        }
2204
2205        // -- general --
2206        if let Some(n) = self.config.databases {
2207            conf.push_str(&format!("databases {n}\n"));
2208        }
2209
2210        // -- memory --
2211        if let Some(ref limit) = self.config.maxmemory {
2212            conf.push_str(&format!("maxmemory {limit}\n"));
2213        }
2214        if let Some(ref policy) = self.config.maxmemory_policy {
2215            conf.push_str(&format!("maxmemory-policy {policy}\n"));
2216        }
2217        if let Some(n) = self.config.maxmemory_samples {
2218            conf.push_str(&format!("maxmemory-samples {n}\n"));
2219        }
2220        if let Some(ref limit) = self.config.maxmemory_clients {
2221            conf.push_str(&format!("maxmemory-clients {limit}\n"));
2222        }
2223        if let Some(n) = self.config.maxmemory_eviction_tenacity {
2224            conf.push_str(&format!("maxmemory-eviction-tenacity {n}\n"));
2225        }
2226        if let Some(n) = self.config.maxclients {
2227            conf.push_str(&format!("maxclients {n}\n"));
2228        }
2229        if let Some(n) = self.config.lfu_log_factor {
2230            conf.push_str(&format!("lfu-log-factor {n}\n"));
2231        }
2232        if let Some(n) = self.config.lfu_decay_time {
2233            conf.push_str(&format!("lfu-decay-time {n}\n"));
2234        }
2235        if let Some(n) = self.config.active_expire_effort {
2236            conf.push_str(&format!("active-expire-effort {n}\n"));
2237        }
2238
2239        // -- lazyfree --
2240        if let Some(v) = self.config.lazyfree_lazy_eviction {
2241            conf.push_str(&format!("lazyfree-lazy-eviction {}\n", yn(v)));
2242        }
2243        if let Some(v) = self.config.lazyfree_lazy_expire {
2244            conf.push_str(&format!("lazyfree-lazy-expire {}\n", yn(v)));
2245        }
2246        if let Some(v) = self.config.lazyfree_lazy_server_del {
2247            conf.push_str(&format!("lazyfree-lazy-server-del {}\n", yn(v)));
2248        }
2249        if let Some(v) = self.config.lazyfree_lazy_user_del {
2250            conf.push_str(&format!("lazyfree-lazy-user-del {}\n", yn(v)));
2251        }
2252        if let Some(v) = self.config.lazyfree_lazy_user_flush {
2253            conf.push_str(&format!("lazyfree-lazy-user-flush {}\n", yn(v)));
2254        }
2255
2256        // -- persistence --
2257        match &self.config.save {
2258            SavePolicy::Disabled => conf.push_str("save \"\"\n"),
2259            SavePolicy::Default => {}
2260            SavePolicy::Custom(pairs) => {
2261                for (secs, changes) in pairs {
2262                    conf.push_str(&format!("save {secs} {changes}\n"));
2263                }
2264            }
2265        }
2266        if self.config.appendonly {
2267            conf.push_str("appendonly yes\n");
2268        }
2269        if let Some(ref policy) = self.config.appendfsync {
2270            conf.push_str(&format!("appendfsync {policy}\n"));
2271        }
2272        if let Some(ref name) = self.config.appendfilename {
2273            conf.push_str(&format!("appendfilename \"{name}\"\n"));
2274        }
2275        if let Some(ref name) = self.config.appenddirname {
2276            conf.push_str(&format!("appenddirname \"{}\"\n", name.display()));
2277        }
2278        if let Some(v) = self.config.aof_use_rdb_preamble {
2279            conf.push_str(&format!("aof-use-rdb-preamble {}\n", yn(v)));
2280        }
2281        if let Some(v) = self.config.aof_load_truncated {
2282            conf.push_str(&format!("aof-load-truncated {}\n", yn(v)));
2283        }
2284        if let Some(ref size) = self.config.aof_load_corrupt_tail_max_size {
2285            conf.push_str(&format!("aof-load-corrupt-tail-max-size {size}\n"));
2286        }
2287        if let Some(v) = self.config.aof_rewrite_incremental_fsync {
2288            conf.push_str(&format!("aof-rewrite-incremental-fsync {}\n", yn(v)));
2289        }
2290        if let Some(v) = self.config.aof_timestamp_enabled {
2291            conf.push_str(&format!("aof-timestamp-enabled {}\n", yn(v)));
2292        }
2293        if let Some(pct) = self.config.auto_aof_rewrite_percentage {
2294            conf.push_str(&format!("auto-aof-rewrite-percentage {pct}\n"));
2295        }
2296        if let Some(ref size) = self.config.auto_aof_rewrite_min_size {
2297            conf.push_str(&format!("auto-aof-rewrite-min-size {size}\n"));
2298        }
2299        if let Some(v) = self.config.no_appendfsync_on_rewrite {
2300            conf.push_str(&format!("no-appendfsync-on-rewrite {}\n", yn(v)));
2301        }
2302
2303        // -- replication --
2304        if let Some((ref host, port)) = self.config.replicaof {
2305            conf.push_str(&format!("replicaof {host} {port}\n"));
2306        }
2307        if let Some(ref pw) = self.config.masterauth {
2308            conf.push_str(&format!("masterauth {pw}\n"));
2309        }
2310        if let Some(ref user) = self.config.masteruser {
2311            conf.push_str(&format!("masteruser {user}\n"));
2312        }
2313        if let Some(ref size) = self.config.repl_backlog_size {
2314            conf.push_str(&format!("repl-backlog-size {size}\n"));
2315        }
2316        if let Some(ttl) = self.config.repl_backlog_ttl {
2317            conf.push_str(&format!("repl-backlog-ttl {ttl}\n"));
2318        }
2319        if let Some(v) = self.config.repl_disable_tcp_nodelay {
2320            conf.push_str(&format!("repl-disable-tcp-nodelay {}\n", yn(v)));
2321        }
2322        if let Some(ref policy) = self.config.repl_diskless_load {
2323            conf.push_str(&format!("repl-diskless-load {policy}\n"));
2324        }
2325        if let Some(v) = self.config.repl_diskless_sync {
2326            conf.push_str(&format!("repl-diskless-sync {}\n", yn(v)));
2327        }
2328        if let Some(delay) = self.config.repl_diskless_sync_delay {
2329            conf.push_str(&format!("repl-diskless-sync-delay {delay}\n"));
2330        }
2331        if let Some(n) = self.config.repl_diskless_sync_max_replicas {
2332            conf.push_str(&format!("repl-diskless-sync-max-replicas {n}\n"));
2333        }
2334        if let Some(period) = self.config.repl_ping_replica_period {
2335            conf.push_str(&format!("repl-ping-replica-period {period}\n"));
2336        }
2337        if let Some(t) = self.config.repl_timeout {
2338            conf.push_str(&format!("repl-timeout {t}\n"));
2339        }
2340        if let Some(ref ip) = self.config.replica_announce_ip {
2341            conf.push_str(&format!("replica-announce-ip {ip}\n"));
2342        }
2343        if let Some(port) = self.config.replica_announce_port {
2344            conf.push_str(&format!("replica-announce-port {port}\n"));
2345        }
2346        if let Some(v) = self.config.replica_announced {
2347            conf.push_str(&format!("replica-announced {}\n", yn(v)));
2348        }
2349        if let Some(ref size) = self.config.replica_full_sync_buffer_limit {
2350            conf.push_str(&format!("replica-full-sync-buffer-limit {size}\n"));
2351        }
2352        if let Some(v) = self.config.replica_ignore_disk_write_errors {
2353            conf.push_str(&format!("replica-ignore-disk-write-errors {}\n", yn(v)));
2354        }
2355        if let Some(v) = self.config.replica_ignore_maxmemory {
2356            conf.push_str(&format!("replica-ignore-maxmemory {}\n", yn(v)));
2357        }
2358        if let Some(v) = self.config.replica_lazy_flush {
2359            conf.push_str(&format!("replica-lazy-flush {}\n", yn(v)));
2360        }
2361        if let Some(priority) = self.config.replica_priority {
2362            conf.push_str(&format!("replica-priority {priority}\n"));
2363        }
2364        if let Some(v) = self.config.replica_read_only {
2365            conf.push_str(&format!("replica-read-only {}\n", yn(v)));
2366        }
2367        if let Some(v) = self.config.replica_serve_stale_data {
2368            conf.push_str(&format!("replica-serve-stale-data {}\n", yn(v)));
2369        }
2370        if let Some(n) = self.config.min_replicas_to_write {
2371            conf.push_str(&format!("min-replicas-to-write {n}\n"));
2372        }
2373        if let Some(lag) = self.config.min_replicas_max_lag {
2374            conf.push_str(&format!("min-replicas-max-lag {lag}\n"));
2375        }
2376
2377        // -- security --
2378        if let Some(ref pw) = self.config.password {
2379            conf.push_str(&format!("requirepass {pw}\n"));
2380        }
2381        if let Some(ref path) = self.config.acl_file {
2382            conf.push_str(&format!("aclfile \"{}\"\n", path.display()));
2383        }
2384
2385        // -- cluster --
2386        if self.config.cluster_enabled {
2387            conf.push_str("cluster-enabled yes\n");
2388            if let Some(ref path) = self.config.cluster_config_file {
2389                conf.push_str(&format!("cluster-config-file \"{}\"\n", path.display()));
2390            } else {
2391                conf.push_str(&format!(
2392                    "cluster-config-file \"{}/nodes.conf\"\n",
2393                    node_dir.display()
2394                ));
2395            }
2396            if let Some(timeout) = self.config.cluster_node_timeout {
2397                conf.push_str(&format!("cluster-node-timeout {timeout}\n"));
2398            }
2399            if let Some(v) = self.config.cluster_require_full_coverage {
2400                conf.push_str(&format!("cluster-require-full-coverage {}\n", yn(v)));
2401            }
2402            if let Some(v) = self.config.cluster_allow_reads_when_down {
2403                conf.push_str(&format!("cluster-allow-reads-when-down {}\n", yn(v)));
2404            }
2405            if let Some(v) = self.config.cluster_allow_pubsubshard_when_down {
2406                conf.push_str(&format!("cluster-allow-pubsubshard-when-down {}\n", yn(v)));
2407            }
2408            if let Some(v) = self.config.cluster_allow_replica_migration {
2409                conf.push_str(&format!("cluster-allow-replica-migration {}\n", yn(v)));
2410            }
2411            if let Some(barrier) = self.config.cluster_migration_barrier {
2412                conf.push_str(&format!("cluster-migration-barrier {barrier}\n"));
2413            }
2414            if let Some(v) = self.config.cluster_replica_no_failover {
2415                conf.push_str(&format!("cluster-replica-no-failover {}\n", yn(v)));
2416            }
2417            if let Some(factor) = self.config.cluster_replica_validity_factor {
2418                conf.push_str(&format!("cluster-replica-validity-factor {factor}\n"));
2419            }
2420            if let Some(ref ip) = self.config.cluster_announce_ip {
2421                conf.push_str(&format!("cluster-announce-ip {ip}\n"));
2422            }
2423            if let Some(port) = self.config.cluster_announce_port {
2424                conf.push_str(&format!("cluster-announce-port {port}\n"));
2425            }
2426            if let Some(port) = self.config.cluster_announce_bus_port {
2427                conf.push_str(&format!("cluster-announce-bus-port {port}\n"));
2428            }
2429            if let Some(port) = self.config.cluster_announce_tls_port {
2430                conf.push_str(&format!("cluster-announce-tls-port {port}\n"));
2431            }
2432            if let Some(ref hostname) = self.config.cluster_announce_hostname {
2433                conf.push_str(&format!("cluster-announce-hostname {hostname}\n"));
2434            }
2435            if let Some(ref name) = self.config.cluster_announce_human_nodename {
2436                conf.push_str(&format!("cluster-announce-human-nodename {name}\n"));
2437            }
2438            if let Some(port) = self.config.cluster_port {
2439                conf.push_str(&format!("cluster-port {port}\n"));
2440            }
2441            if let Some(ref endpoint_type) = self.config.cluster_preferred_endpoint_type {
2442                conf.push_str(&format!(
2443                    "cluster-preferred-endpoint-type {endpoint_type}\n"
2444                ));
2445            }
2446            if let Some(limit) = self.config.cluster_link_sendbuf_limit {
2447                conf.push_str(&format!("cluster-link-sendbuf-limit {limit}\n"));
2448            }
2449            if let Some(ratio) = self.config.cluster_compatibility_sample_ratio {
2450                conf.push_str(&format!("cluster-compatibility-sample-ratio {ratio}\n"));
2451            }
2452            if let Some(bytes) = self.config.cluster_slot_migration_handoff_max_lag_bytes {
2453                conf.push_str(&format!(
2454                    "cluster-slot-migration-handoff-max-lag-bytes {bytes}\n"
2455                ));
2456            }
2457            if let Some(ms) = self.config.cluster_slot_migration_write_pause_timeout {
2458                conf.push_str(&format!(
2459                    "cluster-slot-migration-write-pause-timeout {ms}\n"
2460                ));
2461            }
2462            if let Some(v) = self.config.cluster_slot_stats_enabled {
2463                conf.push_str(&format!("cluster-slot-stats-enabled {}\n", yn(v)));
2464            }
2465        }
2466
2467        // -- data structures --
2468        if let Some(n) = self.config.hash_max_listpack_entries {
2469            conf.push_str(&format!("hash-max-listpack-entries {n}\n"));
2470        }
2471        if let Some(n) = self.config.hash_max_listpack_value {
2472            conf.push_str(&format!("hash-max-listpack-value {n}\n"));
2473        }
2474        if let Some(n) = self.config.list_max_listpack_size {
2475            conf.push_str(&format!("list-max-listpack-size {n}\n"));
2476        }
2477        if let Some(n) = self.config.list_compress_depth {
2478            conf.push_str(&format!("list-compress-depth {n}\n"));
2479        }
2480        if let Some(n) = self.config.set_max_intset_entries {
2481            conf.push_str(&format!("set-max-intset-entries {n}\n"));
2482        }
2483        if let Some(n) = self.config.set_max_listpack_entries {
2484            conf.push_str(&format!("set-max-listpack-entries {n}\n"));
2485        }
2486        if let Some(n) = self.config.set_max_listpack_value {
2487            conf.push_str(&format!("set-max-listpack-value {n}\n"));
2488        }
2489        if let Some(n) = self.config.zset_max_listpack_entries {
2490            conf.push_str(&format!("zset-max-listpack-entries {n}\n"));
2491        }
2492        if let Some(n) = self.config.zset_max_listpack_value {
2493            conf.push_str(&format!("zset-max-listpack-value {n}\n"));
2494        }
2495        if let Some(n) = self.config.hll_sparse_max_bytes {
2496            conf.push_str(&format!("hll-sparse-max-bytes {n}\n"));
2497        }
2498        if let Some(n) = self.config.stream_node_max_bytes {
2499            conf.push_str(&format!("stream-node-max-bytes {n}\n"));
2500        }
2501        if let Some(n) = self.config.stream_node_max_entries {
2502            conf.push_str(&format!("stream-node-max-entries {n}\n"));
2503        }
2504        if let Some(ms) = self.config.stream_idmp_duration {
2505            conf.push_str(&format!("stream-idmp-duration {ms}\n"));
2506        }
2507        if let Some(n) = self.config.stream_idmp_maxsize {
2508            conf.push_str(&format!("stream-idmp-maxsize {n}\n"));
2509        }
2510
2511        // -- modules --
2512        for (path, args) in &self.config.loadmodule {
2513            conf.push_str(&format!("loadmodule \"{}\"", path.display()));
2514            for arg in args {
2515                conf.push(' ');
2516                conf.push_str(arg);
2517            }
2518            conf.push('\n');
2519        }
2520
2521        // -- advanced --
2522        if let Some(hz) = self.config.hz {
2523            conf.push_str(&format!("hz {hz}\n"));
2524        }
2525        if let Some(n) = self.config.io_threads {
2526            conf.push_str(&format!("io-threads {n}\n"));
2527        }
2528        if let Some(enable) = self.config.io_threads_do_reads {
2529            conf.push_str(&format!("io-threads-do-reads {}\n", yn(enable)));
2530        }
2531        if let Some(ref events) = self.config.notify_keyspace_events {
2532            conf.push_str(&format!("notify-keyspace-events {events}\n"));
2533        }
2534
2535        // -- slow log --
2536        if let Some(us) = self.config.slowlog_log_slower_than {
2537            conf.push_str(&format!("slowlog-log-slower-than {us}\n"));
2538        }
2539        if let Some(n) = self.config.slowlog_max_len {
2540            conf.push_str(&format!("slowlog-max-len {n}\n"));
2541        }
2542
2543        // -- latency tracking --
2544        if let Some(ms) = self.config.latency_monitor_threshold {
2545            conf.push_str(&format!("latency-monitor-threshold {ms}\n"));
2546        }
2547        if let Some(enable) = self.config.latency_tracking {
2548            conf.push_str(&format!("latency-tracking {}\n", yn(enable)));
2549        }
2550        if let Some(ref pcts) = self.config.latency_tracking_info_percentiles {
2551            conf.push_str(&format!("latency-tracking-info-percentiles \"{pcts}\"\n"));
2552        }
2553
2554        // -- active defragmentation --
2555        if let Some(enable) = self.config.activedefrag {
2556            conf.push_str(&format!("activedefrag {}\n", yn(enable)));
2557        }
2558        if let Some(ref bytes) = self.config.active_defrag_ignore_bytes {
2559            conf.push_str(&format!("active-defrag-ignore-bytes {bytes}\n"));
2560        }
2561        if let Some(pct) = self.config.active_defrag_threshold_lower {
2562            conf.push_str(&format!("active-defrag-threshold-lower {pct}\n"));
2563        }
2564        if let Some(pct) = self.config.active_defrag_threshold_upper {
2565            conf.push_str(&format!("active-defrag-threshold-upper {pct}\n"));
2566        }
2567        if let Some(pct) = self.config.active_defrag_cycle_min {
2568            conf.push_str(&format!("active-defrag-cycle-min {pct}\n"));
2569        }
2570        if let Some(pct) = self.config.active_defrag_cycle_max {
2571            conf.push_str(&format!("active-defrag-cycle-max {pct}\n"));
2572        }
2573        if let Some(n) = self.config.active_defrag_max_scan_fields {
2574            conf.push_str(&format!("active-defrag-max-scan-fields {n}\n"));
2575        }
2576
2577        // -- logging and process --
2578        if let Some(enable) = self.config.syslog_enabled {
2579            conf.push_str(&format!("syslog-enabled {}\n", yn(enable)));
2580        }
2581        if let Some(ref ident) = self.config.syslog_ident {
2582            conf.push_str(&format!("syslog-ident {ident}\n"));
2583        }
2584        if let Some(ref facility) = self.config.syslog_facility {
2585            conf.push_str(&format!("syslog-facility {facility}\n"));
2586        }
2587        if let Some(ref mode) = self.config.supervised {
2588            conf.push_str(&format!("supervised {mode}\n"));
2589        }
2590        if let Some(enable) = self.config.always_show_logo {
2591            conf.push_str(&format!("always-show-logo {}\n", yn(enable)));
2592        }
2593        if let Some(enable) = self.config.set_proc_title {
2594            conf.push_str(&format!("set-proc-title {}\n", yn(enable)));
2595        }
2596        if let Some(ref template) = self.config.proc_title_template {
2597            conf.push_str(&format!("proc-title-template \"{template}\"\n"));
2598        }
2599
2600        // -- security and ACL --
2601        if let Some(ref default) = self.config.acl_pubsub_default {
2602            conf.push_str(&format!("acl-pubsub-default {default}\n"));
2603        }
2604        if let Some(n) = self.config.acllog_max_len {
2605            conf.push_str(&format!("acllog-max-len {n}\n"));
2606        }
2607        if let Some(ref mode) = self.config.enable_debug_command {
2608            conf.push_str(&format!("enable-debug-command {mode}\n"));
2609        }
2610        if let Some(ref mode) = self.config.enable_module_command {
2611            conf.push_str(&format!("enable-module-command {mode}\n"));
2612        }
2613        if let Some(ref mode) = self.config.enable_protected_configs {
2614            conf.push_str(&format!("enable-protected-configs {mode}\n"));
2615        }
2616        for (cmd, new_name) in &self.config.rename_command {
2617            conf.push_str(&format!("rename-command {cmd} \"{new_name}\"\n"));
2618        }
2619        if let Some(ref mode) = self.config.sanitize_dump_payload {
2620            conf.push_str(&format!("sanitize-dump-payload {mode}\n"));
2621        }
2622        if let Some(enable) = self.config.hide_user_data_from_log {
2623            conf.push_str(&format!("hide-user-data-from-log {}\n", yn(enable)));
2624        }
2625
2626        // -- networking (additional) --
2627        if let Some(ref addr) = self.config.bind_source_addr {
2628            conf.push_str(&format!("bind-source-addr {addr}\n"));
2629        }
2630        if let Some(ms) = self.config.busy_reply_threshold {
2631            conf.push_str(&format!("busy-reply-threshold {ms}\n"));
2632        }
2633        for limit in &self.config.client_output_buffer_limit {
2634            conf.push_str(&format!("client-output-buffer-limit {limit}\n"));
2635        }
2636        if let Some(ref limit) = self.config.client_query_buffer_limit {
2637            conf.push_str(&format!("client-query-buffer-limit {limit}\n"));
2638        }
2639        if let Some(ref len) = self.config.proto_max_bulk_len {
2640            conf.push_str(&format!("proto-max-bulk-len {len}\n"));
2641        }
2642        if let Some(n) = self.config.max_new_connections_per_cycle {
2643            conf.push_str(&format!("max-new-connections-per-cycle {n}\n"));
2644        }
2645        if let Some(n) = self.config.max_new_tls_connections_per_cycle {
2646            conf.push_str(&format!("max-new-tls-connections-per-cycle {n}\n"));
2647        }
2648        if let Some(id) = self.config.socket_mark_id {
2649            conf.push_str(&format!("socket-mark-id {id}\n"));
2650        }
2651
2652        // -- RDB (additional) --
2653        if let Some(ref name) = self.config.dbfilename {
2654            conf.push_str(&format!("dbfilename {name}\n"));
2655        }
2656        if let Some(enable) = self.config.rdbcompression {
2657            conf.push_str(&format!("rdbcompression {}\n", yn(enable)));
2658        }
2659        if let Some(enable) = self.config.rdbchecksum {
2660            conf.push_str(&format!("rdbchecksum {}\n", yn(enable)));
2661        }
2662        if let Some(enable) = self.config.rdb_save_incremental_fsync {
2663            conf.push_str(&format!("rdb-save-incremental-fsync {}\n", yn(enable)));
2664        }
2665        if let Some(enable) = self.config.rdb_del_sync_files {
2666            conf.push_str(&format!("rdb-del-sync-files {}\n", yn(enable)));
2667        }
2668        if let Some(enable) = self.config.stop_writes_on_bgsave_error {
2669            conf.push_str(&format!("stop-writes-on-bgsave-error {}\n", yn(enable)));
2670        }
2671
2672        // -- shutdown --
2673        if let Some(ref behavior) = self.config.shutdown_on_sigint {
2674            conf.push_str(&format!("shutdown-on-sigint {behavior}\n"));
2675        }
2676        if let Some(ref behavior) = self.config.shutdown_on_sigterm {
2677            conf.push_str(&format!("shutdown-on-sigterm {behavior}\n"));
2678        }
2679        if let Some(seconds) = self.config.shutdown_timeout {
2680            conf.push_str(&format!("shutdown-timeout {seconds}\n"));
2681        }
2682
2683        // -- other --
2684        if let Some(enable) = self.config.activerehashing {
2685            conf.push_str(&format!("activerehashing {}\n", yn(enable)));
2686        }
2687        if let Some(enable) = self.config.crash_log_enabled {
2688            conf.push_str(&format!("crash-log-enabled {}\n", yn(enable)));
2689        }
2690        if let Some(enable) = self.config.crash_memcheck_enabled {
2691            conf.push_str(&format!("crash-memcheck-enabled {}\n", yn(enable)));
2692        }
2693        if let Some(enable) = self.config.disable_thp {
2694            conf.push_str(&format!("disable-thp {}\n", yn(enable)));
2695        }
2696        if let Some(enable) = self.config.dynamic_hz {
2697            conf.push_str(&format!("dynamic-hz {}\n", yn(enable)));
2698        }
2699        if let Some(ref warning) = self.config.ignore_warnings {
2700            conf.push_str(&format!("ignore-warnings {warning}\n"));
2701        }
2702        for path in &self.config.include {
2703            conf.push_str(&format!("include \"{}\"\n", path.display()));
2704        }
2705        if let Some(enable) = self.config.jemalloc_bg_thread {
2706            conf.push_str(&format!("jemalloc-bg-thread {}\n", yn(enable)));
2707        }
2708        if let Some(ref locale) = self.config.locale_collate {
2709            conf.push_str(&format!("locale-collate {locale}\n"));
2710        }
2711        if let Some(ms) = self.config.lua_time_limit {
2712            conf.push_str(&format!("lua-time-limit {ms}\n"));
2713        }
2714        if let Some(ref mode) = self.config.oom_score_adj {
2715            conf.push_str(&format!("oom-score-adj {mode}\n"));
2716        }
2717        if let Some(ref values) = self.config.oom_score_adj_values {
2718            conf.push_str(&format!("oom-score-adj-values {values}\n"));
2719        }
2720        if let Some(ref behavior) = self.config.propagation_error_behavior {
2721            conf.push_str(&format!("propagation-error-behavior {behavior}\n"));
2722        }
2723        if let Some(n) = self.config.tracking_table_max_keys {
2724            conf.push_str(&format!("tracking-table-max-keys {n}\n"));
2725        }
2726
2727        // -- catch-all --
2728        for (key, value) in &self.config.extra {
2729            conf.push_str(&format!("{key} {value}\n"));
2730        }
2731
2732        conf
2733    }
2734}
2735
2736impl Default for RedisServer {
2737    fn default() -> Self {
2738        Self::new()
2739    }
2740}
2741
2742/// Handle to a running Redis server. Stops the server on Drop.
2743pub struct RedisServerHandle {
2744    config: RedisServerConfig,
2745    cli: RedisCli,
2746    pid: u32,
2747    detached: bool,
2748}
2749
2750impl RedisServerHandle {
2751    /// The server's address as "host:port".
2752    pub fn addr(&self) -> String {
2753        format!("{}:{}", self.config.bind, self.config.port)
2754    }
2755
2756    /// The server's port.
2757    pub fn port(&self) -> u16 {
2758        self.config.port
2759    }
2760
2761    /// The server's bind address.
2762    pub fn host(&self) -> &str {
2763        &self.config.bind
2764    }
2765
2766    /// The PID of the `redis-server` process.
2767    pub fn pid(&self) -> u32 {
2768        self.pid
2769    }
2770
2771    /// Check if the server is alive via PING.
2772    pub async fn is_alive(&self) -> bool {
2773        self.cli.ping().await
2774    }
2775
2776    /// Get a `RedisCli` configured for this server.
2777    pub fn cli(&self) -> &RedisCli {
2778        &self.cli
2779    }
2780
2781    /// Run a redis-cli command against this server.
2782    pub async fn run(&self, args: &[&str]) -> Result<String> {
2783        self.cli.run(args).await
2784    }
2785
2786    /// Consume the handle without stopping the server.
2787    pub fn detach(mut self) {
2788        self.detached = true;
2789    }
2790
2791    /// Stop the server via an escalating shutdown strategy.
2792    ///
2793    /// 1. Sends `SHUTDOWN NOSAVE` via `redis-cli` for a graceful shutdown.
2794    /// 2. Waits 500ms for the process to exit.
2795    /// 3. If still alive, calls [`crate::process::force_kill`] (SIGTERM then SIGKILL).
2796    /// 4. Attempts to release the port via [`crate::process::kill_by_port`] as a final safety net.
2797    pub fn stop(&self) {
2798        // Step 1: graceful shutdown.
2799        self.cli.shutdown();
2800        // Step 2: grace period.
2801        std::thread::sleep(std::time::Duration::from_millis(500));
2802        // Step 3: force kill if still alive.
2803        if crate::process::pid_alive(self.pid) {
2804            crate::process::force_kill(self.pid);
2805        }
2806        // Step 4: port cleanup as safety net.
2807        crate::process::kill_by_port(self.config.port);
2808    }
2809
2810    /// Wait until the server is ready (PING -> PONG).
2811    pub async fn wait_for_ready(&self, timeout: Duration) -> Result<()> {
2812        self.cli.wait_for_ready(timeout).await
2813    }
2814}
2815
2816impl Drop for RedisServerHandle {
2817    fn drop(&mut self) {
2818        if !self.detached {
2819            self.stop();
2820        }
2821    }
2822}
2823
2824#[cfg(test)]
2825mod tests {
2826    use super::*;
2827
2828    #[test]
2829    fn default_config() {
2830        let s = RedisServer::new();
2831        assert_eq!(s.config.port, 6379);
2832        assert_eq!(s.config.bind, "127.0.0.1");
2833        assert!(matches!(s.config.save, SavePolicy::Disabled));
2834    }
2835
2836    #[test]
2837    fn builder_chain() {
2838        let s = RedisServer::new()
2839            .port(6400)
2840            .bind("0.0.0.0")
2841            .save(true)
2842            .appendonly(true)
2843            .password("secret")
2844            .logfile("/tmp/redis.log")
2845            .loglevel(LogLevel::Warning)
2846            .extra("maxmemory", "100mb");
2847
2848        assert_eq!(s.config.port, 6400);
2849        assert_eq!(s.config.bind, "0.0.0.0");
2850        assert!(matches!(s.config.save, SavePolicy::Default));
2851        assert!(s.config.appendonly);
2852        assert_eq!(s.config.password.as_deref(), Some("secret"));
2853        assert_eq!(s.config.logfile.as_deref(), Some("/tmp/redis.log"));
2854        assert_eq!(s.config.extra.get("maxmemory").unwrap(), "100mb");
2855    }
2856
2857    #[test]
2858    fn save_schedule() {
2859        let s = RedisServer::new().save_schedule(vec![(900, 1), (300, 10)]);
2860        match &s.config.save {
2861            SavePolicy::Custom(pairs) => {
2862                assert_eq!(pairs, &[(900, 1), (300, 10)]);
2863            }
2864            _ => panic!("expected SavePolicy::Custom"),
2865        }
2866    }
2867
2868    #[test]
2869    fn aof_tuning() {
2870        let s = RedisServer::new()
2871            .appendonly(true)
2872            .appendfsync(AppendFsync::Always)
2873            .appendfilename("my.aof")
2874            .aof_use_rdb_preamble(true)
2875            .auto_aof_rewrite_percentage(100)
2876            .auto_aof_rewrite_min_size("64mb")
2877            .no_appendfsync_on_rewrite(true);
2878
2879        assert!(s.config.appendonly);
2880        assert!(matches!(s.config.appendfsync, Some(AppendFsync::Always)));
2881        assert_eq!(s.config.appendfilename.as_deref(), Some("my.aof"));
2882        assert_eq!(s.config.aof_use_rdb_preamble, Some(true));
2883        assert_eq!(s.config.auto_aof_rewrite_percentage, Some(100));
2884        assert_eq!(s.config.auto_aof_rewrite_min_size.as_deref(), Some("64mb"));
2885        assert_eq!(s.config.no_appendfsync_on_rewrite, Some(true));
2886    }
2887
2888    #[test]
2889    fn memory_eviction_and_lazyfree() {
2890        let s = RedisServer::new()
2891            .maxmemory("256mb")
2892            .maxmemory_policy("allkeys-lfu")
2893            .maxmemory_samples(10)
2894            .maxmemory_clients("0")
2895            .maxmemory_eviction_tenacity(50)
2896            .lfu_log_factor(10)
2897            .lfu_decay_time(1)
2898            .active_expire_effort(25)
2899            .lazyfree_lazy_eviction(true)
2900            .lazyfree_lazy_expire(true)
2901            .lazyfree_lazy_server_del(true)
2902            .lazyfree_lazy_user_del(false)
2903            .lazyfree_lazy_user_flush(true);
2904
2905        assert_eq!(s.config.maxmemory.as_deref(), Some("256mb"));
2906        assert_eq!(s.config.maxmemory_policy.as_deref(), Some("allkeys-lfu"));
2907        assert_eq!(s.config.maxmemory_samples, Some(10));
2908        assert_eq!(s.config.maxmemory_clients.as_deref(), Some("0"));
2909        assert_eq!(s.config.maxmemory_eviction_tenacity, Some(50));
2910        assert_eq!(s.config.lfu_log_factor, Some(10));
2911        assert_eq!(s.config.lfu_decay_time, Some(1));
2912        assert_eq!(s.config.active_expire_effort, Some(25));
2913        assert_eq!(s.config.lazyfree_lazy_eviction, Some(true));
2914        assert_eq!(s.config.lazyfree_lazy_expire, Some(true));
2915        assert_eq!(s.config.lazyfree_lazy_server_del, Some(true));
2916        assert_eq!(s.config.lazyfree_lazy_user_del, Some(false));
2917        assert_eq!(s.config.lazyfree_lazy_user_flush, Some(true));
2918    }
2919
2920    #[test]
2921    fn replication_tuning() {
2922        let s = RedisServer::new()
2923            .replicaof("127.0.0.1", 6379)
2924            .masterauth("secret")
2925            .masteruser("repl-user")
2926            .repl_backlog_size("1mb")
2927            .repl_backlog_ttl(3600)
2928            .repl_disable_tcp_nodelay(true)
2929            .repl_diskless_load(ReplDisklessLoad::Swapdb)
2930            .repl_diskless_sync(true)
2931            .repl_diskless_sync_delay(5)
2932            .repl_diskless_sync_max_replicas(3)
2933            .repl_ping_replica_period(10)
2934            .repl_timeout(60)
2935            .replica_announce_ip("10.0.0.1")
2936            .replica_announce_port(6380)
2937            .replica_announced(true)
2938            .replica_full_sync_buffer_limit("256mb")
2939            .replica_ignore_disk_write_errors(false)
2940            .replica_ignore_maxmemory(true)
2941            .replica_lazy_flush(true)
2942            .replica_priority(100)
2943            .replica_read_only(true)
2944            .replica_serve_stale_data(false)
2945            .min_replicas_to_write(2)
2946            .min_replicas_max_lag(10);
2947
2948        assert_eq!(s.config.replicaof, Some(("127.0.0.1".into(), 6379)));
2949        assert_eq!(s.config.masterauth.as_deref(), Some("secret"));
2950        assert_eq!(s.config.masteruser.as_deref(), Some("repl-user"));
2951        assert_eq!(s.config.repl_backlog_size.as_deref(), Some("1mb"));
2952        assert_eq!(s.config.repl_backlog_ttl, Some(3600));
2953        assert_eq!(s.config.repl_disable_tcp_nodelay, Some(true));
2954        assert!(matches!(
2955            s.config.repl_diskless_load,
2956            Some(ReplDisklessLoad::Swapdb)
2957        ));
2958        assert_eq!(s.config.repl_diskless_sync, Some(true));
2959        assert_eq!(s.config.repl_diskless_sync_delay, Some(5));
2960        assert_eq!(s.config.repl_diskless_sync_max_replicas, Some(3));
2961        assert_eq!(s.config.repl_ping_replica_period, Some(10));
2962        assert_eq!(s.config.repl_timeout, Some(60));
2963        assert_eq!(s.config.replica_announce_ip.as_deref(), Some("10.0.0.1"));
2964        assert_eq!(s.config.replica_announce_port, Some(6380));
2965        assert_eq!(s.config.replica_announced, Some(true));
2966        assert_eq!(
2967            s.config.replica_full_sync_buffer_limit.as_deref(),
2968            Some("256mb")
2969        );
2970        assert_eq!(s.config.replica_ignore_disk_write_errors, Some(false));
2971        assert_eq!(s.config.replica_ignore_maxmemory, Some(true));
2972        assert_eq!(s.config.replica_lazy_flush, Some(true));
2973        assert_eq!(s.config.replica_priority, Some(100));
2974        assert_eq!(s.config.replica_read_only, Some(true));
2975        assert_eq!(s.config.replica_serve_stale_data, Some(false));
2976        assert_eq!(s.config.min_replicas_to_write, Some(2));
2977        assert_eq!(s.config.min_replicas_max_lag, Some(10));
2978    }
2979
2980    #[test]
2981    fn cluster_config() {
2982        let s = RedisServer::new()
2983            .port(7000)
2984            .cluster_enabled(true)
2985            .cluster_node_timeout(5000)
2986            .cluster_config_file("/tmp/nodes.conf")
2987            .cluster_require_full_coverage(false)
2988            .cluster_allow_reads_when_down(true)
2989            .cluster_allow_pubsubshard_when_down(true)
2990            .cluster_allow_replica_migration(true)
2991            .cluster_migration_barrier(1)
2992            .cluster_replica_no_failover(false)
2993            .cluster_replica_validity_factor(10)
2994            .cluster_announce_ip("10.0.0.1")
2995            .cluster_announce_port(7000)
2996            .cluster_announce_bus_port(17000)
2997            .cluster_announce_tls_port(7100)
2998            .cluster_announce_hostname("node1.example.com")
2999            .cluster_announce_human_nodename("node-1")
3000            .cluster_port(17000)
3001            .cluster_preferred_endpoint_type("ip")
3002            .cluster_link_sendbuf_limit(67108864)
3003            .cluster_compatibility_sample_ratio(50)
3004            .cluster_slot_migration_handoff_max_lag_bytes(1048576)
3005            .cluster_slot_migration_write_pause_timeout(5000)
3006            .cluster_slot_stats_enabled(true);
3007
3008        assert!(s.config.cluster_enabled);
3009        assert_eq!(s.config.cluster_node_timeout, Some(5000));
3010        assert_eq!(
3011            s.config.cluster_config_file,
3012            Some(PathBuf::from("/tmp/nodes.conf"))
3013        );
3014        assert_eq!(s.config.cluster_require_full_coverage, Some(false));
3015        assert_eq!(s.config.cluster_allow_reads_when_down, Some(true));
3016        assert_eq!(s.config.cluster_allow_pubsubshard_when_down, Some(true));
3017        assert_eq!(s.config.cluster_allow_replica_migration, Some(true));
3018        assert_eq!(s.config.cluster_migration_barrier, Some(1));
3019        assert_eq!(s.config.cluster_replica_no_failover, Some(false));
3020        assert_eq!(s.config.cluster_replica_validity_factor, Some(10));
3021        assert_eq!(s.config.cluster_announce_ip.as_deref(), Some("10.0.0.1"));
3022        assert_eq!(s.config.cluster_announce_port, Some(7000));
3023        assert_eq!(s.config.cluster_announce_bus_port, Some(17000));
3024        assert_eq!(s.config.cluster_announce_tls_port, Some(7100));
3025        assert_eq!(
3026            s.config.cluster_announce_hostname.as_deref(),
3027            Some("node1.example.com")
3028        );
3029        assert_eq!(
3030            s.config.cluster_announce_human_nodename.as_deref(),
3031            Some("node-1")
3032        );
3033        assert_eq!(s.config.cluster_port, Some(17000));
3034        assert_eq!(
3035            s.config.cluster_preferred_endpoint_type.as_deref(),
3036            Some("ip")
3037        );
3038        assert_eq!(s.config.cluster_link_sendbuf_limit, Some(67108864));
3039        assert_eq!(s.config.cluster_compatibility_sample_ratio, Some(50));
3040        assert_eq!(
3041            s.config.cluster_slot_migration_handoff_max_lag_bytes,
3042            Some(1048576)
3043        );
3044        assert_eq!(
3045            s.config.cluster_slot_migration_write_pause_timeout,
3046            Some(5000)
3047        );
3048        assert_eq!(s.config.cluster_slot_stats_enabled, Some(true));
3049    }
3050
3051    #[test]
3052    fn data_structure_tuning() {
3053        let s = RedisServer::new()
3054            .hash_max_listpack_entries(128)
3055            .hash_max_listpack_value(64)
3056            .list_max_listpack_size(-2)
3057            .list_compress_depth(1)
3058            .set_max_intset_entries(512)
3059            .set_max_listpack_entries(128)
3060            .set_max_listpack_value(64)
3061            .zset_max_listpack_entries(128)
3062            .zset_max_listpack_value(64)
3063            .hll_sparse_max_bytes(3000)
3064            .stream_node_max_bytes(4096)
3065            .stream_node_max_entries(100)
3066            .stream_idmp_duration(5000)
3067            .stream_idmp_maxsize(1000);
3068
3069        assert_eq!(s.config.hash_max_listpack_entries, Some(128));
3070        assert_eq!(s.config.hash_max_listpack_value, Some(64));
3071        assert_eq!(s.config.list_max_listpack_size, Some(-2));
3072        assert_eq!(s.config.list_compress_depth, Some(1));
3073        assert_eq!(s.config.set_max_intset_entries, Some(512));
3074        assert_eq!(s.config.set_max_listpack_entries, Some(128));
3075        assert_eq!(s.config.set_max_listpack_value, Some(64));
3076        assert_eq!(s.config.zset_max_listpack_entries, Some(128));
3077        assert_eq!(s.config.zset_max_listpack_value, Some(64));
3078        assert_eq!(s.config.hll_sparse_max_bytes, Some(3000));
3079        assert_eq!(s.config.stream_node_max_bytes, Some(4096));
3080        assert_eq!(s.config.stream_node_max_entries, Some(100));
3081        assert_eq!(s.config.stream_idmp_duration, Some(5000));
3082        assert_eq!(s.config.stream_idmp_maxsize, Some(1000));
3083    }
3084
3085    #[test]
3086    fn tls_config() {
3087        let s = RedisServer::new()
3088            .port(6400)
3089            .tls_port(6401)
3090            .tls_cert_file("/etc/tls/redis.crt")
3091            .tls_key_file("/etc/tls/redis.key")
3092            .tls_key_file_pass("keypass")
3093            .tls_ca_cert_file("/etc/tls/ca.crt")
3094            .tls_ca_cert_dir("/etc/tls/certs")
3095            .tls_auth_clients(true)
3096            .tls_client_cert_file("/etc/tls/client.crt")
3097            .tls_client_key_file("/etc/tls/client.key")
3098            .tls_client_key_file_pass("clientpass")
3099            .tls_dh_params_file("/etc/tls/dhparams.pem")
3100            .tls_ciphers("ECDHE-RSA-AES256-GCM-SHA384")
3101            .tls_ciphersuites("TLS_AES_256_GCM_SHA384")
3102            .tls_protocols("TLSv1.2 TLSv1.3")
3103            .tls_prefer_server_ciphers(true)
3104            .tls_session_caching(true)
3105            .tls_session_cache_size(20480)
3106            .tls_session_cache_timeout(300)
3107            .tls_replication(true)
3108            .tls_cluster(true);
3109
3110        assert_eq!(s.config.tls_port, Some(6401));
3111        assert_eq!(
3112            s.config.tls_cert_file.as_deref(),
3113            Some(std::path::Path::new("/etc/tls/redis.crt"))
3114        );
3115        assert_eq!(
3116            s.config.tls_key_file.as_deref(),
3117            Some(std::path::Path::new("/etc/tls/redis.key"))
3118        );
3119        assert_eq!(s.config.tls_key_file_pass.as_deref(), Some("keypass"));
3120        assert_eq!(
3121            s.config.tls_ca_cert_file.as_deref(),
3122            Some(std::path::Path::new("/etc/tls/ca.crt"))
3123        );
3124        assert_eq!(
3125            s.config.tls_ca_cert_dir.as_deref(),
3126            Some(std::path::Path::new("/etc/tls/certs"))
3127        );
3128        assert_eq!(s.config.tls_auth_clients, Some(true));
3129        assert_eq!(
3130            s.config.tls_client_cert_file.as_deref(),
3131            Some(std::path::Path::new("/etc/tls/client.crt"))
3132        );
3133        assert_eq!(
3134            s.config.tls_client_key_file.as_deref(),
3135            Some(std::path::Path::new("/etc/tls/client.key"))
3136        );
3137        assert_eq!(
3138            s.config.tls_client_key_file_pass.as_deref(),
3139            Some("clientpass")
3140        );
3141        assert_eq!(
3142            s.config.tls_dh_params_file.as_deref(),
3143            Some(std::path::Path::new("/etc/tls/dhparams.pem"))
3144        );
3145        assert_eq!(
3146            s.config.tls_ciphers.as_deref(),
3147            Some("ECDHE-RSA-AES256-GCM-SHA384")
3148        );
3149        assert_eq!(
3150            s.config.tls_ciphersuites.as_deref(),
3151            Some("TLS_AES_256_GCM_SHA384")
3152        );
3153        assert_eq!(s.config.tls_protocols.as_deref(), Some("TLSv1.2 TLSv1.3"));
3154        assert_eq!(s.config.tls_prefer_server_ciphers, Some(true));
3155        assert_eq!(s.config.tls_session_caching, Some(true));
3156        assert_eq!(s.config.tls_session_cache_size, Some(20480));
3157        assert_eq!(s.config.tls_session_cache_timeout, Some(300));
3158        assert_eq!(s.config.tls_replication, Some(true));
3159        assert_eq!(s.config.tls_cluster, Some(true));
3160    }
3161
3162    #[test]
3163    fn loadmodule_config() {
3164        let s = RedisServer::new().loadmodule("/x/mod.so");
3165        let config = s.generate_config(std::path::Path::new("/tmp/rsw-test"));
3166        assert!(config.contains("loadmodule \"/x/mod.so\"\n"));
3167    }
3168
3169    #[test]
3170    fn loadmodule_with_args_config() {
3171        let s = RedisServer::new()
3172            .loadmodule_with_args("/x/mod.so", ["stream-prefix", "ks:", "events"]);
3173        let config = s.generate_config(std::path::Path::new("/tmp/rsw-test"));
3174        assert!(config.contains("loadmodule \"/x/mod.so\" stream-prefix ks: events\n"));
3175    }
3176}