1use std::io::{BufRead, BufReader, Write};
45use std::net::{SocketAddr, TcpListener, TcpStream};
46use std::sync::Arc;
47use std::sync::atomic::{AtomicBool, Ordering};
48use std::thread::JoinHandle;
49use std::time::Duration;
50
51use crate::ack_policy::AckPolicy;
52use crate::policy_control::{AckPolicyControl, Refused, RequestOutcome};
53use crate::promotion::PromotionRequest;
54
55use ed25519_dalek::{Verifier, VerifyingKey};
56use tracing::{debug, error, info, warn};
57
58use melin_app::auth::AuthorizedKeys;
59use melin_wire_protocol::control::TransportResponse;
60use melin_wire_protocol::control_codec;
61
62pub fn spawn(
78 bind_addr: SocketAddr,
79 promote: Option<PromotionRequest>,
80 rotate_requested: Option<Arc<AtomicBool>>,
81 ack_policy: Option<Arc<AckPolicyControl>>,
82 shutdown: Arc<AtomicBool>,
83 authorized_keys: Arc<AuthorizedKeys>,
84) -> Result<(JoinHandle<()>, SocketAddr), Box<dyn std::error::Error>> {
85 let listener = TcpListener::bind(bind_addr)
86 .map_err(|e| format!("failed to bind admin listener on {bind_addr}: {e}"))?;
87 listener
88 .set_nonblocking(true)
89 .map_err(|e| format!("failed to set non-blocking on admin listener: {e}"))?;
90 let addr = listener
91 .local_addr()
92 .map_err(|e| format!("failed to read admin listener address: {e}"))?;
93 let handle = std::thread::Builder::new()
94 .name("admin-listener".into())
95 .spawn(move || {
96 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
97 run(
98 listener,
99 addr,
100 promote.as_ref(),
101 rotate_requested.as_deref(),
102 ack_policy.as_deref(),
103 &shutdown,
104 &authorized_keys,
105 )
106 }));
107 if let Err(panic) = result {
108 let msg = panic_message(&panic);
109 error!(addr = %addr, panic = %msg, "admin listener thread panicked");
110 }
111 })
112 .map_err(|e| format!("failed to spawn admin listener thread: {e}"))?;
113 Ok((handle, addr))
114}
115
116fn panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
117 if let Some(s) = payload.downcast_ref::<&'static str>() {
118 (*s).to_string()
119 } else if let Some(s) = payload.downcast_ref::<String>() {
120 s.clone()
121 } else {
122 "<non-string panic payload>".to_string()
123 }
124}
125
126fn run(
129 listener: TcpListener,
130 addr: SocketAddr,
131 promote: Option<&PromotionRequest>,
132 rotate_requested: Option<&AtomicBool>,
133 ack_policy: Option<&AckPolicyControl>,
134 shutdown: &AtomicBool,
135 authorized_keys: &AuthorizedKeys,
136) {
137 info!(
138 addr = %addr,
139 promote_enabled = promote.is_some(),
140 rotate_enabled = rotate_requested.is_some(),
141 ack_policy_enabled = ack_policy.is_some(),
142 "admin listener started"
143 );
144
145 loop {
146 if shutdown.load(Ordering::Relaxed) {
147 return;
148 }
149
150 match listener.accept() {
151 Ok((stream, peer)) => {
152 debug!(peer = %peer, "admin connection accepted");
153 handle_connection(
154 stream,
155 promote,
156 rotate_requested,
157 ack_policy,
158 authorized_keys,
159 );
160 }
161 Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
162 std::thread::sleep(Duration::from_millis(100));
163 }
164 Err(e) => {
165 debug!(error = %e, "admin listener accept error");
166 std::thread::sleep(Duration::from_millis(100));
167 }
168 }
169 }
170}
171
172fn authenticate(stream: &mut TcpStream, authorized_keys: &AuthorizedKeys) -> Result<(), String> {
176 let mut nonce = [0u8; 32];
177 getrandom::fill(&mut nonce).map_err(|e| format!("getrandom failed: {e}"))?;
178
179 let mut buf = [0u8; 128];
180 let written =
181 control_codec::encode_transport_response(&TransportResponse::Challenge { nonce }, &mut buf)
182 .map_err(|e| format!("encode Challenge: {e}"))?;
183 stream
184 .write_all(&buf[..written])
185 .map_err(|e| format!("send Challenge: {e}"))?;
186 stream
187 .flush()
188 .map_err(|e| format!("flush Challenge: {e}"))?;
189
190 let mut len_buf = [0u8; 4];
191 std::io::Read::read_exact(stream, &mut len_buf)
192 .map_err(|e| format!("read auth frame length: {e}"))?;
193 let frame_len = u32::from_le_bytes(len_buf) as usize;
194 if frame_len > 256 {
195 send_auth_failed(stream);
196 return Err(format!("auth frame too large: {frame_len}"));
197 }
198 let mut frame_buf = [0u8; 256];
199 std::io::Read::read_exact(stream, &mut frame_buf[..frame_len])
200 .map_err(|e| format!("read auth frame payload: {e}"))?;
201
202 let cr = match control_codec::decode_challenge_response(&frame_buf[..frame_len]) {
203 Ok(cr) => cr,
204 Err(e) => {
205 send_auth_failed(stream);
206 return Err(format!("decode ChallengeResponse: {e}"));
207 }
208 };
209
210 let (signature_bytes, public_key_bytes) = (cr.signature, cr.public_key);
211
212 let role = match authorized_keys.lookup(&public_key_bytes) {
213 Some(role) => role,
214 None => {
215 send_auth_failed(stream);
216 return Err("unknown public key".into());
217 }
218 };
219 if !role.client().is_some_and(|role| role.is_operator()) {
220 send_auth_failed(stream);
221 return Err(format!(
222 "admin endpoint requires an operator key, got a {} key",
223 authorized_keys.token(role)
224 ));
225 }
226
227 let verifying_key = VerifyingKey::from_bytes(&public_key_bytes).map_err(|e| {
228 send_auth_failed(stream);
229 format!("invalid public key: {e}")
230 })?;
231 let signature = ed25519_dalek::Signature::from_bytes(&signature_bytes);
232 verifying_key.verify(&nonce, &signature).map_err(|e| {
233 send_auth_failed(stream);
234 format!("signature verification failed: {e}")
235 })?;
236
237 let written =
238 control_codec::encode_transport_response(&TransportResponse::ServerReady, &mut buf)
239 .map_err(|e| format!("encode ServerReady: {e}"))?;
240 stream
241 .write_all(&buf[..written])
242 .map_err(|e| format!("send ServerReady: {e}"))?;
243 stream
244 .flush()
245 .map_err(|e| format!("flush ServerReady: {e}"))?;
246
247 Ok(())
248}
249
250fn send_auth_failed(stream: &mut TcpStream) {
251 let mut buf = [0u8; 8];
252 if let Ok(written) =
253 control_codec::encode_transport_response(&TransportResponse::AuthFailed, &mut buf)
254 {
255 send_best_effort(stream, &buf[..written]);
259 }
260}
261
262fn send_best_effort(stream: &mut TcpStream, payload: &[u8]) {
268 if let Err(e) = stream.write_all(payload) {
269 debug!(error = %e, "admin write failed");
270 return;
271 }
272 if let Err(e) = stream.flush() {
273 debug!(error = %e, "admin flush failed");
274 }
275}
276
277fn handle_connection(
280 mut stream: TcpStream,
281 promote: Option<&PromotionRequest>,
282 rotate_requested: Option<&AtomicBool>,
283 ack_policy: Option<&AckPolicyControl>,
284 authorized_keys: &AuthorizedKeys,
285) {
286 stream.set_read_timeout(Some(Duration::from_secs(5))).ok();
287 stream.set_write_timeout(Some(Duration::from_secs(5))).ok();
288
289 if let Err(reason) = authenticate(&mut stream, authorized_keys) {
290 debug!(reason = %reason, "admin auth failed");
291 return;
292 }
293
294 let cloned = match stream.try_clone() {
295 Ok(s) => s,
296 Err(e) => {
297 debug!(error = %e, "failed to clone admin stream");
298 return;
299 }
300 };
301 let mut reader = BufReader::new(cloned);
302
303 let mut line = String::new();
304 if reader.read_line(&mut line).is_err() {
305 debug!("failed to read from admin connection");
306 return;
307 }
308
309 let trimmed = line.trim();
310 match trimmed {
311 "PROMOTE" => match promote {
312 Some(request) => {
313 if request.request(PromotionRequest::MANUAL) {
317 info!("promotion triggered by operator");
318 } else {
319 debug!("PROMOTE received but a promotion is already in flight");
323 }
324 send_best_effort(&mut stream, b"OK\n");
325 }
326 None => {
327 send_best_effort(&mut stream, b"ERR PROMOTE not available on this node\n");
328 debug!("rejected PROMOTE — flag not wired (primary node?)");
329 }
330 },
331 "ROTATE" => match rotate_requested {
332 Some(flag) => {
333 flag.store(true, Ordering::Release);
334 send_best_effort(&mut stream, b"OK\n");
335 info!("rotation requested by operator");
336 }
337 None => {
338 send_best_effort(&mut stream, b"ERR ROTATE not available on this node\n");
339 debug!("rejected ROTATE — flag not wired");
340 }
341 },
342 cmd if cmd.starts_with("ACK-POLICY") => {
343 let mut parts = cmd.splitn(2, char::is_whitespace);
350 let _ = parts.next();
352 let arg = parts.next().map(str::trim).unwrap_or("");
353 handle_ack_policy(&mut stream, ack_policy, arg);
354 }
355 cmd if cmd.starts_with("DURABILITY") => {
356 let mut parts = cmd.splitn(2, char::is_whitespace);
363 let _ = parts.next();
364 let arg = parts.next().map(str::trim).unwrap_or("");
365 let mapped = AckPolicy::parse_legacy(arg).map(AckPolicy::as_str);
366 warn!(
367 received = %cmd,
368 "DURABILITY is deprecated and will be removed in the next minor release; use ACK-POLICY {}",
369 mapped.unwrap_or("<disk|ram|disk+ram|two-disks>")
370 );
371 handle_ack_policy(&mut stream, ack_policy, mapped.unwrap_or(arg));
372 }
373 other => {
374 debug!(received = %other, "unknown admin command");
375 send_best_effort(&mut stream, b"ERR unknown command\n");
376 }
377 }
378}
379
380fn handle_ack_policy(stream: &mut TcpStream, ack_policy: Option<&AckPolicyControl>, arg: &str) {
387 let Some(control) = ack_policy else {
388 send_best_effort(stream, b"ERR ACK-POLICY not available on this node\n");
389 debug!("rejected ACK-POLICY — policy mailbox not wired");
390 return;
391 };
392 if arg.is_empty() {
393 send_best_effort(
394 stream,
395 b"ERR ACK-POLICY requires a policy (disk|ram|disk+ram|two-disks)\n",
396 );
397 debug!("rejected ACK-POLICY — missing argument");
398 return;
399 }
400 let Some(next) = AckPolicy::parse(arg) else {
401 let mut buf = [0u8; 128];
406 let msg = format_unknown_policy(&mut buf, arg);
407 send_best_effort(stream, msg);
408 debug!(received = %arg, "rejected ACK-POLICY — unknown policy");
409 return;
410 };
411 let outcome = control.request(next);
412 send_best_effort(stream, ack_policy_reply(outcome));
413 match outcome {
414 RequestOutcome::Published => {
415 info!(
416 policy = next.as_str(),
417 "ack policy change requested by operator"
418 );
419 }
420 RequestOutcome::Staged => {
421 info!(
422 policy = next.as_str(),
423 "ack policy staged by operator: journaled when this node serves as primary"
424 );
425 }
426 RequestOutcome::Refused(Refused::RingFull) => {
427 warn!(
430 policy = next.as_str(),
431 "ack policy change refused: the input ring stayed full, so it could not be journaled in time"
432 );
433 }
434 RequestOutcome::Refused(Refused::ProducerStopped) => {
435 info!(
436 policy = next.as_str(),
437 "ack policy change refused: the node stopped serving as primary (stepping down or shutting down) before journaling it"
438 );
439 }
440 RequestOutcome::Refused(Refused::AnotherWaiting) => {
441 warn!(
444 policy = next.as_str(),
445 "ack policy change refused: another request was still waiting to be journaled"
446 );
447 }
448 RequestOutcome::Refused(Refused::NoReplicaCanAttach) => {
449 warn!(
452 policy = next.as_str(),
453 "ack policy change refused: the policy needs a replica and this node accepts none (no --replication-bind), so it could never be met"
454 );
455 }
456 }
457}
458
459fn ack_policy_reply(outcome: RequestOutcome) -> &'static [u8] {
464 match outcome {
465 RequestOutcome::Published => b"OK\n",
466 RequestOutcome::Staged => b"OK staged\n",
467 RequestOutcome::Refused(Refused::RingFull) => {
468 b"ERR ACK-POLICY not journaled: the input ring stayed full; retry once the pipeline drains\n"
469 }
470 RequestOutcome::Refused(Refused::ProducerStopped) => {
471 b"ERR ACK-POLICY not journaled: the node stopped serving as primary (stepping down or shutting down)\n"
472 }
473 RequestOutcome::Refused(Refused::AnotherWaiting) => {
474 b"ERR ACK-POLICY not journaled: another ACK-POLICY request is still waiting\n"
475 }
476 RequestOutcome::Refused(Refused::NoReplicaCanAttach) => {
477 b"ERR ACK-POLICY not journaled: this policy needs a replica and this node accepts none (no --replication-bind), so it could never be met\n"
478 }
479 }
480}
481
482fn format_unknown_policy<'a>(buf: &'a mut [u8], arg: &str) -> &'a [u8] {
487 use std::io::Write as _;
488 let mut cursor = std::io::Cursor::new(&mut buf[..]);
489 let _ = writeln!(
493 cursor,
494 "ERR ACK-POLICY unknown policy `{arg}` (expected disk|ram|disk+ram|two-disks)"
495 );
496 let n = cursor.position() as usize;
497 &cursor.into_inner()[..n]
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use std::io::{BufRead, BufReader, Read, Write};
504
505 use ed25519_dalek::{Signer, SigningKey};
506 use melin_wire_protocol::control_codec::{
507 TAG_AUTH_FAILED, TAG_CHALLENGE, TAG_CHALLENGE_RESPONSE, TAG_SERVER_READY,
508 };
509
510 use crate::test_roles::desk_keys;
511
512 fn operator_keys() -> (SigningKey, Arc<AuthorizedKeys>) {
513 let signing_key = SigningKey::from_bytes(&[0xAD; 32]);
514 let pub_b64 = base64::Engine::encode(
515 &base64::engine::general_purpose::STANDARD,
516 signing_key.verifying_key().to_bytes(),
517 );
518 (signing_key, Arc::new(desk_keys("operator", &pub_b64)))
519 }
520
521 fn trader_keys() -> (SigningKey, Arc<AuthorizedKeys>) {
524 let signing_key = SigningKey::from_bytes(&[0xBD; 32]);
525 let pub_b64 = base64::Engine::encode(
526 &base64::engine::general_purpose::STANDARD,
527 signing_key.verifying_key().to_bytes(),
528 );
529 (signing_key, Arc::new(desk_keys("trader", &pub_b64)))
530 }
531
532 fn client_authenticate(stream: &mut TcpStream, key: &SigningKey) -> u8 {
537 let mut len_buf = [0u8; 4];
539 stream.read_exact(&mut len_buf).expect("read challenge len");
540 let frame_len = u32::from_le_bytes(len_buf) as usize;
541 let mut frame_buf = vec![0u8; frame_len];
542 stream
543 .read_exact(&mut frame_buf)
544 .expect("read challenge payload");
545 assert_eq!(frame_buf[0], TAG_CHALLENGE, "expected Challenge");
546 let nonce = &frame_buf[1..33];
547
548 let signature = key.sign(nonce);
551 let mut frame = Vec::with_capacity(97);
552 frame.push(TAG_CHALLENGE_RESPONSE);
553 frame.extend_from_slice(&signature.to_bytes());
554 frame.extend_from_slice(&key.verifying_key().to_bytes());
555 stream
556 .write_all(&(frame.len() as u32).to_le_bytes())
557 .expect("send result len");
558 stream.write_all(&frame).expect("send");
559 stream.flush().expect("flush");
560
561 let mut len_buf2 = [0u8; 4];
563 stream.read_exact(&mut len_buf2).expect("read result len");
564 let result_len = u32::from_le_bytes(len_buf2) as usize;
565 let mut result_buf = vec![0u8; result_len];
566 stream
567 .read_exact(&mut result_buf)
568 .expect("read result payload");
569 result_buf[0]
570 }
571
572 fn send_command(addr: SocketAddr, key: &SigningKey, command: &[u8]) -> String {
575 let mut stream = TcpStream::connect(addr).unwrap();
576 assert_eq!(client_authenticate(&mut stream, key), TAG_SERVER_READY);
577 stream.write_all(command).unwrap();
578 stream.flush().unwrap();
579 let mut reader = BufReader::new(stream);
580 let mut line = String::new();
581 reader.read_line(&mut line).unwrap();
582 line.trim().to_string()
583 }
584
585 #[test]
590 fn spawn_errs_when_port_taken() {
591 let (_key, auth_keys) = operator_keys();
592 let holder = TcpListener::bind("127.0.0.1:0").unwrap();
593 let addr = holder.local_addr().unwrap();
594 let shutdown = Arc::new(AtomicBool::new(false));
595 let err = spawn(addr, None, None, None, shutdown, auth_keys)
596 .expect_err("bind on a taken port must fail");
597 assert!(
598 err.to_string().contains("failed to bind admin listener"),
599 "{err}"
600 );
601 }
602
603 #[test]
604 fn promote_command_sets_flag_when_wired() {
605 let (key, auth_keys) = operator_keys();
606 let promote = PromotionRequest::new();
607 let rotate = Arc::new(AtomicBool::new(false));
608 let shutdown = Arc::new(AtomicBool::new(false));
609 let (_h, addr) = spawn(
610 "127.0.0.1:0".parse().unwrap(),
611 Some(promote.clone()),
612 Some(Arc::clone(&rotate)),
613 None,
614 Arc::clone(&shutdown),
615 auth_keys,
616 )
617 .expect("spawn admin listener");
618
619 assert_eq!(send_command(addr, &key, b"PROMOTE\n"), "OK");
620 assert_eq!(promote.pending(), Some(PromotionRequest::MANUAL));
621 assert!(!rotate.load(Ordering::Acquire));
622
623 shutdown.store(true, Ordering::Release);
624 }
625
626 #[test]
627 fn rotate_command_sets_flag_when_wired() {
628 let (key, auth_keys) = operator_keys();
629 let promote = PromotionRequest::new();
630 let rotate = Arc::new(AtomicBool::new(false));
631 let shutdown = Arc::new(AtomicBool::new(false));
632 let (_h, addr) = spawn(
633 "127.0.0.1:0".parse().unwrap(),
634 Some(promote.clone()),
635 Some(Arc::clone(&rotate)),
636 None,
637 Arc::clone(&shutdown),
638 auth_keys,
639 )
640 .expect("spawn admin listener");
641
642 assert_eq!(send_command(addr, &key, b"ROTATE\n"), "OK");
643 assert!(rotate.load(Ordering::Acquire));
644 assert!(!promote.is_requested());
645
646 shutdown.store(true, Ordering::Release);
647 }
648
649 #[test]
652 fn promote_rejected_when_not_wired() {
653 let (key, auth_keys) = operator_keys();
654 let rotate = Arc::new(AtomicBool::new(false));
655 let shutdown = Arc::new(AtomicBool::new(false));
656 let (_h, addr) = spawn(
657 "127.0.0.1:0".parse().unwrap(),
658 None,
659 Some(Arc::clone(&rotate)),
660 None,
661 Arc::clone(&shutdown),
662 auth_keys,
663 )
664 .expect("spawn admin listener");
665
666 let resp = send_command(addr, &key, b"PROMOTE\n");
667 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
668 assert!(!rotate.load(Ordering::Acquire));
669
670 shutdown.store(true, Ordering::Release);
671 }
672
673 #[test]
675 fn rotate_rejected_when_not_wired() {
676 let (key, auth_keys) = operator_keys();
677 let promote = PromotionRequest::new();
678 let shutdown = Arc::new(AtomicBool::new(false));
679 let (_h, addr) = spawn(
680 "127.0.0.1:0".parse().unwrap(),
681 Some(promote.clone()),
682 None,
683 None,
684 Arc::clone(&shutdown),
685 auth_keys,
686 )
687 .expect("spawn admin listener");
688
689 let resp = send_command(addr, &key, b"ROTATE\n");
690 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
691 assert!(!promote.is_requested());
692
693 shutdown.store(true, Ordering::Release);
694 }
695
696 #[test]
700 fn listener_handles_multiple_commands() {
701 let (key, auth_keys) = operator_keys();
702 let promote = PromotionRequest::new();
703 let rotate = Arc::new(AtomicBool::new(false));
704 let shutdown = Arc::new(AtomicBool::new(false));
705 let (_h, addr) = spawn(
706 "127.0.0.1:0".parse().unwrap(),
707 Some(promote.clone()),
708 Some(Arc::clone(&rotate)),
709 None,
710 Arc::clone(&shutdown),
711 auth_keys,
712 )
713 .expect("spawn admin listener");
714
715 for _ in 0..3 {
718 assert_eq!(send_command(addr, &key, b"ROTATE\n"), "OK");
719 assert!(rotate.load(Ordering::Acquire));
720 rotate
721 .compare_exchange(true, false, Ordering::AcqRel, Ordering::Relaxed)
722 .expect("flag should still be set");
723 std::thread::sleep(Duration::from_millis(100));
724 }
725
726 assert_eq!(send_command(addr, &key, b"PROMOTE\n"), "OK");
728 assert_eq!(promote.pending(), Some(PromotionRequest::MANUAL));
729
730 shutdown.store(true, Ordering::Release);
731 }
732
733 #[test]
734 fn unknown_command_rejected() {
735 let (key, auth_keys) = operator_keys();
736 let promote = PromotionRequest::new();
737 let rotate = Arc::new(AtomicBool::new(false));
738 let shutdown = Arc::new(AtomicBool::new(false));
739 let (_h, addr) = spawn(
740 "127.0.0.1:0".parse().unwrap(),
741 Some(promote.clone()),
742 Some(Arc::clone(&rotate)),
743 None,
744 Arc::clone(&shutdown),
745 auth_keys,
746 )
747 .expect("spawn admin listener");
748
749 let resp = send_command(addr, &key, b"INVALID\n");
750 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
751 assert!(!promote.is_requested());
752 assert!(!rotate.load(Ordering::Acquire));
753
754 shutdown.store(true, Ordering::Release);
755 }
756
757 #[test]
758 fn non_operator_key_rejected() {
759 let (trader_key, auth_keys) = trader_keys();
760 let promote = PromotionRequest::new();
761 let rotate = Arc::new(AtomicBool::new(false));
762 let shutdown = Arc::new(AtomicBool::new(false));
763 let (_h, addr) = spawn(
764 "127.0.0.1:0".parse().unwrap(),
765 Some(promote.clone()),
766 Some(Arc::clone(&rotate)),
767 None,
768 Arc::clone(&shutdown),
769 auth_keys,
770 )
771 .expect("spawn admin listener");
772
773 let mut stream = TcpStream::connect(addr).unwrap();
774 let result = client_authenticate(&mut stream, &trader_key);
775 assert_eq!(result, TAG_AUTH_FAILED);
776 assert!(!promote.is_requested());
777 assert!(!rotate.load(Ordering::Acquire));
778
779 shutdown.store(true, Ordering::Release);
780 }
781
782 fn spawn_with_policy(
784 control: &Arc<AckPolicyControl>,
785 ) -> (SigningKey, SocketAddr, Arc<AtomicBool>) {
786 let (key, auth_keys) = operator_keys();
787 let shutdown = Arc::new(AtomicBool::new(false));
788 let (_h, addr) = spawn(
789 "127.0.0.1:0".parse().unwrap(),
790 None,
791 None,
792 Some(Arc::clone(control)),
793 Arc::clone(&shutdown),
794 auth_keys,
795 )
796 .expect("spawn admin listener");
797 (key, addr, shutdown)
798 }
799
800 fn run_ack_policy(initial: AckPolicy, cmd: &[u8]) -> (String, AckPolicy) {
805 run_ack_policy_on(
806 crate::policy_control::test_support::replicated(initial),
807 cmd,
808 )
809 }
810
811 fn run_ack_policy_on(control: Arc<AckPolicyControl>, cmd: &[u8]) -> (String, AckPolicy) {
813 let (key, addr, shutdown) = spawn_with_policy(&control);
814 let resp = send_command(addr, &key, cmd);
815 let after = control.claim().policy();
816 shutdown.store(true, Ordering::Release);
817 (resp, after)
818 }
819
820 #[test]
821 fn ack_policy_command_stages_the_policy_with_no_producer() {
822 let (resp, after) = run_ack_policy(AckPolicy::DiskAndRam, b"ACK-POLICY disk\n");
823 assert_eq!(resp, "OK staged");
824 assert_eq!(after, AckPolicy::Disk);
825 }
826
827 #[test]
830 fn ack_policy_command_refuses_a_policy_no_replica_can_meet() {
831 let standalone =
832 || AckPolicyControl::new(AckPolicy::Disk, crate::halt::Replication::Disabled);
833 let (resp, after) = run_ack_policy_on(standalone(), b"ACK-POLICY disk+ram\n");
834 assert_eq!(
835 format!("{resp}\n").as_bytes(),
836 ack_policy_reply(RequestOutcome::Refused(Refused::NoReplicaCanAttach)),
837 "{resp}"
838 );
839 assert_eq!(after, AckPolicy::Disk, "nothing staged");
840 let (resp, _) = run_ack_policy_on(standalone(), b"ACK-POLICY disk\n");
841 assert_eq!(resp, "OK staged", "`disk` is still taken");
842 }
843
844 #[test]
847 fn ack_policy_command_is_journaled_by_the_producer() {
848 let control = crate::policy_control::test_support::replicated(AckPolicy::DiskAndRam);
849 let (mut producer, mut consumer) =
850 crate::policy_control::test_support::ring::<counter_server::CounterEvent>(8);
851 let replicas = crate::halt::test_support::presence(0);
853 let halt = crate::halt::test_support::gate_over(Some(replicas));
854 let mut publisher = crate::policy_control::test_support::booted(&control).attach(
855 &mut producer,
856 &halt,
857 None,
858 );
859 let (key, addr, shutdown) = spawn_with_policy(&control);
860 let polling = Arc::new(AtomicBool::new(true));
861 let poller = {
862 let polling = Arc::clone(&polling);
863 std::thread::spawn(move || {
864 while polling.load(Ordering::Relaxed) {
865 publisher.poll(&mut producer, &halt);
866 std::thread::yield_now();
867 }
868 })
869 };
870
871 assert_eq!(send_command(addr, &key, b"ACK-POLICY disk\n"), "OK");
872 polling.store(false, Ordering::Relaxed);
873 poller.join().expect("the producer panicked");
874 let (_, slot) = consumer.try_consume().expect("the entry is on the ring");
875 let melin_journal::JournalEvent::AckPolicyChange(change) = slot.event else {
876 panic!("not a policy change");
877 };
878 assert_eq!(
879 change,
880 melin_journal::AckPolicyChange {
881 policy: AckPolicy::Disk.to_byte(),
882 unwitnessed: true,
883 },
884 "a runtime `disk` with no replica connected is forced"
885 );
886 shutdown.store(true, Ordering::Release);
887 }
888
889 #[test]
892 fn ack_policy_command_refused_when_it_cannot_be_journaled() {
893 use crate::policy_control::test_support::{attach, control_with_publish_wait, ring};
894 let control = control_with_publish_wait(AckPolicy::DiskAndRam, Duration::from_millis(50));
895 let (mut producer, _consumer) = ring::<counter_server::CounterEvent>(8);
896 let _publisher = attach(&control, &mut producer);
897 let (key, addr, shutdown) = spawn_with_policy(&control);
898 let resp = send_command(addr, &key, b"ACK-POLICY disk\n");
899 assert_eq!(
900 format!("{resp}\n").as_bytes(),
901 ack_policy_reply(RequestOutcome::Refused(Refused::RingFull)),
902 "{resp}"
903 );
904 assert_eq!(control.requested(), AckPolicy::DiskAndRam);
905 shutdown.store(true, Ordering::Release);
906 }
907
908 #[test]
910 fn the_ack_policy_reply_names_each_outcome() {
911 let reply = |outcome| std::str::from_utf8(ack_policy_reply(outcome)).expect("ASCII");
912 assert_eq!(reply(RequestOutcome::Published), "OK\n");
913 assert_eq!(reply(RequestOutcome::Staged), "OK staged\n");
914 for (cause, names) in [
915 (Refused::RingFull, "the input ring stayed full"),
916 (
917 Refused::ProducerStopped,
918 "stopped serving as primary (stepping down or shutting down)",
919 ),
920 (Refused::AnotherWaiting, "another ACK-POLICY request"),
921 (
922 Refused::NoReplicaCanAttach,
923 "needs a replica and this node accepts none (no --replication-bind), so it could never be met",
924 ),
925 ] {
926 let text = reply(RequestOutcome::Refused(cause));
927 assert!(
928 text.starts_with("ERR ACK-POLICY not journaled: ") && text.contains(names),
929 "{cause:?}: {text}"
930 );
931 assert!(text.ends_with('\n') && !text[..text.len() - 1].contains('\n'));
932 }
933 }
934
935 #[test]
936 fn deprecated_durability_alias_maps_old_names() {
937 let (resp, after) = run_ack_policy(AckPolicy::DiskAndRam, b"DURABILITY local\n");
940 assert_eq!(resp, "OK staged");
941 assert_eq!(after, AckPolicy::Disk);
942
943 let (resp, after) = run_ack_policy(AckPolicy::Disk, b"DURABILITY durably-replicated\n");
944 assert_eq!(resp, "OK staged");
945 assert_eq!(after, AckPolicy::TwoDisks);
946
947 let (resp, after) = run_ack_policy(AckPolicy::Disk, b"DURABILITY ram\n");
949 assert_eq!(resp, "OK staged");
950 assert_eq!(after, AckPolicy::Ram);
951 let (resp, after) = run_ack_policy(AckPolicy::Disk, b"DURABILITY fast\n");
952 assert!(resp.starts_with("ERR ACK-POLICY unknown policy"), "{resp}");
953 assert_eq!(after, AckPolicy::Disk);
954 }
955
956 #[test]
957 fn ack_policy_command_accepts_each_policy() {
958 for target in [
959 AckPolicy::Disk,
960 AckPolicy::Ram,
961 AckPolicy::DiskAndRam,
962 AckPolicy::TwoDisks,
963 ] {
964 let cmd = format!("ACK-POLICY {}\n", target.as_str());
965 let (resp, after) = run_ack_policy(AckPolicy::Disk, cmd.as_bytes());
966 assert_eq!(resp, "OK staged", "policy {target}");
967 assert_eq!(after, target);
968 }
969 }
970
971 #[test]
972 fn ack_policy_command_rejects_unknown_policy() {
973 let (resp, after) = run_ack_policy(AckPolicy::DiskAndRam, b"ACK-POLICY fast\n");
974 assert!(
975 resp.starts_with("ERR ACK-POLICY unknown policy"),
976 "expected unknown-policy ERR, got {resp}"
977 );
978 assert_eq!(after, AckPolicy::DiskAndRam);
980 }
981
982 #[test]
983 fn ack_policy_command_rejects_missing_argument() {
984 let (resp, after) = run_ack_policy(AckPolicy::DiskAndRam, b"ACK-POLICY\n");
985 assert!(
986 resp.starts_with("ERR ACK-POLICY requires a policy"),
987 "expected missing-arg ERR, got {resp}"
988 );
989 assert_eq!(after, AckPolicy::DiskAndRam);
990 }
991
992 #[test]
993 fn ack_policy_command_rejected_when_not_wired() {
994 let (key, auth_keys) = operator_keys();
997 let promote = PromotionRequest::new();
998 let shutdown = Arc::new(AtomicBool::new(false));
999 let (_h, addr) = spawn(
1000 "127.0.0.1:0".parse().unwrap(),
1001 Some(promote.clone()),
1002 None,
1003 None,
1004 Arc::clone(&shutdown),
1005 auth_keys,
1006 )
1007 .expect("spawn admin listener");
1008
1009 let resp = send_command(addr, &key, b"ACK-POLICY disk\n");
1010 assert!(
1011 resp.starts_with("ERR ACK-POLICY not available"),
1012 "expected not-available ERR, got {resp}"
1013 );
1014
1015 shutdown.store(true, Ordering::Release);
1016 }
1017}