Skip to main content

linkrs_server/graph_service/
factory.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use log::{info, warn};
5use parking_lot::RwLock;
6
7#[cfg(feature = "vector")]
8use linkrs_api::api_core::VectorApi;
9use linkrs_api::api_core::{QueryApi, SyncApi};
10use linkrs_core::event_dispatch::EventSubscriptions;
11use linkrs_core::metadata::SchemaManager;
12use linkrs_metrics::StatsManager;
13use linkrs_query::query_manager::QueryManager;
14use linkrs_query::SessionEvent;
15#[cfg(feature = "vector")]
16use linkrs_sync::backend::VectorBackend;
17use linkrs_transaction::TransactionManager;
18
19use super::GraphService;
20use crate::auth::AuthenticatorFactory;
21use crate::config::Config;
22use crate::permission::{PermissionManager, GOD_SPACE_ID};
23use crate::query::executor::streaming::pool::SharedScheduler;
24use crate::query::executor::streaming::query_registry::QueryRegistry;
25use crate::query::optimizer::PartitioningConfig;
26use crate::session::GraphSessionManager;
27use crate::storage::{
28    StorageClient, StorageOperationContextOps, StorageSchemaContextOps, StorageSyncContextOps,
29};
30
31impl<
32        S: StorageClient
33            + StorageSchemaContextOps
34            + StorageSyncContextOps
35            + StorageOperationContextOps
36            + Clone
37            + 'static,
38    > GraphService<S>
39{
40    /// Create a new GraphService (without a transaction manager, for use in a production environment).
41    pub async fn new(config: Config, storage: Arc<S>) -> Arc<Self> {
42        #[cfg(feature = "vector")]
43        return Self::create_service(config, storage, None, true, None, None).await;
44        #[cfg(not(feature = "vector"))]
45        return Self::create_service(config, storage, None, true, None).await;
46    }
47
48    /// Create a new GraphService (without a transaction manager and without starting any background tasks, for testing purposes).
49    pub async fn new_for_test(config: Config, storage: Arc<S>) -> Arc<Self> {
50        #[cfg(feature = "vector")]
51        return Self::create_service(config, storage, None, false, None, None).await;
52        #[cfg(not(feature = "vector"))]
53        return Self::create_service(config, storage, None, false, None).await;
54    }
55
56    /// Use the transaction manager to create a GraphService.
57    pub async fn new_with_transaction_manager(
58        config: Config,
59        storage: Arc<S>,
60        transaction_manager: Arc<TransactionManager>,
61    ) -> Arc<Self> {
62        #[cfg(feature = "vector")]
63        return Self::create_service(config, storage, Some(transaction_manager), true, None, None)
64            .await;
65        #[cfg(not(feature = "vector"))]
66        return Self::create_service(config, storage, Some(transaction_manager), true, None).await;
67    }
68
69    /// Use the transaction manager and external StatsManager to create a GraphService.
70    pub async fn new_with_transaction_manager_and_stats(
71        config: Config,
72        storage: Arc<S>,
73        transaction_manager: Arc<TransactionManager>,
74        stats_manager: Arc<StatsManager>,
75    ) -> Arc<Self> {
76        #[cfg(feature = "vector")]
77        {
78            Self::create_service(
79                config,
80                storage,
81                Some(transaction_manager),
82                true,
83                Some(stats_manager),
84                None,
85            )
86            .await
87        }
88        #[cfg(not(feature = "vector"))]
89        {
90            Self::create_service(
91                config,
92                storage,
93                Some(transaction_manager),
94                true,
95                Some(stats_manager),
96            )
97            .await
98        }
99    }
100
101    /// Use the transaction manager, external StatsManager, and shared VectorBackend to create a GraphService.
102    #[cfg(feature = "vector")]
103    pub async fn with_shared_vector_backend(
104        config: Config,
105        storage: Arc<S>,
106        transaction_manager: Arc<TransactionManager>,
107        stats_manager: Arc<StatsManager>,
108        backend: VectorBackend,
109    ) -> Arc<Self> {
110        Self::create_service(
111            config,
112            storage,
113            Some(transaction_manager),
114            true,
115            Some(stats_manager),
116            Some(backend),
117        )
118        .await
119    }
120
121    /// Internal constructor: Extracts the common logic
122    ///
123    /// # Parameters
124    /// `start_cleanup_task` – Whether to initiate the background task for session cleanup
125    /// `shared_vector_backend` – Optional shared VectorBackend to avoid duplicate initialization
126    async fn create_service(
127        config: Config,
128        storage: Arc<S>,
129        transaction_manager: Option<Arc<TransactionManager>>,
130        start_cleanup_task: bool,
131        external_stats_manager: Option<Arc<StatsManager>>,
132        #[cfg(feature = "vector")] shared_vector_backend: Option<VectorBackend>,
133    ) -> Arc<Self> {
134        crate::query::executor::streaming::operators::source_operator::set_column_block_enabled(
135            config.common.columnar.column_block_enabled,
136        );
137
138        let session_idle_timeout =
139            Duration::from_secs(config.server.auth.session_idle_timeout_secs.max(1));
140        let session_events = Arc::new(EventSubscriptions::<SessionEvent>::new());
141        let query_manager = Arc::new(QueryManager::new_with_shared(session_events.clone()));
142        query_manager.set_slow_query_threshold_ms(config.monitoring.slow_query_threshold_ms as i64);
143        let session_manager = GraphSessionManager::new_with_shared(
144            format!("{}:{}", config.database.host, config.database.port),
145            config.database.max_sessions,
146            session_idle_timeout,
147            session_events.clone(),
148        );
149        session_manager.set_default_auto_commit(config.common.transaction.auto_commit);
150
151        if start_cleanup_task {
152            session_manager.start_cleanup_task().await;
153        }
154
155        let schema_manager: Option<Arc<SchemaManager>> = storage.get_schema_manager();
156
157        let stats_manager = if let Some(ext_stats) = external_stats_manager {
158            ext_stats
159        } else {
160            let slow_query_config = config.to_slow_query_config();
161            let m = &config.monitoring;
162            Arc::new(
163                StatsManager::with_slow_query_logger(
164                    m.enabled,
165                    m.memory_cache_size,
166                    m.slow_query_threshold_ms * 1000,
167                    slow_query_config,
168                )
169                .expect("Failed to create StatsManager with slow query logger"),
170            )
171        };
172
173        let runtime = crate::config::RuntimeConfig::file(config.storage_path());
174        let cost_profile = config
175            .common
176            .optimizer
177            .storage_cost_profile
178            .resolve_for_runtime(&runtime);
179        let cost_config = linkrs_api::api_core::cost_config_for_profile(cost_profile);
180        info!(
181            "Optimizer cost profile resolved to {:?} (random_page_cost={})",
182            cost_profile, cost_config.random_page_cost,
183        );
184        let mut optimizer_engine = crate::query::OptimizerEngine::new(cost_config);
185        optimizer_engine.set_partitioning_config(Self::partitioning_config_from(&config));
186        {
187            let rules = &config.common.optimizer.rules;
188            for name in rules
189                .disabled_rules
190                .iter()
191                .chain(rules.enabled_rules.iter())
192            {
193                if !Self::is_known_optimizer_rule(name) {
194                    log::warn!("Unknown optimizer rule name '{name}' in [optimizer.rules]");
195                }
196            }
197            optimizer_engine.set_rule_selection(&rules.disabled_rules, &rules.enabled_rules);
198        }
199        let shared_scheduler = Arc::new(SharedScheduler::new(
200            optimizer_engine.partitioning_config().max_workers.max(1),
201        ));
202        let optimizer_engine = Arc::new(optimizer_engine);
203        let query_registry = Arc::new(QueryRegistry::new());
204        query_manager.set_query_registry(Arc::clone(&query_registry));
205        info!(
206            "Shared query scheduler created with {} worker(s)",
207            shared_scheduler.max_workers()
208        );
209
210        #[cfg(feature = "vector")]
211        let (query_api, vector_api) = if config.is_vector_enabled() {
212            let backend = match shared_vector_backend {
213                Some(backend) => backend,
214                None => {
215                    let engine = simvec::LocalVectorEngine::open(config.vector_data_dir())
216                        .unwrap_or_else(|_| panic!("Failed to open local vector engine"));
217                    if let Some(hnsw) =
218                        linkrs_api::vector_config::local_hnsw_config(&config.vector_config().local)
219                    {
220                        engine.set_default_hnsw_config(hnsw);
221                    }
222                    if let Some(ivf) =
223                        linkrs_api::vector_config::local_ivf_config(&config.vector_config().local)
224                    {
225                        engine.set_default_ivf_config(ivf);
226                    }
227                    if let Some(quant) = linkrs_api::vector_config::local_quantization_config(
228                        &config.vector_config().local,
229                    ) {
230                        engine.set_default_quantization_config(quant);
231                    }
232                    linkrs_sync::backend::VectorBackend::local(engine)
233                }
234            };
235
236            match QueryApi::with_vector_backend(
237                Arc::new(RwLock::new((*storage).clone())),
238                stats_manager.clone(),
239                backend.clone(),
240                schema_manager.clone(),
241            )
242            .await
243            {
244                Ok(mut api) => {
245                    api = api
246                        .with_statistics_settings(
247                            config.common.optimizer.statistics_sample_limit,
248                            config.common.optimizer.statistics_min_epoch_delta,
249                        )
250                        .with_default_cost_profile_label(format!("{cost_profile:?}"))
251                        .with_space_cost_profiles(
252                            &config.common.optimizer.space_cost_profiles,
253                            &runtime,
254                        )
255                        .with_password_history_depth(
256                            config.server.security.password_policy.history_size,
257                        );
258                    api.install_shared_scheduler(shared_scheduler.clone(), query_registry.clone());
259                    api.install_query_manager(Arc::clone(&query_manager));
260                    // The standalone VectorApi needs the sync manager that
261                    // hosts the vector coordinator; without it the HTTP/gRPC
262                    // vector endpoints cannot list indexes, so it is left
263                    // unconfigured and those endpoints report unavailability
264                    // instead of silently serving empty results.
265                    let vector_api = storage.get_sync_manager().and_then(|sync_manager| {
266                        sync_manager
267                            .vector_coordinator()
268                            .cloned()
269                            .map(|coordinator| {
270                                Arc::new(VectorApi::new(backend, coordinator, sync_manager))
271                            })
272                    });
273                    (Arc::new(RwLock::new(api)), vector_api)
274                }
275                Err(e) => {
276                    warn!(
277                        "Failed to initialize vector search, falling back to basic QueryApi: {}",
278                        e
279                    );
280                    let mut api = Self::build_query_api(
281                        &storage,
282                        &stats_manager,
283                        schema_manager.as_ref(),
284                        optimizer_engine.clone(),
285                        &config.common.optimizer,
286                        cost_profile,
287                        &runtime,
288                        config.server.security.password_policy.history_size,
289                    );
290                    api.install_shared_scheduler(shared_scheduler.clone(), query_registry.clone());
291                    api.install_query_manager(Arc::clone(&query_manager));
292                    (Arc::new(RwLock::new(api)), None)
293                }
294            }
295        } else {
296            let mut api = Self::build_query_api(
297                &storage,
298                &stats_manager,
299                schema_manager.as_ref(),
300                optimizer_engine.clone(),
301                &config.common.optimizer,
302                cost_profile,
303                &runtime,
304                config.server.security.password_policy.history_size,
305            );
306            api.install_shared_scheduler(shared_scheduler.clone(), query_registry.clone());
307            api.install_query_manager(Arc::clone(&query_manager));
308            (Arc::new(RwLock::new(api)), None)
309        };
310
311        #[cfg(not(feature = "vector"))]
312        let query_api = {
313            let mut api = Self::build_query_api(
314                &storage,
315                &stats_manager,
316                schema_manager.as_ref(),
317                optimizer_engine.clone(),
318                &config.common.optimizer,
319                cost_profile,
320                &runtime,
321                config.server.security.password_policy.history_size,
322            );
323            api.install_shared_scheduler(shared_scheduler.clone(), query_registry.clone());
324            api.install_query_manager(Arc::clone(&query_manager));
325            Arc::new(RwLock::new(api))
326        };
327
328        if start_cleanup_task {
329            Self::spawn_startup_statistics_load(query_api.clone(), storage.clone());
330        }
331
332        let permission_manager = Arc::new(PermissionManager::new());
333        Self::ensure_admin_seed_and_rebuild(&storage, &permission_manager, &config);
334
335        let authenticator =
336            AuthenticatorFactory::create_with_storage(&config.server.auth, Arc::clone(&storage));
337
338        let sync_api = storage
339            .get_sync_manager()
340            .map(|sync_manager| Arc::new(SyncApi::new(sync_manager)));
341
342        let service = Self {
343            session_manager,
344            query_api,
345            authenticator,
346            permission_manager,
347            stats_manager,
348            storage,
349            #[cfg(feature = "vector")]
350            vector_api,
351            sync_api,
352            transaction_manager,
353            shared_scheduler,
354            query_registry,
355            query_manager,
356            progress_rows_interval: config.monitoring.progress_report_rows_interval,
357            next_query_id: std::sync::atomic::AtomicU64::new(1),
358            bootstrap_config: config.server.bootstrap.clone(),
359            security_config: config.server.security.clone(),
360        };
361        Arc::new(service)
362    }
363
364    /// Shared helper: build a QueryApi with optional SchemaManager, reusing the
365    /// server-level optimizer engine so `[parallel]` settings take effect.
366    fn build_query_api(
367        storage: &Arc<S>,
368        stats_manager: &Arc<StatsManager>,
369        schema_manager: Option<&Arc<SchemaManager>>,
370        optimizer_engine: Arc<crate::query::OptimizerEngine>,
371        optimizer_config: &crate::config::OptimizerConfig,
372        cost_profile: crate::config::StorageCostProfile,
373        runtime: &crate::config::RuntimeConfig,
374        password_history_depth: usize,
375    ) -> QueryApi<S> {
376        let inner = Arc::new(RwLock::new((**storage).clone()));
377        QueryApi::with_optimizer_engine(
378            inner,
379            stats_manager.clone(),
380            optimizer_engine,
381            schema_manager.cloned(),
382        )
383        .with_statistics_settings(
384            optimizer_config.statistics_sample_limit,
385            optimizer_config.statistics_min_epoch_delta,
386        )
387        .with_default_cost_profile_label(format!("{cost_profile:?}"))
388        .with_space_cost_profiles(&optimizer_config.space_cost_profiles, runtime)
389        .with_password_history_depth(password_history_depth)
390    }
391
392    /// Whether a rule name is known to the physical heuristic registry.
393    ///
394    /// Accepts the type name with or without the `Rule` suffix, matching
395    /// `RuleRegistry::filtered`.
396    fn is_known_optimizer_rule(name: &str) -> bool {
397        let trimmed = name.strip_suffix("Rule").unwrap_or(name);
398        let registry = crate::query::optimizer::heuristic::rule_enum::RuleRegistry::default();
399        let known = registry
400            .iter()
401            .any(|rule| rule.name().strip_suffix("Rule") == Some(trimmed));
402        known
403    }
404
405    /// Map the `[parallel]` config section onto the query optimizer's
406    /// partitioning configuration.
407    fn partitioning_config_from(config: &Config) -> PartitioningConfig {
408        let parallel = &config.common.parallel;
409        PartitioningConfig {
410            enabled: parallel.enabled,
411            min_rows_per_partition: parallel.min_rows_per_partition,
412            max_partitions: parallel.max_partitions,
413            vertex_id_range: parallel.vertex_id_range(),
414            max_workers: parallel.workers,
415            max_buffered_chunks: parallel.max_buffered_chunks,
416            // Economic gate stays inert in production until the overhead
417            // terms are calibrated per hardware (follow-up config knobs).
418            fixed_overhead_rows: 0,
419            per_partition_rows: 0,
420            exchange_row_per_mille: 0,
421        }
422    }
423
424    /// Spawn a background task that collects optimizer statistics for all
425    /// loaded spaces. Failure of any space is a warning only.
426    fn spawn_startup_statistics_load(query_api: Arc<RwLock<QueryApi<S>>>, storage: Arc<S>) {
427        tokio::spawn(async move {
428            let spaces = match storage.list_spaces() {
429                Ok(spaces) => spaces,
430                Err(error) => {
431                    warn!("Startup statistics load: failed to list spaces: {}", error);
432                    return;
433                }
434            };
435            for space in spaces {
436                let result = query_api
437                    .read()
438                    .collect_statistics(&space.space_name, false);
439                match result {
440                    Ok(()) => info!("Startup statistics loaded for space '{}'", space.space_name),
441                    Err(error) => warn!(
442                        "Startup statistics load failed for space '{}': {}",
443                        space.space_name, error
444                    ),
445                }
446            }
447        });
448    }
449
450    /// Seed the default admin on first start and rebuild role mappings.
451    ///
452    /// When the user table is empty, the resolved bootstrap credentials are
453    /// hashed into a new admin with the global God role; the plaintext
454    /// password is never compared afterwards. Otherwise permission state is
455    /// rebuilt from the persisted role segment so restarts keep grants.
456    ///
457    /// The seed credentials are intentionally exempt from the password
458    /// policy: the bootstrap password may be weak for local bootstrap and
459    /// the account is forced through the must-change flow on first login
460    /// instead.
461    fn ensure_admin_seed_and_rebuild(
462        storage: &Arc<S>,
463        permission_manager: &Arc<PermissionManager>,
464        config: &Config,
465    ) {
466        let mut handle = (**storage).clone();
467        if handle.list_users().is_empty() {
468            let username = config.server.auth.default_username.clone();
469            let password = config.server.auth.bootstrap_password.clone();
470            if !username.is_empty() && !password.is_empty() {
471                match linkrs_core::types::UserInfo::new(username.clone(), password) {
472                    Ok(info) => {
473                        if let Err(error) = handle.create_user(&info) {
474                            warn!("Admin seed failed for '{}': {}", username, error);
475                        } else if let Err(error) =
476                            handle.grant_role(&username, GOD_SPACE_ID, linkrs_core::RoleType::God)
477                        {
478                            warn!("Admin seed grant failed for '{}': {}", username, error);
479                        } else {
480                            info!("Seeded default admin '{}'", username);
481                        }
482                    }
483                    Err(error) => warn!("Admin seed hashing failed: {}", error),
484                }
485            }
486        }
487        let roles = handle.list_all_user_roles();
488        if !roles.is_empty() {
489            permission_manager.rebuild_from_snapshot(roles);
490        }
491    }
492}