Consolidate deferred environment provisioning APIs (#37340)

## What changed

- Remove the separate deferred registration handle, direct readiness publisher,
  and Noise environment upsert API from `EnvironmentManager`.
- Use `report_environment_provisioning_status` and
  `materialize_pending_noise_environment` as the provisioning flow while keeping
  ordinary environments isolated from provisioning reports.
- Cover readiness updates, invalid reports, duplicate materialization, and
  conflicts with ordinary environments in the deferred environment tests.

GitOrigin-RevId: d14207c0abb0636d331be4f875fec6e53f3d2fe6
This commit is contained in:
TAFOYA-OAI
2026-08-06 21:13:05 +00:00
committed by copyberry
parent d9eac10406
commit f8ac8fa6c6
3 changed files with 175 additions and 461 deletions

View File

@@ -89,13 +89,6 @@ pub struct EnvironmentReadyInfo {
pub selected_capability_roots: Vec<SelectedCapabilityRoot>,
}
/// The one-shot capability to complete a deferred environment registration.
#[must_use = "the deferred environment cannot connect until registration is completed"]
pub struct DeferredEnvironmentRegistration {
environment_id: String,
environment: Option<Arc<Environment>>,
}
/// Maximum capability roots accepted from environment ready information.
pub const MAX_SELECTED_CAPABILITY_ROOTS: usize = 256;
@@ -232,7 +225,16 @@ impl EnvironmentManager {
local_runtime_paths,
http_client_factory,
};
manager.upsert_noise_environment(REMOTE_ENVIRONMENT_ID.to_string(), connect_provider)?;
let identity = noise_channel_identity()?;
let environment = Arc::new(Environment::remote_with_transport(
ExecServerTransportParams::NoiseRendezvous {
provider: connect_provider,
identity,
},
manager.local_runtime_paths.clone(),
manager.http_client_factory.clone(),
));
manager.insert_environment(REMOTE_ENVIRONMENT_ID.to_string(), environment)?;
Ok(manager)
}
@@ -380,26 +382,6 @@ impl EnvironmentManager {
.cloned()
}
/// Publishes capability roots without changing an environment's provisioning state.
pub fn publish_ready_info(
&self,
environment_id: &str,
ready_info: EnvironmentReadyInfo,
) -> Result<(), ExecServerError> {
validate_environment_id(environment_id)?;
validate_environment_ready_info(environment_id, &ready_info)?;
let environments = self
.environments
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let environment = environments.get(environment_id).ok_or_else(|| {
ExecServerError::Protocol(format!("environment `{environment_id}` is not registered"))
})?;
environment.ready_info.store(Some(Arc::new(ready_info)));
Ok(())
}
/// Records a Ready or Failed provisioning result for an environment.
///
/// Ordinary environments are ignored. A provisioned environment keeps the same `Arc` from
@@ -489,22 +471,6 @@ impl EnvironmentManager {
self.insert_environment(environment_id, environment)
}
/// Adds or replaces a Noise environment completed through its registration handle.
pub fn register_deferred_noise_environment(
&self,
environment_id: String,
provider: Arc<dyn NoiseRendezvousConnectProvider>,
) -> Result<DeferredEnvironmentRegistration, ExecServerError> {
validate_environment_id(&environment_id)?;
let environment =
Arc::new(self.provisioning_noise_environment(provider, /*initial_result*/ None)?);
self.insert_environment(environment_id.clone(), Arc::clone(&environment))?;
Ok(DeferredEnvironmentRegistration {
environment_id,
environment: Some(environment),
})
}
/// Returns the stable environment for an ID, creating it as pending when absent.
pub fn materialize_pending_noise_environment(
&self,
@@ -548,22 +514,6 @@ impl EnvironmentManager {
Ok(environment)
}
/// Adds or replaces a named remote environment using authenticated Noise rendezvous.
pub fn upsert_noise_environment(
&self,
environment_id: String,
provider: Arc<dyn NoiseRendezvousConnectProvider>,
) -> Result<(), ExecServerError> {
validate_environment_id(&environment_id)?;
let identity = noise_channel_identity()?;
let environment = Arc::new(Environment::remote_with_transport(
ExecServerTransportParams::NoiseRendezvous { provider, identity },
self.local_runtime_paths.clone(),
self.http_client_factory.clone(),
));
self.insert_environment(environment_id, environment)
}
fn insert_environment(
&self,
environment_id: String,
@@ -582,36 +532,6 @@ impl EnvironmentManager {
}
}
impl DeferredEnvironmentRegistration {
/// Completes provisioning with ready information or a terminal error message.
pub fn complete(
mut self,
result: Result<EnvironmentReadyInfo, String>,
) -> Result<(), ExecServerError> {
let Some(environment) = self.environment.take() else {
return Err(ExecServerError::Disconnected(
"deferred environment registration is inactive".into(),
));
};
match result {
Ok(ready_info) => environment.apply_ready_report(&self.environment_id, ready_info),
Err(error) => environment.apply_error_report(&self.environment_id, error),
}
}
}
impl Drop for DeferredEnvironmentRegistration {
fn drop(&mut self) {
if let Some(environment) = self.environment.take() {
let _ = environment.apply_error_report(
&self.environment_id,
"environment registration ended before completion".to_string(),
);
}
}
}
fn validate_environment_ready_info(
environment_id: &str,
ready_info: &EnvironmentReadyInfo,

View File

@@ -81,7 +81,6 @@ pub use environment::CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR;
pub use environment::CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR;
pub use environment::CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR;
pub use environment::CODEX_EXEC_SERVER_URL_ENV_VAR;
pub use environment::DeferredEnvironmentRegistration;
pub use environment::Environment;
pub use environment::EnvironmentConnectionState;
pub use environment::EnvironmentManager;

View File

@@ -54,367 +54,6 @@ fn ready_info(root_id: &str, environment_id: &str) -> anyhow::Result<Environment
})
}
#[tokio::test]
async fn deferred_environment_waits_before_connecting() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
let provider = Arc::new(FailingNoiseConnectProvider::default());
let registration =
manager.register_deferred_noise_environment("tools".to_string(), provider.clone())?;
let environment = manager.get_environment("tools").expect("environment");
let connection_state = environment
.subscribe_connection_state()
.expect("remote environment connection state");
let mut readiness = Box::pin(environment.wait_until_ready());
assert!(poll!(&mut readiness).is_pending());
assert_eq!(provider.calls(), 0);
assert!(environment.selected_capability_roots().is_empty());
let ready_info = ready_info("selected-root", "tools")?;
registration.complete(Ok(ready_info.clone()))?;
assert_eq!(
environment.selected_capability_roots(),
ready_info.selected_capability_roots
);
let error = readiness.await.unwrap_err();
assert!(error.to_string().contains("test Noise provider called"));
assert_eq!(provider.calls(), 1);
assert!(!connection_state.has_changed()?);
Ok(())
}
#[tokio::test]
async fn deferred_registration_replaces_an_ordinary_noise_environment() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
manager.upsert_noise_environment(
"tools".to_string(),
Arc::new(FailingNoiseConnectProvider::default()),
)?;
let ordinary = manager
.get_environment("tools")
.expect("ordinary environment");
let deferred_provider = Arc::new(FailingNoiseConnectProvider::default());
let registration = manager
.register_deferred_noise_environment("tools".to_string(), deferred_provider.clone())?;
let deferred = manager
.get_environment("tools")
.expect("deferred environment");
assert!(!Arc::ptr_eq(&ordinary, &deferred));
let mut readiness = Box::pin(deferred.wait_until_ready());
assert!(poll!(&mut readiness).is_pending());
registration.complete(Ok(ready_info("selected-root", "tools")?))?;
let error = readiness.await.unwrap_err();
assert!(error.to_string().contains("test Noise provider called"));
assert_eq!(deferred_provider.calls(), 1);
Ok(())
}
#[tokio::test]
async fn ordinary_noise_environment_replaces_a_deferred_registration() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
let deferred_provider = Arc::new(FailingNoiseConnectProvider::default());
let registration = manager
.register_deferred_noise_environment("tools".to_string(), deferred_provider.clone())?;
let deferred = manager
.get_environment("tools")
.expect("deferred environment");
let ordinary_provider = Arc::new(FailingNoiseConnectProvider::default());
manager.upsert_noise_environment("tools".to_string(), ordinary_provider.clone())?;
let ordinary = manager
.get_environment("tools")
.expect("ordinary environment");
assert!(!Arc::ptr_eq(&deferred, &ordinary));
drop(registration);
let error = deferred.wait_until_ready().await.unwrap_err();
assert!(
error
.to_string()
.contains("registration ended before completion")
);
assert_eq!(deferred_provider.calls(), 0);
let error = ordinary.wait_until_ready().await.unwrap_err();
assert!(error.to_string().contains("test Noise provider called"));
assert_eq!(ordinary_provider.calls(), 1);
Ok(())
}
#[tokio::test]
async fn existing_environment_publishes_readiness_without_replacement() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
let existing_provider = Arc::new(FailingNoiseConnectProvider::default());
manager.upsert_noise_environment("tools".to_string(), existing_provider)?;
let existing_environment = manager
.get_environment("tools")
.expect("existing environment");
let ready_info = ready_info("selected-root", "tools")?;
manager.publish_ready_info("tools", ready_info.clone())?;
let current_environment = manager
.get_environment("tools")
.expect("current environment");
assert!(Arc::ptr_eq(&existing_environment, &current_environment));
assert_eq!(
existing_environment.selected_capability_roots(),
ready_info.selected_capability_roots
);
Ok(())
}
#[tokio::test]
async fn readiness_updates_the_current_environment_after_replacement() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
manager.upsert_noise_environment(
"tools".to_string(),
Arc::new(FailingNoiseConnectProvider::default()),
)?;
let captured_environment = manager
.get_environment("tools")
.expect("captured environment");
manager.upsert_noise_environment(
"tools".to_string(),
Arc::new(FailingNoiseConnectProvider::default()),
)?;
let current_environment = manager
.get_environment("tools")
.expect("replacement environment");
let ready_info = ready_info("selected-root", "tools")?;
manager.publish_ready_info("tools", ready_info.clone())?;
assert!(!Arc::ptr_eq(&captured_environment, &current_environment));
assert!(captured_environment.selected_capability_roots().is_empty());
assert_eq!(
current_environment.selected_capability_roots(),
ready_info.selected_capability_roots
);
Ok(())
}
#[tokio::test]
async fn publishing_readiness_requires_existing_environment() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
let error = manager
.publish_ready_info("tools", ready_info("selected-root", "tools")?)
.unwrap_err();
assert!(matches!(error, ExecServerError::Protocol(_)));
assert!(manager.get_environment("tools").is_none());
Ok(())
}
#[tokio::test]
async fn existing_environment_accepts_matching_readiness() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
manager.upsert_noise_environment(
"tools".to_string(),
Arc::new(FailingNoiseConnectProvider::default()),
)?;
let environment = manager.get_environment("tools").expect("environment");
let ready_info = ready_info("selected-root", "tools")?;
for _ in 0..2 {
manager.publish_ready_info("tools", ready_info.clone())?;
}
assert_eq!(
environment.selected_capability_roots(),
ready_info.selected_capability_roots
);
Ok(())
}
#[tokio::test]
async fn existing_environment_overwrites_published_readiness() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
manager.upsert_noise_environment(
"tools".to_string(),
Arc::new(FailingNoiseConnectProvider::default()),
)?;
let environment = manager.get_environment("tools").expect("environment");
let selected_ready_info = ready_info("selected-root", "tools")?;
manager.publish_ready_info("tools", selected_ready_info)?;
let updated_ready_info = ready_info("different-root", "tools")?;
manager.publish_ready_info("tools", updated_ready_info.clone())?;
assert_eq!(
environment.selected_capability_roots(),
updated_ready_info.selected_capability_roots
);
assert!(Arc::ptr_eq(
&environment,
&manager.get_environment("tools").expect("environment")
));
manager.publish_ready_info("tools", EnvironmentReadyInfo::default())?;
assert!(environment.selected_capability_roots().is_empty());
Ok(())
}
#[tokio::test]
async fn publishing_readiness_before_deferred_completion_preserves_the_gate() -> anyhow::Result<()>
{
let manager = environment_manager_without_environments();
let provider = Arc::new(FailingNoiseConnectProvider::default());
let registration =
manager.register_deferred_noise_environment("tools".to_string(), provider.clone())?;
let environment = manager.get_environment("tools").expect("environment");
let mut readiness = Box::pin(environment.wait_until_ready());
manager.publish_ready_info("tools", ready_info("published-root", "tools")?)?;
assert!(poll!(&mut readiness).is_pending());
assert_eq!(provider.calls(), 0);
let completed_ready_info = ready_info("completed-root", "tools")?;
registration.complete(Ok(completed_ready_info.clone()))?;
assert_eq!(
environment.selected_capability_roots(),
completed_ready_info.selected_capability_roots
);
let error = readiness.await.unwrap_err();
assert!(error.to_string().contains("test Noise provider called"));
assert_eq!(provider.calls(), 1);
Ok(())
}
#[tokio::test]
async fn existing_environment_rejects_invalid_readiness() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
manager.upsert_noise_environment(
"tools".to_string(),
Arc::new(FailingNoiseConnectProvider::default()),
)?;
let existing_environment = manager
.get_environment("tools")
.expect("existing environment");
let error = manager
.publish_ready_info("tools", ready_info("selected-root", "other")?)
.unwrap_err();
assert!(matches!(error, ExecServerError::Protocol(_)));
assert!(existing_environment.selected_capability_roots().is_empty());
let current_environment = manager
.get_environment("tools")
.expect("current environment");
assert!(Arc::ptr_eq(&existing_environment, &current_environment));
Ok(())
}
#[tokio::test]
async fn failure_and_dropped_registration_are_terminal() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
let failed_provider = Arc::new(FailingNoiseConnectProvider::default());
let failed = manager
.register_deferred_noise_environment("failed".to_string(), failed_provider.clone())?;
let failed_environment = manager.get_environment("failed").expect("environment");
failed.complete(Err("provisioning failed".to_string()))?;
let error = failed_environment.wait_until_ready().await.unwrap_err();
assert!(
error
.to_string()
.ends_with("environment unavailable: provisioning failed")
);
assert_eq!(failed_provider.calls(), 0);
let dropped_provider = Arc::new(FailingNoiseConnectProvider::default());
let dropped = manager
.register_deferred_noise_environment("dropped".to_string(), dropped_provider.clone())?;
let dropped_environment = manager.get_environment("dropped").expect("environment");
drop(dropped);
let error = dropped_environment.wait_until_ready().await.unwrap_err();
assert!(
error
.to_string()
.contains("registration ended before completion")
);
assert_eq!(dropped_provider.calls(), 0);
assert!(manager.get_environment("failed").is_some());
assert!(manager.get_environment("dropped").is_some());
Ok(())
}
#[tokio::test]
async fn invalid_ready_info_is_terminal() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
let provider = Arc::new(FailingNoiseConnectProvider::default());
let registration =
manager.register_deferred_noise_environment("tools".to_string(), provider.clone())?;
let environment = manager.get_environment("tools").expect("environment");
let error = registration
.complete(Ok(ready_info("selected-root", "other")?))
.unwrap_err();
assert!(matches!(error, ExecServerError::Protocol(_)));
let readiness_error = environment.wait_until_ready().await.unwrap_err();
assert!(
readiness_error
.to_string()
.contains("belong to environment")
);
assert!(environment.selected_capability_roots().is_empty());
assert_eq!(provider.calls(), 0);
Ok(())
}
#[tokio::test]
async fn late_completion_is_isolated_from_replacement() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
let old_provider = Arc::new(FailingNoiseConnectProvider::default());
let old_registration =
manager.register_deferred_noise_environment("tools".to_string(), old_provider.clone())?;
let old_environment = manager.get_environment("tools").expect("old environment");
let current_provider = Arc::new(FailingNoiseConnectProvider::default());
let current_registration = manager
.register_deferred_noise_environment("tools".to_string(), current_provider.clone())?;
let current = manager.get_environment("tools").expect("current");
let old_ready_info = ready_info("old-root", "tools")?;
old_registration.complete(Ok(old_ready_info.clone()))?;
assert_eq!(
old_environment.selected_capability_roots(),
old_ready_info.selected_capability_roots
);
assert!(current.selected_capability_roots().is_empty());
let old_error = old_environment.wait_until_ready().await.unwrap_err();
assert!(old_error.to_string().contains("test Noise provider called"));
assert_eq!(old_provider.calls(), 1);
let mut current_readiness = Box::pin(current.wait_until_ready());
assert!(poll!(&mut current_readiness).is_pending());
assert_eq!(current_provider.calls(), 0);
let current_ready_info = ready_info("current-root", "tools")?;
current_registration.complete(Ok(current_ready_info.clone()))?;
assert_eq!(
current.selected_capability_roots(),
current_ready_info.selected_capability_roots
);
let current_error = current_readiness.await.unwrap_err();
assert!(
current_error
.to_string()
.contains("test Noise provider called")
);
assert_eq!(current_provider.calls(), 1);
Ok(())
}
#[tokio::test]
async fn eager_noise_environment_connects_without_registration() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
let provider = Arc::new(FailingNoiseConnectProvider::default());
manager.upsert_noise_environment("tools".to_string(), provider.clone())?;
let environment = manager.get_environment("tools").expect("environment");
let error = environment.wait_until_ready().await.unwrap_err();
assert!(error.to_string().contains("test Noise provider called"));
assert_eq!(provider.calls(), 1);
Ok(())
}
#[tokio::test]
async fn readiness_before_materialization_creates_the_stable_environment() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
@@ -469,6 +108,33 @@ async fn materialize_then_report_ready_reuses_the_pending_environment() -> anyho
Ok(())
}
#[tokio::test]
async fn ordinary_environment_ignores_provisioning_reports() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
manager.upsert_environment(
"tools".to_string(),
"ws://127.0.0.1:1".to_string(),
Some(std::time::Duration::from_millis(1)),
)?;
let existing_environment = manager
.get_environment("tools")
.expect("existing environment");
let reported = manager.report_environment_provisioning_status(
"tools".to_string(),
Ok(ready_info("selected-root", "tools")?),
Arc::new(FailingNoiseConnectProvider::default()),
)?;
let current_environment = manager
.get_environment("tools")
.expect("current environment");
assert!(Arc::ptr_eq(&existing_environment, &current_environment));
assert!(reported.is_none());
assert!(existing_environment.selected_capability_roots().is_empty());
Ok(())
}
#[tokio::test]
async fn failure_before_materialization_is_terminal_without_connecting() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
@@ -537,15 +203,6 @@ async fn repeated_failure_preserves_the_first_error_and_rejects_ready() -> anyho
.expect("repeated failure should be idempotent");
assert!(Arc::ptr_eq(&failed, &repeated));
let error = manager
.report_environment_provisioning_status(
"tools".to_string(),
Ok(ready_info("selected-root", "other")?),
provider.clone(),
)
.unwrap_err();
assert!(error.to_string().contains("first failure"));
let error = manager
.report_environment_provisioning_status(
"tools".to_string(),
@@ -585,3 +242,141 @@ async fn ready_environment_rejects_a_later_failure() -> anyhow::Result<()> {
assert_eq!(ready.selected_capability_roots().len(), 1);
Ok(())
}
#[tokio::test]
async fn existing_environment_accepts_matching_readiness() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
let provider = Arc::new(FailingNoiseConnectProvider::default());
let ready_info = ready_info("selected-root", "tools")?;
let environment = manager
.report_environment_provisioning_status(
"tools".to_string(),
Ok(ready_info.clone()),
provider.clone(),
)?
.expect("readiness report should create the environment");
manager.report_environment_provisioning_status(
"tools".to_string(),
Ok(ready_info.clone()),
provider,
)?;
assert_eq!(
environment.selected_capability_roots(),
ready_info.selected_capability_roots
);
Ok(())
}
#[tokio::test]
async fn existing_environment_overwrites_reported_readiness() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
let provider = Arc::new(FailingNoiseConnectProvider::default());
let environment = manager
.report_environment_provisioning_status(
"tools".to_string(),
Ok(ready_info("selected-root", "tools")?),
provider.clone(),
)?
.expect("readiness report should create the environment");
let updated_ready_info = ready_info("different-root", "tools")?;
manager.report_environment_provisioning_status(
"tools".to_string(),
Ok(updated_ready_info.clone()),
provider,
)?;
assert_eq!(
environment.selected_capability_roots(),
updated_ready_info.selected_capability_roots
);
assert!(Arc::ptr_eq(
&environment,
&manager.get_environment("tools").expect("environment")
));
Ok(())
}
#[tokio::test]
async fn invalid_ready_report_fails_the_provisioning_gate() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
let provider = Arc::new(FailingNoiseConnectProvider::default());
let environment =
manager.materialize_pending_noise_environment("tools".to_string(), provider.clone())?;
let readiness = Box::pin(environment.wait_until_ready());
let error = manager
.report_environment_provisioning_status(
"tools".to_string(),
Ok(ready_info("selected-root", "other")?),
provider.clone(),
)
.unwrap_err();
assert!(matches!(error, ExecServerError::Protocol(_)));
let readiness_error = readiness.await.unwrap_err();
assert!(readiness_error.to_string().contains(&error.to_string()));
assert!(environment.selected_capability_roots().is_empty());
assert_eq!(provider.calls(), 0);
let later_ready_error = manager
.report_environment_provisioning_status(
"tools".to_string(),
Ok(ready_info("selected-root", "tools")?),
provider.clone(),
)
.unwrap_err();
assert!(
later_ready_error
.to_string()
.contains("provisioning already failed")
);
assert!(environment.selected_capability_roots().is_empty());
assert_eq!(provider.calls(), 0);
Ok(())
}
#[tokio::test]
async fn duplicate_materialization_reuses_the_pending_environment() -> anyhow::Result<()> {
let manager = environment_manager_without_environments();
let provider = Arc::new(FailingNoiseConnectProvider::default());
let environment =
manager.materialize_pending_noise_environment("tools".to_string(), provider.clone())?;
let replacement_provider = Arc::new(FailingNoiseConnectProvider::default());
let current = manager
.materialize_pending_noise_environment("tools".to_string(), replacement_provider.clone())?;
assert!(Arc::ptr_eq(&environment, &current));
assert_eq!(provider.calls(), 0);
assert_eq!(replacement_provider.calls(), 0);
Ok(())
}
#[tokio::test]
async fn deferred_materialization_conflicts_with_an_existing_ordinary_environment()
-> anyhow::Result<()> {
let manager = environment_manager_without_environments();
manager.upsert_environment(
"tools".to_string(),
"ws://127.0.0.1:1".to_string(),
Some(std::time::Duration::from_millis(1)),
)?;
let existing_environment = manager.get_environment("tools").expect("environment");
let deferred_provider = Arc::new(FailingNoiseConnectProvider::default());
let error = manager
.materialize_pending_noise_environment("tools".to_string(), deferred_provider.clone())
.unwrap_err();
assert!(matches!(
error,
ExecServerError::ProvisioningModeConflict { environment_id }
if environment_id == "tools"
));
let current_environment = manager
.get_environment("tools")
.expect("ordinary environment should remain registered");
assert!(Arc::ptr_eq(&existing_environment, &current_environment));
assert_eq!(deferred_provider.calls(), 0);
Ok(())
}