superstac_engine/
engine.rs1use parking_lot::Mutex;
2use reqwest::Client;
3use std::sync::{
4 atomic::{AtomicBool, Ordering},
5 Arc,
6};
7
8use std::collections::HashMap;
9
10use crate::{
11 capabilities,
12 discovery::{self, CollectionAvailability},
13 health::HealthManager,
14 types::SharedStorage,
15};
16use superstac_core::{
17 errors::SuperSTACError,
18 models::catalog::{Catalog, CatalogFilters},
19 storages::factory::StorageBackend,
20};
21use std::time::Duration;
22
23use superstac_search::{
24 executor::SearchExecutor,
25 options::{FederationOptions, RetryPolicy},
26 query::SearchQuery,
27 response::SearchResponse,
28};
29
30pub struct SuperSTACEngine {
38 storage: SharedStorage,
39 client: Client,
40 health_manager: Option<HealthManager>,
41 executor: SearchExecutor,
42 started: AtomicBool,
43}
44
45const USER_AGENT: &str = concat!("superstac/", env!("CARGO_PKG_VERSION"));
50
51impl SuperSTACEngine {
52 pub fn new(storage: Box<dyn StorageBackend + Send + Sync>) -> Self {
56 let client = Client::builder()
57 .user_agent(USER_AGENT)
58 .build()
59 .expect("failed to build reqwest client");
60
61 Self {
62 storage: Arc::new(Mutex::new(storage)),
63 client: client.clone(),
64 health_manager: Some(HealthManager::new(client.clone())),
65 executor: SearchExecutor::new(client),
66 started: AtomicBool::new(false),
67 }
68 }
69
70 pub async fn start(&self) -> Result<(), SuperSTACError> {
73 if self.started.load(Ordering::Relaxed) {
74 return Ok(());
75 }
76
77 if let Some(manager) = &self.health_manager {
78 let catalogs = {
79 let storage = self.storage.lock();
80 storage.list_catalogs(None)?
81 };
82
83 manager.start(Arc::clone(&self.storage), catalogs).await?;
84 }
85
86 self.introspect_capabilities().await?;
90
91 self.started.store(true, Ordering::Relaxed);
92
93 let catalog_count = self.storage.lock().list_catalogs(None)?.len();
94 tracing::info!(catalogs = catalog_count, "engine ready");
95
96 Ok(())
97 }
98
99 pub async fn shutdown(&self) {
101 tracing::debug!("shutting down engine");
102
103 if let Some(manager) = &self.health_manager {
104 manager.stop_all().await;
105 }
106
107 self.started.store(false, Ordering::Relaxed);
108 }
109
110 async fn introspect_capabilities(&self) -> Result<(), SuperSTACError> {
111 let healthy_catalogs = {
112 let storage = self.storage.lock();
113 storage.list_catalogs(Some(CatalogFilters {
114 available: Some(true),
115 ..CatalogFilters::default()
116 }))?
117 };
118
119 for catalog in healthy_catalogs {
120 match capabilities::fetch_supported_collections(&self.client, &catalog).await {
121 Ok(set) => {
122 tracing::debug!(
123 catalog = %catalog.id,
124 collections = set.len(),
125 "introspected /collections"
126 );
127 let mut storage = self.storage.lock();
128 if let Err(e) =
129 storage.update_supported_collections(&catalog.id, Some(set))
130 {
131 tracing::warn!(
132 catalog = %catalog.id,
133 error = %e,
134 "failed to persist /collections result"
135 );
136 }
137 }
138 Err(e) => {
139 tracing::warn!(
140 catalog = %catalog.id,
141 error = %e,
142 "/collections introspection failed; catalog will pass-through"
143 );
144 }
145 }
146 }
147
148 Ok(())
149 }
150
151 pub async fn ensure_started(&self) -> Result<(), SuperSTACError> {
154 if !self.started.load(Ordering::Relaxed) {
155 self.start().await?;
156 }
157
158 Ok(())
159 }
160
161 pub async fn search(&self, query: SearchQuery) -> Result<SearchResponse, SuperSTACError> {
164 self.ensure_started().await?;
165
166 let (candidate_catalogs, options) = {
167 let storage = self.storage.lock();
168 let settings = storage.get_settings();
169
170 let catalogs = if let Some(true) = settings.search_healthy_catalogs_only {
171 storage.list_catalogs(Some(CatalogFilters {
172 available: Some(true),
173 ..CatalogFilters::default()
174 }))?
175 } else {
176 storage.list_catalogs(None)?
177 };
178
179 let options = FederationOptions {
180 deduplicate: settings.deduplicate_items.unwrap_or(true),
181 unify_response: settings.unify_response.unwrap_or(true),
182 max_concurrent: settings.max_concurrent_catalogs.unwrap_or(8),
183 per_catalog_timeout: Duration::from_secs(
184 settings.per_catalog_timeout_seconds.unwrap_or(30),
185 ),
186 retry: RetryPolicy {
187 max_attempts: settings.max_retry_attempts.unwrap_or(2),
188 initial_backoff: Duration::from_millis(
189 settings.retry_initial_backoff_ms.unwrap_or(100),
190 ),
191 max_backoff: Duration::from_millis(
192 settings.retry_max_backoff_ms.unwrap_or(2000),
193 ),
194 },
195 max_items_per_catalog: settings.max_items_per_catalog.unwrap_or(1000),
196 };
197
198 (catalogs, options)
199 };
200
201 let unsupported =
204 discovery::unsupported_collections(&candidate_catalogs, &query.collections);
205
206 let catalogs: Vec<Catalog> = candidate_catalogs
210 .into_iter()
211 .filter(|c| c.supports_any_of(&query.collections))
212 .collect();
213
214 let mut response = self
215 .executor
216 .federated_search(catalogs, query, options)
217 .await?;
218 response.metadata.unsupported_collections = unsupported;
219
220 Ok(response)
221 }
222
223 pub async fn list_collections(&self) -> Result<Vec<CollectionAvailability>, SuperSTACError> {
226 self.ensure_started().await?;
227 let catalogs = {
228 let storage = self.storage.lock();
229 storage.list_catalogs(None)?
230 };
231 Ok(discovery::aggregate_collections(&catalogs))
232 }
233
234 pub async fn catalogs_supporting(
237 &self,
238 collection_id: &str,
239 ) -> Result<Vec<String>, SuperSTACError> {
240 self.ensure_started().await?;
241 let catalogs = {
242 let storage = self.storage.lock();
243 storage.list_catalogs(None)?
244 };
245 Ok(discovery::catalogs_supporting(&catalogs, collection_id))
246 }
247
248 pub async fn collections_by_catalog(
251 &self,
252 ) -> Result<HashMap<String, Vec<String>>, SuperSTACError> {
253 self.ensure_started().await?;
254 let catalogs = {
255 let storage = self.storage.lock();
256 storage.list_catalogs(None)?
257 };
258 Ok(discovery::collections_by_catalog(&catalogs))
259 }
260
261 pub async fn describe_collection(
266 &self,
267 catalog_id: &str,
268 collection_id: &str,
269 ) -> Result<Option<stac::Collection>, SuperSTACError> {
270 self.ensure_started().await?;
271
272 let catalog = {
273 let storage = self.storage.lock();
274 storage.get_catalog(catalog_id)?.clone()
275 };
276
277 let local_id = catalog.resolve_collection(collection_id).to_string();
278
279 let stac_client =
280 stac_io::api::Client::with_client(self.client.clone(), &catalog.url)
281 .map_err(|e| SuperSTACError::SearchFailed(format!("stac client init: {}", e)))?;
282
283 stac_client
284 .collection(&local_id)
285 .await
286 .map_err(|e| SuperSTACError::SearchFailed(format!("fetch collection: {}", e)))
287 }
288}