1use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use std::time::Duration;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use crate::cli::RedisCli;
9use crate::error::{Error, Result};
10use crate::server::{RedisServer, RedisServerHandle, SavePolicy};
11
12pub struct NodeContext {
18 pub server: RedisServer,
23 pub index: usize,
28 pub port: u16,
30 pub total_nodes: u16,
32 pub masters: u16,
34 pub replicas_per_master: u16,
36}
37
38impl NodeContext {
39 pub fn is_master(&self) -> bool {
41 self.index < self.masters as usize
42 }
43
44 pub fn is_replica(&self) -> bool {
46 !self.is_master()
47 }
48}
49
50pub struct RedisClusterBuilder {
71 masters: u16,
72 replicas_per_master: u16,
73 base_port: u16,
74 bind: String,
75 password: Option<String>,
76 logfile: Option<String>,
77 save: Option<SavePolicy>,
78 appendonly: Option<bool>,
79 cluster_node_timeout: Option<u64>,
80 cluster_require_full_coverage: Option<bool>,
81 cluster_allow_reads_when_down: Option<bool>,
82 cluster_allow_pubsubshard_when_down: Option<bool>,
83 cluster_allow_replica_migration: Option<bool>,
84 cluster_migration_barrier: Option<u32>,
85 cluster_announce_hostname: Option<String>,
86 cluster_announce_human_nodename: Option<String>,
87 cluster_preferred_endpoint_type: Option<String>,
88 cluster_replica_no_failover: Option<bool>,
89 cluster_replica_validity_factor: Option<u32>,
90 cluster_announce_ip: Option<String>,
91 cluster_announce_port: Option<u16>,
92 cluster_announce_bus_port: Option<u16>,
93 cluster_announce_tls_port: Option<u16>,
94 cluster_port: Option<u16>,
95 cluster_link_sendbuf_limit: Option<u64>,
96 cluster_compatibility_sample_ratio: Option<u32>,
97 cluster_slot_migration_handoff_max_lag_bytes: Option<u64>,
98 cluster_slot_migration_write_pause_timeout: Option<u64>,
99 cluster_slot_stats_enabled: Option<bool>,
100 min_replicas_to_write: Option<u32>,
101 min_replicas_max_lag: Option<u32>,
102 repl_diskless_sync: Option<bool>,
103 repl_diskless_sync_delay: Option<u32>,
104 repl_ping_replica_period: Option<u32>,
105 repl_timeout: Option<u32>,
106 tls_port: Option<u16>,
107 tls_cert_file: Option<PathBuf>,
108 tls_key_file: Option<PathBuf>,
109 tls_ca_cert_file: Option<PathBuf>,
110 tls_ca_cert_dir: Option<PathBuf>,
111 tls_auth_clients: Option<bool>,
112 tls_replication: Option<bool>,
113 tls_cluster: Option<bool>,
114 extra: HashMap<String, String>,
115 redis_server_bin: String,
116 redis_cli_bin: String,
117 node_config_fn: Option<Box<dyn FnMut(NodeContext) -> RedisServer + Send>>,
118}
119
120impl RedisClusterBuilder {
121 pub fn masters(mut self, n: u16) -> Self {
123 self.masters = n;
124 self
125 }
126
127 pub fn replicas_per_master(mut self, n: u16) -> Self {
129 self.replicas_per_master = n;
130 self
131 }
132
133 pub fn base_port(mut self, port: u16) -> Self {
137 self.base_port = port;
138 self
139 }
140
141 pub fn bind(mut self, bind: impl Into<String>) -> Self {
143 self.bind = bind.into();
144 self
145 }
146
147 pub fn password(mut self, password: impl Into<String>) -> Self {
149 self.password = Some(password.into());
150 self
151 }
152
153 pub fn logfile(mut self, path: impl Into<String>) -> Self {
155 self.logfile = Some(path.into());
156 self
157 }
158
159 pub fn save(mut self, save: bool) -> Self {
164 self.save = Some(if save {
165 SavePolicy::Default
166 } else {
167 SavePolicy::Disabled
168 });
169 self
170 }
171
172 pub fn save_schedule(mut self, schedule: Vec<(u64, u64)>) -> Self {
174 self.save = Some(SavePolicy::Custom(schedule));
175 self
176 }
177
178 pub fn appendonly(mut self, appendonly: bool) -> Self {
180 self.appendonly = Some(appendonly);
181 self
182 }
183
184 pub fn cluster_node_timeout(mut self, ms: u64) -> Self {
186 self.cluster_node_timeout = Some(ms);
187 self
188 }
189
190 pub fn cluster_require_full_coverage(mut self, require: bool) -> Self {
192 self.cluster_require_full_coverage = Some(require);
193 self
194 }
195
196 pub fn cluster_allow_reads_when_down(mut self, allow: bool) -> Self {
198 self.cluster_allow_reads_when_down = Some(allow);
199 self
200 }
201
202 pub fn cluster_allow_pubsubshard_when_down(mut self, allow: bool) -> Self {
204 self.cluster_allow_pubsubshard_when_down = Some(allow);
205 self
206 }
207
208 pub fn cluster_allow_replica_migration(mut self, allow: bool) -> Self {
210 self.cluster_allow_replica_migration = Some(allow);
211 self
212 }
213
214 pub fn cluster_migration_barrier(mut self, barrier: u32) -> Self {
216 self.cluster_migration_barrier = Some(barrier);
217 self
218 }
219
220 pub fn cluster_announce_hostname(mut self, hostname: impl Into<String>) -> Self {
222 self.cluster_announce_hostname = Some(hostname.into());
223 self
224 }
225
226 pub fn cluster_preferred_endpoint_type(mut self, endpoint_type: impl Into<String>) -> Self {
228 self.cluster_preferred_endpoint_type = Some(endpoint_type.into());
229 self
230 }
231
232 pub fn cluster_replica_no_failover(mut self, no_failover: bool) -> Self {
236 self.cluster_replica_no_failover = Some(no_failover);
237 self
238 }
239
240 pub fn cluster_replica_validity_factor(mut self, factor: u32) -> Self {
246 self.cluster_replica_validity_factor = Some(factor);
247 self
248 }
249
250 pub fn cluster_announce_ip(mut self, ip: impl Into<String>) -> Self {
252 self.cluster_announce_ip = Some(ip.into());
253 self
254 }
255
256 pub fn cluster_announce_port(mut self, port: u16) -> Self {
258 self.cluster_announce_port = Some(port);
259 self
260 }
261
262 pub fn cluster_announce_bus_port(mut self, port: u16) -> Self {
264 self.cluster_announce_bus_port = Some(port);
265 self
266 }
267
268 pub fn cluster_announce_tls_port(mut self, port: u16) -> Self {
270 self.cluster_announce_tls_port = Some(port);
271 self
272 }
273
274 pub fn cluster_announce_human_nodename(mut self, name: impl Into<String>) -> Self {
276 self.cluster_announce_human_nodename = Some(name.into());
277 self
278 }
279
280 pub fn cluster_port(mut self, port: u16) -> Self {
282 self.cluster_port = Some(port);
283 self
284 }
285
286 pub fn cluster_link_sendbuf_limit(mut self, limit: u64) -> Self {
290 self.cluster_link_sendbuf_limit = Some(limit);
291 self
292 }
293
294 pub fn cluster_compatibility_sample_ratio(mut self, ratio: u32) -> Self {
296 self.cluster_compatibility_sample_ratio = Some(ratio);
297 self
298 }
299
300 pub fn cluster_slot_migration_handoff_max_lag_bytes(mut self, bytes: u64) -> Self {
302 self.cluster_slot_migration_handoff_max_lag_bytes = Some(bytes);
303 self
304 }
305
306 pub fn cluster_slot_migration_write_pause_timeout(mut self, ms: u64) -> Self {
308 self.cluster_slot_migration_write_pause_timeout = Some(ms);
309 self
310 }
311
312 pub fn cluster_slot_stats_enabled(mut self, enable: bool) -> Self {
314 self.cluster_slot_stats_enabled = Some(enable);
315 self
316 }
317
318 pub fn min_replicas_to_write(mut self, n: u32) -> Self {
325 self.min_replicas_to_write = Some(n);
326 self
327 }
328
329 pub fn min_replicas_max_lag(mut self, seconds: u32) -> Self {
333 self.min_replicas_max_lag = Some(seconds);
334 self
335 }
336
337 pub fn repl_diskless_sync(mut self, enable: bool) -> Self {
341 self.repl_diskless_sync = Some(enable);
342 self
343 }
344
345 pub fn repl_diskless_sync_delay(mut self, seconds: u32) -> Self {
349 self.repl_diskless_sync_delay = Some(seconds);
350 self
351 }
352
353 pub fn repl_ping_replica_period(mut self, seconds: u32) -> Self {
358 self.repl_ping_replica_period = Some(seconds);
359 self
360 }
361
362 pub fn repl_timeout(mut self, seconds: u32) -> Self {
366 self.repl_timeout = Some(seconds);
367 self
368 }
369
370 pub fn tls_port(mut self, port: u16) -> Self {
374 self.tls_port = Some(port);
375 self
376 }
377
378 pub fn tls_cert_file(mut self, path: impl Into<PathBuf>) -> Self {
380 self.tls_cert_file = Some(path.into());
381 self
382 }
383
384 pub fn tls_key_file(mut self, path: impl Into<PathBuf>) -> Self {
386 self.tls_key_file = Some(path.into());
387 self
388 }
389
390 pub fn tls_ca_cert_file(mut self, path: impl Into<PathBuf>) -> Self {
392 self.tls_ca_cert_file = Some(path.into());
393 self
394 }
395
396 pub fn tls_ca_cert_dir(mut self, path: impl Into<PathBuf>) -> Self {
398 self.tls_ca_cert_dir = Some(path.into());
399 self
400 }
401
402 pub fn tls_auth_clients(mut self, auth: bool) -> Self {
404 self.tls_auth_clients = Some(auth);
405 self
406 }
407
408 pub fn tls_replication(mut self, enable: bool) -> Self {
410 self.tls_replication = Some(enable);
411 self
412 }
413
414 pub fn tls_cluster(mut self, enable: bool) -> Self {
416 self.tls_cluster = Some(enable);
417 self
418 }
419
420 pub fn with_node_config(
455 mut self,
456 f: impl FnMut(NodeContext) -> RedisServer + Send + 'static,
457 ) -> Self {
458 self.node_config_fn = Some(Box::new(f));
459 self
460 }
461
462 pub fn extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
464 self.extra.insert(key.into(), value.into());
465 self
466 }
467
468 pub fn redis_server_bin(mut self, bin: impl Into<String>) -> Self {
470 self.redis_server_bin = bin.into();
471 self
472 }
473
474 pub fn redis_cli_bin(mut self, bin: impl Into<String>) -> Self {
476 self.redis_cli_bin = bin.into();
477 self
478 }
479
480 fn total_nodes(&self) -> u16 {
481 self.masters * (1 + self.replicas_per_master)
482 }
483
484 fn ports(&self) -> impl Iterator<Item = u16> {
485 let base = self.base_port;
486 let total = self.total_nodes();
487 (0..total).map(move |i| base + i)
488 }
489
490 fn has_tls(&self) -> bool {
492 self.tls_cert_file.is_some() && self.tls_key_file.is_some()
493 }
494
495 fn apply_tls_to_cli(&self, mut cli: RedisCli) -> RedisCli {
497 if self.has_tls() {
498 cli = cli.tls(true);
499 if let Some(ref ca) = self.tls_ca_cert_file {
500 cli = cli.cacert(ca);
501 } else {
502 cli = cli.insecure(true);
503 }
504 if let Some(ref cert) = self.tls_cert_file {
505 cli = cli.cert(cert);
506 }
507 if let Some(ref key) = self.tls_key_file {
508 cli = cli.key(key);
509 }
510 }
511 cli
512 }
513
514 pub async fn start(mut self) -> Result<RedisClusterHandle> {
516 for port in self.ports() {
518 let mut cli = RedisCli::new()
519 .bin(&self.redis_cli_bin)
520 .host(&self.bind)
521 .port(port);
522 if let Some(ref password) = self.password {
523 cli = cli.password(password);
524 }
525 cli = self.apply_tls_to_cli(cli);
526 cli.shutdown();
527 }
528 tokio::time::sleep(Duration::from_millis(500)).await;
529
530 let total_nodes = self.total_nodes();
532 let ports: Vec<u16> = self.ports().collect();
533 let unique = SystemTime::now()
534 .duration_since(UNIX_EPOCH)
535 .map(|duration| duration.as_nanos())
536 .unwrap_or(0);
537 let cluster_base = std::env::temp_dir().join(format!(
538 "redis-cluster-wrapper-{}-{}",
539 std::process::id(),
540 unique
541 ));
542 let mut nodes = Vec::new();
543 for (index, port) in ports.into_iter().enumerate() {
544 let node_dir = cluster_base.join(format!("node-{port}"));
545 let mut server = RedisServer::new()
546 .port(port)
547 .bind(&self.bind)
548 .dir(node_dir)
549 .cluster_enabled(true)
550 .cluster_node_timeout(self.cluster_node_timeout.unwrap_or(5000))
551 .redis_server_bin(&self.redis_server_bin)
552 .redis_cli_bin(&self.redis_cli_bin);
553 if let Some(v) = self.cluster_require_full_coverage {
554 server = server.cluster_require_full_coverage(v);
555 }
556 if let Some(v) = self.cluster_allow_reads_when_down {
557 server = server.cluster_allow_reads_when_down(v);
558 }
559 if let Some(v) = self.cluster_allow_pubsubshard_when_down {
560 server = server.cluster_allow_pubsubshard_when_down(v);
561 }
562 if let Some(v) = self.cluster_allow_replica_migration {
563 server = server.cluster_allow_replica_migration(v);
564 }
565 if let Some(barrier) = self.cluster_migration_barrier {
566 server = server.cluster_migration_barrier(barrier);
567 }
568 if let Some(ref hostname) = self.cluster_announce_hostname {
569 server = server.cluster_announce_hostname(hostname.clone());
570 }
571 if let Some(ref endpoint_type) = self.cluster_preferred_endpoint_type {
572 server = server.cluster_preferred_endpoint_type(endpoint_type.clone());
573 }
574 if let Some(v) = self.cluster_replica_no_failover {
575 server = server.cluster_replica_no_failover(v);
576 }
577 if let Some(factor) = self.cluster_replica_validity_factor {
578 server = server.cluster_replica_validity_factor(factor);
579 }
580 if let Some(ref ip) = self.cluster_announce_ip {
581 server = server.cluster_announce_ip(ip.clone());
582 }
583 if let Some(port) = self.cluster_announce_port {
584 server = server.cluster_announce_port(port);
585 }
586 if let Some(port) = self.cluster_announce_bus_port {
587 server = server.cluster_announce_bus_port(port);
588 }
589 if let Some(port) = self.cluster_announce_tls_port {
590 server = server.cluster_announce_tls_port(port);
591 }
592 if let Some(ref name) = self.cluster_announce_human_nodename {
593 server = server.cluster_announce_human_nodename(name.clone());
594 }
595 if let Some(port) = self.cluster_port {
596 server = server.cluster_port(port);
597 }
598 if let Some(limit) = self.cluster_link_sendbuf_limit {
599 server = server.cluster_link_sendbuf_limit(limit);
600 }
601 if let Some(ratio) = self.cluster_compatibility_sample_ratio {
602 server = server.cluster_compatibility_sample_ratio(ratio);
603 }
604 if let Some(bytes) = self.cluster_slot_migration_handoff_max_lag_bytes {
605 server = server.cluster_slot_migration_handoff_max_lag_bytes(bytes);
606 }
607 if let Some(ms) = self.cluster_slot_migration_write_pause_timeout {
608 server = server.cluster_slot_migration_write_pause_timeout(ms);
609 }
610 if let Some(v) = self.cluster_slot_stats_enabled {
611 server = server.cluster_slot_stats_enabled(v);
612 }
613 if let Some(n) = self.min_replicas_to_write {
614 server = server.min_replicas_to_write(n);
615 }
616 if let Some(seconds) = self.min_replicas_max_lag {
617 server = server.min_replicas_max_lag(seconds);
618 }
619 if let Some(v) = self.repl_diskless_sync {
620 server = server.repl_diskless_sync(v);
621 }
622 if let Some(seconds) = self.repl_diskless_sync_delay {
623 server = server.repl_diskless_sync_delay(seconds);
624 }
625 if let Some(seconds) = self.repl_ping_replica_period {
626 server = server.repl_ping_replica_period(seconds);
627 }
628 if let Some(seconds) = self.repl_timeout {
629 server = server.repl_timeout(seconds);
630 }
631 if let Some(ref password) = self.password {
632 server = server.password(password).masterauth(password);
633 }
634 if let Some(ref logfile) = self.logfile {
635 server = server.logfile(logfile.clone());
636 }
637 if let Some(ref save) = self.save {
638 match save {
639 SavePolicy::Disabled => server = server.save(false),
640 SavePolicy::Default => server = server.save(true),
641 SavePolicy::Custom(pairs) => {
642 server = server.save_schedule(pairs.clone());
643 }
644 }
645 }
646 if let Some(appendonly) = self.appendonly {
647 server = server.appendonly(appendonly);
648 }
649 if let Some(port) = self.tls_port {
651 server = server.tls_port(port + index as u16);
652 }
653 if let Some(ref path) = self.tls_cert_file {
654 server = server.tls_cert_file(path);
655 }
656 if let Some(ref path) = self.tls_key_file {
657 server = server.tls_key_file(path);
658 }
659 if let Some(ref path) = self.tls_ca_cert_file {
660 server = server.tls_ca_cert_file(path);
661 }
662 if let Some(ref path) = self.tls_ca_cert_dir {
663 server = server.tls_ca_cert_dir(path);
664 }
665 if let Some(v) = self.tls_auth_clients {
666 server = server.tls_auth_clients(v);
667 }
668 if let Some(v) = self.tls_replication {
669 server = server.tls_replication(v);
670 }
671 if let Some(v) = self.tls_cluster {
672 server = server.tls_cluster(v);
673 }
674 for (key, value) in &self.extra {
675 server = server.extra(key.clone(), value.clone());
676 }
677 if let Some(ref mut f) = self.node_config_fn {
679 server = f(NodeContext {
680 server,
681 index,
682 port,
683 total_nodes,
684 masters: self.masters,
685 replicas_per_master: self.replicas_per_master,
686 });
687 }
688 let handle = server.start().await?;
689 nodes.push(handle);
690 }
691
692 let node_addrs: Vec<String> = nodes.iter().map(|n| n.addr()).collect();
694 let mut cli = RedisCli::new()
695 .bin(&self.redis_cli_bin)
696 .host(&self.bind)
697 .port(self.base_port);
698 if let Some(ref password) = self.password {
699 cli = cli.password(password);
700 }
701 cli = self.apply_tls_to_cli(cli);
702 cli.cluster_create(&node_addrs, self.replicas_per_master)
703 .await?;
704
705 tokio::time::sleep(Duration::from_secs(2)).await;
707
708 Ok(RedisClusterHandle {
709 nodes,
710 num_masters: self.masters,
711 bind: self.bind,
712 base_port: self.base_port,
713 password: self.password,
714 redis_cli_bin: self.redis_cli_bin,
715 tls: TlsConfig {
716 cert_file: self.tls_cert_file,
717 key_file: self.tls_key_file,
718 ca_cert_file: self.tls_ca_cert_file,
719 },
720 cluster_base,
721 })
722 }
723}
724
725#[derive(Clone, Debug, Default)]
727struct TlsConfig {
728 cert_file: Option<PathBuf>,
729 key_file: Option<PathBuf>,
730 ca_cert_file: Option<PathBuf>,
731}
732
733impl TlsConfig {
734 fn has_tls(&self) -> bool {
735 self.cert_file.is_some() && self.key_file.is_some()
736 }
737
738 fn apply(&self, mut cli: RedisCli) -> RedisCli {
739 if self.has_tls() {
740 cli = cli.tls(true);
741 if let Some(ref ca) = self.ca_cert_file {
742 cli = cli.cacert(ca);
743 } else {
744 cli = cli.insecure(true);
745 }
746 if let Some(ref cert) = self.cert_file {
747 cli = cli.cert(cert);
748 }
749 if let Some(ref key) = self.key_file {
750 cli = cli.key(key);
751 }
752 }
753 cli
754 }
755}
756
757pub struct RedisClusterHandle {
759 nodes: Vec<RedisServerHandle>,
760 num_masters: u16,
761 bind: String,
762 base_port: u16,
763 password: Option<String>,
764 redis_cli_bin: String,
765 tls: TlsConfig,
766 cluster_base: PathBuf,
768}
769
770pub struct RedisCluster;
775
776impl RedisCluster {
777 pub fn builder() -> RedisClusterBuilder {
779 RedisClusterBuilder {
780 masters: 3,
781 replicas_per_master: 0,
782 base_port: 7000,
783 bind: "127.0.0.1".into(),
784 password: None,
785 logfile: None,
786 save: None,
787 appendonly: None,
788 cluster_node_timeout: None,
789 cluster_require_full_coverage: None,
790 cluster_allow_reads_when_down: None,
791 cluster_allow_pubsubshard_when_down: None,
792 cluster_allow_replica_migration: None,
793 cluster_migration_barrier: None,
794 cluster_announce_hostname: None,
795 cluster_announce_human_nodename: None,
796 cluster_preferred_endpoint_type: None,
797 cluster_replica_no_failover: None,
798 cluster_replica_validity_factor: None,
799 cluster_announce_ip: None,
800 cluster_announce_port: None,
801 cluster_announce_bus_port: None,
802 cluster_announce_tls_port: None,
803 cluster_port: None,
804 cluster_link_sendbuf_limit: None,
805 cluster_compatibility_sample_ratio: None,
806 cluster_slot_migration_handoff_max_lag_bytes: None,
807 cluster_slot_migration_write_pause_timeout: None,
808 cluster_slot_stats_enabled: None,
809 min_replicas_to_write: None,
810 min_replicas_max_lag: None,
811 repl_diskless_sync: None,
812 repl_diskless_sync_delay: None,
813 repl_ping_replica_period: None,
814 repl_timeout: None,
815 tls_port: None,
816 tls_cert_file: None,
817 tls_key_file: None,
818 tls_ca_cert_file: None,
819 tls_ca_cert_dir: None,
820 tls_auth_clients: None,
821 tls_replication: None,
822 tls_cluster: None,
823 extra: HashMap::new(),
824 redis_server_bin: "redis-server".into(),
825 redis_cli_bin: "redis-cli".into(),
826 node_config_fn: None,
827 }
828 }
829}
830
831impl RedisClusterHandle {
832 pub fn addr(&self) -> String {
834 format!("{}:{}", self.bind, self.base_port)
835 }
836
837 pub fn node_addrs(&self) -> Vec<String> {
839 self.nodes.iter().map(|n| n.addr()).collect()
840 }
841
842 pub fn pids(&self) -> Vec<u32> {
844 self.nodes.iter().map(|n| n.pid()).collect()
845 }
846
847 pub async fn all_alive(&self) -> bool {
849 for node in &self.nodes {
850 if !node.is_alive().await {
851 return false;
852 }
853 }
854 true
855 }
856
857 pub async fn is_healthy(&self) -> bool {
859 for node in &self.nodes {
860 if let Ok(info) = node.run(&["CLUSTER", "INFO"]).await
861 && info.contains("cluster_state:ok")
862 && info.contains("cluster_slots_ok:16384")
863 {
864 return true;
865 }
866 }
867 false
868 }
869
870 pub async fn wait_for_healthy(&self, timeout: Duration) -> Result<()> {
872 let start = std::time::Instant::now();
873 loop {
874 if self.is_healthy().await {
875 return Ok(());
876 }
877 if start.elapsed() > timeout {
878 return Err(Error::Timeout {
879 message: "cluster did not become healthy in time".into(),
880 });
881 }
882 tokio::time::sleep(Duration::from_millis(500)).await;
883 }
884 }
885
886 pub fn node(&self, index: usize) -> &RedisServerHandle {
895 &self.nodes[index]
896 }
897
898 pub fn nodes(&self) -> &[RedisServerHandle] {
900 &self.nodes
901 }
902
903 pub fn num_masters(&self) -> u16 {
905 self.num_masters
906 }
907
908 pub fn master_nodes(&self) -> &[RedisServerHandle] {
913 &self.nodes[..self.num_masters as usize]
914 }
915
916 pub fn replica_nodes(&self) -> &[RedisServerHandle] {
921 &self.nodes[self.num_masters as usize..]
922 }
923
924 pub async fn config_set_all(&self, key: &str, value: &str) -> Result<()> {
926 for node in &self.nodes {
927 node.run(&["CONFIG", "SET", key, value]).await?;
928 }
929 Ok(())
930 }
931
932 pub async fn config_set_masters(&self, key: &str, value: &str) -> Result<()> {
934 for node in self.master_nodes() {
935 node.run(&["CONFIG", "SET", key, value]).await?;
936 }
937 Ok(())
938 }
939
940 pub async fn config_set_replicas(&self, key: &str, value: &str) -> Result<()> {
942 for node in self.replica_nodes() {
943 node.run(&["CONFIG", "SET", key, value]).await?;
944 }
945 Ok(())
946 }
947
948 pub(crate) fn password(&self) -> Option<&str> {
953 self.password.as_deref()
954 }
955
956 pub fn cli(&self) -> RedisCli {
958 let mut cli = RedisCli::new()
959 .bin(&self.redis_cli_bin)
960 .host(&self.bind)
961 .port(self.base_port);
962 if let Some(ref password) = self.password {
963 cli = cli.password(password);
964 }
965 cli = self.tls.apply(cli);
966 cli
967 }
968
969 pub fn cluster_base(&self) -> &Path {
971 &self.cluster_base
972 }
973}
974
975impl Drop for RedisClusterHandle {
976 fn drop(&mut self) {
977 }
979}
980
981#[cfg(test)]
982mod tests {
983 use super::*;
984
985 #[test]
986 fn builder_defaults() {
987 let b = RedisCluster::builder();
988 assert_eq!(b.masters, 3);
989 assert_eq!(b.replicas_per_master, 0);
990 assert_eq!(b.base_port, 7000);
991 assert_eq!(b.password, None);
992 assert!(b.logfile.is_none());
993 assert!(b.extra.is_empty());
994 assert_eq!(b.total_nodes(), 3);
995 assert!(b.cluster_node_timeout.is_none());
996 assert!(b.cluster_require_full_coverage.is_none());
997 assert!(b.cluster_allow_reads_when_down.is_none());
998 assert!(b.cluster_allow_pubsubshard_when_down.is_none());
999 assert!(b.cluster_allow_replica_migration.is_none());
1000 assert!(b.cluster_migration_barrier.is_none());
1001 assert!(b.cluster_announce_hostname.is_none());
1002 assert!(b.cluster_preferred_endpoint_type.is_none());
1003 }
1004
1005 #[test]
1006 fn builder_with_replicas() {
1007 let b = RedisCluster::builder().masters(3).replicas_per_master(1);
1008 assert_eq!(b.total_nodes(), 6);
1009 let ports: Vec<u16> = b.ports().collect();
1010 assert_eq!(ports, vec![7000, 7001, 7002, 7003, 7004, 7005]);
1011 }
1012
1013 #[test]
1014 fn builder_password() {
1015 let b = RedisCluster::builder().password("secret");
1016 assert_eq!(b.password.as_deref(), Some("secret"));
1017 }
1018
1019 #[test]
1020 fn builder_cluster_directives() {
1021 let b = RedisCluster::builder()
1022 .cluster_node_timeout(10000)
1023 .cluster_require_full_coverage(false)
1024 .cluster_allow_reads_when_down(true)
1025 .cluster_allow_pubsubshard_when_down(true)
1026 .cluster_allow_replica_migration(false)
1027 .cluster_migration_barrier(2)
1028 .cluster_announce_hostname("node.example.com")
1029 .cluster_preferred_endpoint_type("hostname")
1030 .cluster_replica_no_failover(true)
1031 .cluster_replica_validity_factor(0)
1032 .cluster_announce_ip("10.0.0.1")
1033 .cluster_announce_port(7000)
1034 .cluster_announce_bus_port(17000)
1035 .cluster_announce_tls_port(7100)
1036 .cluster_announce_human_nodename("node-1")
1037 .cluster_port(17000)
1038 .cluster_link_sendbuf_limit(67108864)
1039 .cluster_compatibility_sample_ratio(50)
1040 .cluster_slot_migration_handoff_max_lag_bytes(1048576)
1041 .cluster_slot_migration_write_pause_timeout(5000)
1042 .cluster_slot_stats_enabled(true);
1043 assert_eq!(b.cluster_node_timeout, Some(10000));
1044 assert_eq!(b.cluster_require_full_coverage, Some(false));
1045 assert_eq!(b.cluster_allow_reads_when_down, Some(true));
1046 assert_eq!(b.cluster_allow_pubsubshard_when_down, Some(true));
1047 assert_eq!(b.cluster_allow_replica_migration, Some(false));
1048 assert_eq!(b.cluster_migration_barrier, Some(2));
1049 assert_eq!(
1050 b.cluster_announce_hostname.as_deref(),
1051 Some("node.example.com")
1052 );
1053 assert_eq!(
1054 b.cluster_preferred_endpoint_type.as_deref(),
1055 Some("hostname")
1056 );
1057 assert_eq!(b.cluster_replica_no_failover, Some(true));
1058 assert_eq!(b.cluster_replica_validity_factor, Some(0));
1059 assert_eq!(b.cluster_announce_ip.as_deref(), Some("10.0.0.1"));
1060 assert_eq!(b.cluster_announce_port, Some(7000));
1061 assert_eq!(b.cluster_announce_bus_port, Some(17000));
1062 assert_eq!(b.cluster_announce_tls_port, Some(7100));
1063 assert_eq!(b.cluster_announce_human_nodename.as_deref(), Some("node-1"));
1064 assert_eq!(b.cluster_port, Some(17000));
1065 assert_eq!(b.cluster_link_sendbuf_limit, Some(67108864));
1066 assert_eq!(b.cluster_compatibility_sample_ratio, Some(50));
1067 assert_eq!(
1068 b.cluster_slot_migration_handoff_max_lag_bytes,
1069 Some(1048576)
1070 );
1071 assert_eq!(b.cluster_slot_migration_write_pause_timeout, Some(5000));
1072 assert_eq!(b.cluster_slot_stats_enabled, Some(true));
1073 }
1074
1075 #[test]
1076 fn builder_replication_directives() {
1077 let b = RedisCluster::builder()
1078 .min_replicas_to_write(1)
1079 .min_replicas_max_lag(10)
1080 .repl_diskless_sync(true)
1081 .repl_diskless_sync_delay(0)
1082 .repl_ping_replica_period(5)
1083 .repl_timeout(30);
1084 assert_eq!(b.min_replicas_to_write, Some(1));
1085 assert_eq!(b.min_replicas_max_lag, Some(10));
1086 assert_eq!(b.repl_diskless_sync, Some(true));
1087 assert_eq!(b.repl_diskless_sync_delay, Some(0));
1088 assert_eq!(b.repl_ping_replica_period, Some(5));
1089 assert_eq!(b.repl_timeout, Some(30));
1090 }
1091
1092 #[test]
1093 fn builder_logfile_and_extra() {
1094 let b = RedisCluster::builder()
1095 .logfile("/tmp/cluster.log")
1096 .extra("maxmemory", "10mb");
1097 assert_eq!(b.logfile.as_deref(), Some("/tmp/cluster.log"));
1098 assert_eq!(b.extra.get("maxmemory").map(String::as_str), Some("10mb"));
1099 }
1100}