From f3690ed2cac700870c4825fdf63a102d9029fc51 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 26 Oct 2026 15:56:13 +0000 Subject: [PATCH] feat(registry): add Registar::deregister Add an additive async deregister(&ServiceBinding) that removes one registration and returns it, so a stopped service can register its final binding again. Bump torrust-server-lib from 0.3.0 to 0.4.0 for the additive public API, as the previous additive API did (0.2.0 to 0.3.0). Related to torrust/torrust-tracker#2450. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/registar.rs | 89 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f16799..905ff14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -722,7 +722,7 @@ dependencies = [ [[package]] name = "torrust-server-lib" -version = "0.3.0" +version = "0.4.0" dependencies = [ "derive_more", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 4296fd5..5e991ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ publish = true readme = "README.md" repository = "https://github.com/torrust/torrust-server-lib" rust-version = "1.88" -version = "0.3.0" +version = "0.4.0" [dependencies] derive_more = { version = "2", features = [ "as_ref", "constructor", "display", "from" ] } diff --git a/src/registar.rs b/src/registar.rs index b6e8261..d9abb1c 100644 --- a/src/registar.rs +++ b/src/registar.rs @@ -179,6 +179,24 @@ impl Registar { Ok(()) } + /// Removes the registration for a final binding and returns it. + /// + /// Returns `None` when no service is registered with that binding. A + /// service that stops should remove its own registration, so the same + /// binding can be registered again, for example when a stopped test + /// environment restarts on a fixed port. + /// + /// The registry is keyed by binding only, so this removes whichever + /// registration holds that binding. Callers should pass only a binding + /// they registered themselves. + pub async fn deregister(&self, service_binding: &ServiceBinding) -> Option> { + self.registry + .lock() + .await + .remove(service_binding) + .map(|registration| RegisteredService { registration }) + } + /// Returns a deterministic, side-effect-free snapshot of all services. /// /// Results are ordered by protocol, then final socket address, never by @@ -325,4 +343,75 @@ mod tests { assert_eq!(error, RegistrationError::DuplicateBinding(service_binding)); } + + #[tokio::test] + async fn it_should_remove_a_registration_and_return_it_when_deregistered() { + let registar = Registar::default(); + let service_binding = binding(Protocol::HTTP, 8000); + + registar + .give_form() + .register(ServiceRegistration::new(service_binding.clone(), "first", None)) + .await + .expect("registration should succeed"); + + let removed = registar.deregister(&service_binding).await; + + assert_eq!(removed.map(|service| *service.metadata()), Some("first")); + assert!(registar.services().await.is_empty()); + } + + #[tokio::test] + async fn it_should_allow_registering_a_binding_again_after_it_was_deregistered() { + let registar = Registar::default(); + let service_binding = binding(Protocol::HTTP, 8000); + + registar + .give_form() + .register(ServiceRegistration::new(service_binding.clone(), "first", None)) + .await + .expect("initial registration should succeed"); + registar.deregister(&service_binding).await; + + registar + .give_form() + .register(ServiceRegistration::new(service_binding, "second", None)) + .await + .expect("registration after deregistration should succeed"); + + assert_eq!(registar.services().await[0].metadata(), &"second"); + } + + #[tokio::test] + async fn it_should_return_none_when_the_binding_to_deregister_is_not_registered() { + let registar = Registar::<()>::default(); + + let removed = registar.deregister(&binding(Protocol::HTTP, 8000)).await; + + assert!(removed.is_none()); + } + + #[tokio::test] + async fn it_should_keep_the_other_registrations_when_one_is_deregistered() { + let registar = Registar::default(); + let removed_binding = binding(Protocol::HTTP, 8000); + + for (port, metadata) in [(8000, "removed"), (9000, "kept")] { + registar + .give_form() + .register(ServiceRegistration::new(binding(Protocol::HTTP, port), metadata, None)) + .await + .expect("registration should succeed"); + } + + registar.deregister(&removed_binding).await; + + let metadata: Vec<_> = registar + .services() + .await + .into_iter() + .map(|service| *service.metadata()) + .collect(); + assert_eq!(metadata, ["kept"]); + } }