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_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];
541 stream.read_exact(&mut len_buf).expect("read challenge len");
542 let frame_len = u32::from_le_bytes(len_buf) as usize;
543 let mut frame_buf = vec![0u8; frame_len];
544 stream
545 .read_exact(&mut frame_buf)
546 .expect("read challenge payload");
547 let nonce = melin_wire_protocol::control_codec::decode_challenge(&frame_buf)
548 .expect("expected Challenge");
549
550 let signature = key.sign(&nonce);
553 let mut frame = Vec::with_capacity(97);
554 frame.push(TAG_CHALLENGE_RESPONSE);
555 frame.extend_from_slice(&signature.to_bytes());
556 frame.extend_from_slice(&key.verifying_key().to_bytes());
557 stream
558 .write_all(&(frame.len() as u32).to_le_bytes())
559 .expect("send result len");
560 stream.write_all(&frame).expect("send");
561 stream.flush().expect("flush");
562
563 let mut len_buf2 = [0u8; 4];
565 stream.read_exact(&mut len_buf2).expect("read result len");
566 let result_len = u32::from_le_bytes(len_buf2) as usize;
567 let mut result_buf = vec![0u8; result_len];
568 stream
569 .read_exact(&mut result_buf)
570 .expect("read result payload");
571 result_buf[0]
572 }
573
574 fn send_command(addr: SocketAddr, key: &SigningKey, command: &[u8]) -> String {
577 let mut stream = TcpStream::connect(addr).unwrap();
578 assert_eq!(client_authenticate(&mut stream, key), TAG_SERVER_READY);
579 stream.write_all(command).unwrap();
580 stream.flush().unwrap();
581 let mut reader = BufReader::new(stream);
582 let mut line = String::new();
583 reader.read_line(&mut line).unwrap();
584 line.trim().to_string()
585 }
586
587 #[test]
592 fn spawn_errs_when_port_taken() {
593 let (_key, auth_keys) = operator_keys();
594 let holder = TcpListener::bind("127.0.0.1:0").unwrap();
595 let addr = holder.local_addr().unwrap();
596 let shutdown = Arc::new(AtomicBool::new(false));
597 let err = spawn(addr, None, None, None, shutdown, auth_keys)
598 .expect_err("bind on a taken port must fail");
599 assert!(
600 err.to_string().contains("failed to bind admin listener"),
601 "{err}"
602 );
603 }
604
605 #[test]
606 fn promote_command_sets_flag_when_wired() {
607 let (key, auth_keys) = operator_keys();
608 let promote = PromotionRequest::new();
609 let rotate = Arc::new(AtomicBool::new(false));
610 let shutdown = Arc::new(AtomicBool::new(false));
611 let (_h, addr) = spawn(
612 "127.0.0.1:0".parse().unwrap(),
613 Some(promote.clone()),
614 Some(Arc::clone(&rotate)),
615 None,
616 Arc::clone(&shutdown),
617 auth_keys,
618 )
619 .expect("spawn admin listener");
620
621 assert_eq!(send_command(addr, &key, b"PROMOTE\n"), "OK");
622 assert_eq!(promote.pending(), Some(PromotionRequest::MANUAL));
623 assert!(!rotate.load(Ordering::Acquire));
624
625 shutdown.store(true, Ordering::Release);
626 }
627
628 #[test]
629 fn rotate_command_sets_flag_when_wired() {
630 let (key, auth_keys) = operator_keys();
631 let promote = PromotionRequest::new();
632 let rotate = Arc::new(AtomicBool::new(false));
633 let shutdown = Arc::new(AtomicBool::new(false));
634 let (_h, addr) = spawn(
635 "127.0.0.1:0".parse().unwrap(),
636 Some(promote.clone()),
637 Some(Arc::clone(&rotate)),
638 None,
639 Arc::clone(&shutdown),
640 auth_keys,
641 )
642 .expect("spawn admin listener");
643
644 assert_eq!(send_command(addr, &key, b"ROTATE\n"), "OK");
645 assert!(rotate.load(Ordering::Acquire));
646 assert!(!promote.is_requested());
647
648 shutdown.store(true, Ordering::Release);
649 }
650
651 #[test]
654 fn promote_rejected_when_not_wired() {
655 let (key, auth_keys) = operator_keys();
656 let rotate = Arc::new(AtomicBool::new(false));
657 let shutdown = Arc::new(AtomicBool::new(false));
658 let (_h, addr) = spawn(
659 "127.0.0.1:0".parse().unwrap(),
660 None,
661 Some(Arc::clone(&rotate)),
662 None,
663 Arc::clone(&shutdown),
664 auth_keys,
665 )
666 .expect("spawn admin listener");
667
668 let resp = send_command(addr, &key, b"PROMOTE\n");
669 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
670 assert!(!rotate.load(Ordering::Acquire));
671
672 shutdown.store(true, Ordering::Release);
673 }
674
675 #[test]
677 fn rotate_rejected_when_not_wired() {
678 let (key, auth_keys) = operator_keys();
679 let promote = PromotionRequest::new();
680 let shutdown = Arc::new(AtomicBool::new(false));
681 let (_h, addr) = spawn(
682 "127.0.0.1:0".parse().unwrap(),
683 Some(promote.clone()),
684 None,
685 None,
686 Arc::clone(&shutdown),
687 auth_keys,
688 )
689 .expect("spawn admin listener");
690
691 let resp = send_command(addr, &key, b"ROTATE\n");
692 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
693 assert!(!promote.is_requested());
694
695 shutdown.store(true, Ordering::Release);
696 }
697
698 #[test]
702 fn listener_handles_multiple_commands() {
703 let (key, auth_keys) = operator_keys();
704 let promote = PromotionRequest::new();
705 let rotate = Arc::new(AtomicBool::new(false));
706 let shutdown = Arc::new(AtomicBool::new(false));
707 let (_h, addr) = spawn(
708 "127.0.0.1:0".parse().unwrap(),
709 Some(promote.clone()),
710 Some(Arc::clone(&rotate)),
711 None,
712 Arc::clone(&shutdown),
713 auth_keys,
714 )
715 .expect("spawn admin listener");
716
717 for _ in 0..3 {
720 assert_eq!(send_command(addr, &key, b"ROTATE\n"), "OK");
721 assert!(rotate.load(Ordering::Acquire));
722 rotate
723 .compare_exchange(true, false, Ordering::AcqRel, Ordering::Relaxed)
724 .expect("flag should still be set");
725 std::thread::sleep(Duration::from_millis(100));
726 }
727
728 assert_eq!(send_command(addr, &key, b"PROMOTE\n"), "OK");
730 assert_eq!(promote.pending(), Some(PromotionRequest::MANUAL));
731
732 shutdown.store(true, Ordering::Release);
733 }
734
735 #[test]
736 fn unknown_command_rejected() {
737 let (key, auth_keys) = operator_keys();
738 let promote = PromotionRequest::new();
739 let rotate = Arc::new(AtomicBool::new(false));
740 let shutdown = Arc::new(AtomicBool::new(false));
741 let (_h, addr) = spawn(
742 "127.0.0.1:0".parse().unwrap(),
743 Some(promote.clone()),
744 Some(Arc::clone(&rotate)),
745 None,
746 Arc::clone(&shutdown),
747 auth_keys,
748 )
749 .expect("spawn admin listener");
750
751 let resp = send_command(addr, &key, b"INVALID\n");
752 assert!(resp.starts_with("ERR"), "expected ERR, got {resp}");
753 assert!(!promote.is_requested());
754 assert!(!rotate.load(Ordering::Acquire));
755
756 shutdown.store(true, Ordering::Release);
757 }
758
759 #[test]
760 fn non_operator_key_rejected() {
761 let (trader_key, auth_keys) = trader_keys();
762 let promote = PromotionRequest::new();
763 let rotate = Arc::new(AtomicBool::new(false));
764 let shutdown = Arc::new(AtomicBool::new(false));
765 let (_h, addr) = spawn(
766 "127.0.0.1:0".parse().unwrap(),
767 Some(promote.clone()),
768 Some(Arc::clone(&rotate)),
769 None,
770 Arc::clone(&shutdown),
771 auth_keys,
772 )
773 .expect("spawn admin listener");
774
775 let mut stream = TcpStream::connect(addr).unwrap();
776 let result = client_authenticate(&mut stream, &trader_key);
777 assert_eq!(result, TAG_AUTH_FAILED);
778 assert!(!promote.is_requested());
779 assert!(!rotate.load(Ordering::Acquire));
780
781 shutdown.store(true, Ordering::Release);
782 }
783
784 fn spawn_with_policy(
786 control: &Arc<AckPolicyControl>,
787 ) -> (SigningKey, SocketAddr, Arc<AtomicBool>) {
788 let (key, auth_keys) = operator_keys();
789 let shutdown = Arc::new(AtomicBool::new(false));
790 let (_h, addr) = spawn(
791 "127.0.0.1:0".parse().unwrap(),
792 None,
793 None,
794 Some(Arc::clone(control)),
795 Arc::clone(&shutdown),
796 auth_keys,
797 )
798 .expect("spawn admin listener");
799 (key, addr, shutdown)
800 }
801
802 fn run_ack_policy(initial: AckPolicy, cmd: &[u8]) -> (String, AckPolicy) {
807 run_ack_policy_on(
808 crate::policy_control::test_support::replicated(initial),
809 cmd,
810 )
811 }
812
813 fn run_ack_policy_on(control: Arc<AckPolicyControl>, cmd: &[u8]) -> (String, AckPolicy) {
815 let (key, addr, shutdown) = spawn_with_policy(&control);
816 let resp = send_command(addr, &key, cmd);
817 let after = control.claim().policy();
818 shutdown.store(true, Ordering::Release);
819 (resp, after)
820 }
821
822 #[test]
823 fn ack_policy_command_stages_the_policy_with_no_producer() {
824 let (resp, after) = run_ack_policy(AckPolicy::DiskAndRam, b"ACK-POLICY disk\n");
825 assert_eq!(resp, "OK staged");
826 assert_eq!(after, AckPolicy::Disk);
827 }
828
829 #[test]
832 fn ack_policy_command_refuses_a_policy_no_replica_can_meet() {
833 let standalone =
834 || AckPolicyControl::new(AckPolicy::Disk, crate::halt::Replication::Disabled);
835 let (resp, after) = run_ack_policy_on(standalone(), b"ACK-POLICY disk+ram\n");
836 assert_eq!(
837 format!("{resp}\n").as_bytes(),
838 ack_policy_reply(RequestOutcome::Refused(Refused::NoReplicaCanAttach)),
839 "{resp}"
840 );
841 assert_eq!(after, AckPolicy::Disk, "nothing staged");
842 let (resp, _) = run_ack_policy_on(standalone(), b"ACK-POLICY disk\n");
843 assert_eq!(resp, "OK staged", "`disk` is still taken");
844 }
845
846 #[test]
849 fn ack_policy_command_is_journaled_by_the_producer() {
850 let control = crate::policy_control::test_support::replicated(AckPolicy::DiskAndRam);
851 let (mut producer, mut consumer) =
852 crate::policy_control::test_support::ring::<counter_server::CounterEvent>(8);
853 let replicas = crate::halt::test_support::presence(0);
855 let halt = crate::halt::test_support::gate_over(Some(replicas));
856 let mut publisher = crate::policy_control::test_support::booted(&control).attach(
857 &mut producer,
858 &halt,
859 None,
860 );
861 let (key, addr, shutdown) = spawn_with_policy(&control);
862 let polling = Arc::new(AtomicBool::new(true));
863 let poller = {
864 let polling = Arc::clone(&polling);
865 std::thread::spawn(move || {
866 while polling.load(Ordering::Relaxed) {
867 publisher.poll(&mut producer, &halt);
868 std::thread::yield_now();
869 }
870 })
871 };
872
873 assert_eq!(send_command(addr, &key, b"ACK-POLICY disk\n"), "OK");
874 polling.store(false, Ordering::Relaxed);
875 poller.join().expect("the producer panicked");
876 let (_, slot) = consumer.try_consume().expect("the entry is on the ring");
877 let melin_journal::JournalEvent::AckPolicyChange(change) = slot.event else {
878 panic!("not a policy change");
879 };
880 assert_eq!(
881 change,
882 melin_journal::AckPolicyChange {
883 policy: AckPolicy::Disk.to_byte(),
884 unwitnessed: true,
885 },
886 "a runtime `disk` with no replica connected is forced"
887 );
888 shutdown.store(true, Ordering::Release);
889 }
890
891 #[test]
894 fn ack_policy_command_refused_when_it_cannot_be_journaled() {
895 use crate::policy_control::test_support::{attach, control_with_publish_wait, ring};
896 let control = control_with_publish_wait(AckPolicy::DiskAndRam, Duration::from_millis(50));
897 let (mut producer, _consumer) = ring::<counter_server::CounterEvent>(8);
898 let _publisher = attach(&control, &mut producer);
899 let (key, addr, shutdown) = spawn_with_policy(&control);
900 let resp = send_command(addr, &key, b"ACK-POLICY disk\n");
901 assert_eq!(
902 format!("{resp}\n").as_bytes(),
903 ack_policy_reply(RequestOutcome::Refused(Refused::RingFull)),
904 "{resp}"
905 );
906 assert_eq!(control.requested(), AckPolicy::DiskAndRam);
907 shutdown.store(true, Ordering::Release);
908 }
909
910 #[test]
912 fn the_ack_policy_reply_names_each_outcome() {
913 let reply = |outcome| std::str::from_utf8(ack_policy_reply(outcome)).expect("ASCII");
914 assert_eq!(reply(RequestOutcome::Published), "OK\n");
915 assert_eq!(reply(RequestOutcome::Staged), "OK staged\n");
916 for (cause, names) in [
917 (Refused::RingFull, "the input ring stayed full"),
918 (
919 Refused::ProducerStopped,
920 "stopped serving as primary (stepping down or shutting down)",
921 ),
922 (Refused::AnotherWaiting, "another ACK-POLICY request"),
923 (
924 Refused::NoReplicaCanAttach,
925 "needs a replica and this node accepts none (no --replication-bind), so it could never be met",
926 ),
927 ] {
928 let text = reply(RequestOutcome::Refused(cause));
929 assert!(
930 text.starts_with("ERR ACK-POLICY not journaled: ") && text.contains(names),
931 "{cause:?}: {text}"
932 );
933 assert!(text.ends_with('\n') && !text[..text.len() - 1].contains('\n'));
934 }
935 }
936
937 #[test]
938 fn deprecated_durability_alias_maps_old_names() {
939 let (resp, after) = run_ack_policy(AckPolicy::DiskAndRam, b"DURABILITY local\n");
942 assert_eq!(resp, "OK staged");
943 assert_eq!(after, AckPolicy::Disk);
944
945 let (resp, after) = run_ack_policy(AckPolicy::Disk, b"DURABILITY durably-replicated\n");
946 assert_eq!(resp, "OK staged");
947 assert_eq!(after, AckPolicy::TwoDisks);
948
949 let (resp, after) = run_ack_policy(AckPolicy::Disk, b"DURABILITY ram\n");
951 assert_eq!(resp, "OK staged");
952 assert_eq!(after, AckPolicy::Ram);
953 let (resp, after) = run_ack_policy(AckPolicy::Disk, b"DURABILITY fast\n");
954 assert!(resp.starts_with("ERR ACK-POLICY unknown policy"), "{resp}");
955 assert_eq!(after, AckPolicy::Disk);
956 }
957
958 #[test]
959 fn ack_policy_command_accepts_each_policy() {
960 for target in [
961 AckPolicy::Disk,
962 AckPolicy::Ram,
963 AckPolicy::DiskAndRam,
964 AckPolicy::TwoDisks,
965 ] {
966 let cmd = format!("ACK-POLICY {}\n", target.as_str());
967 let (resp, after) = run_ack_policy(AckPolicy::Disk, cmd.as_bytes());
968 assert_eq!(resp, "OK staged", "policy {target}");
969 assert_eq!(after, target);
970 }
971 }
972
973 #[test]
974 fn ack_policy_command_rejects_unknown_policy() {
975 let (resp, after) = run_ack_policy(AckPolicy::DiskAndRam, b"ACK-POLICY fast\n");
976 assert!(
977 resp.starts_with("ERR ACK-POLICY unknown policy"),
978 "expected unknown-policy ERR, got {resp}"
979 );
980 assert_eq!(after, AckPolicy::DiskAndRam);
982 }
983
984 #[test]
985 fn ack_policy_command_rejects_missing_argument() {
986 let (resp, after) = run_ack_policy(AckPolicy::DiskAndRam, b"ACK-POLICY\n");
987 assert!(
988 resp.starts_with("ERR ACK-POLICY requires a policy"),
989 "expected missing-arg ERR, got {resp}"
990 );
991 assert_eq!(after, AckPolicy::DiskAndRam);
992 }
993
994 #[test]
995 fn ack_policy_command_rejected_when_not_wired() {
996 let (key, auth_keys) = operator_keys();
999 let promote = PromotionRequest::new();
1000 let shutdown = Arc::new(AtomicBool::new(false));
1001 let (_h, addr) = spawn(
1002 "127.0.0.1:0".parse().unwrap(),
1003 Some(promote.clone()),
1004 None,
1005 None,
1006 Arc::clone(&shutdown),
1007 auth_keys,
1008 )
1009 .expect("spawn admin listener");
1010
1011 let resp = send_command(addr, &key, b"ACK-POLICY disk\n");
1012 assert!(
1013 resp.starts_with("ERR ACK-POLICY not available"),
1014 "expected not-available ERR, got {resp}"
1015 );
1016
1017 shutdown.store(true, Ordering::Release);
1018 }
1019}