Skip to main content

sqlite_graphrag/constants/
runtime.rs

1//! Concurrency permits, memory budgets and slot pacing.
2//!
3//! Split out of the former single-file `constants.rs` in v1.2.5;
4//! every item is re-exported by the parent module, so `crate::constants::X`
5//! resolves exactly as before.
6
7// G46: FASTEMBED_MODEL_DEFAULT removed — the fastembed model was deleted in
8// v1.0.76 (LLM-only build); `schema_meta.model` now records the CLI version.
9
10/// Default worker count for the global Rayon pool.
11///
12/// Each worker holds one batch-embedding call in flight, so a wider pool buys
13/// no throughput on the LLM-only path and risks RSS oversubscription on a
14/// 4-8 GiB host. Override via XDG `parallelism.rayon_threads`.
15pub const DEFAULT_RAYON_THREADS: usize = 2;
16
17/// Default value injected into ORT_NUM_THREADS when not set by the user.
18pub const ORT_NUM_THREADS_DEFAULT: &str = "1";
19
20/// Default value injected into ORT_INTRA_OP_NUM_THREADS when not set.
21pub const ORT_INTRA_OP_NUM_THREADS_DEFAULT: &str = "1";
22
23/// Default value injected into OMP_NUM_THREADS when not set by the user.
24pub const OMP_NUM_THREADS_DEFAULT: &str = "1";
25
26/// Polling interval in milliseconds used by `--wait-lock` between `try_lock_exclusive` attempts.
27pub const CLI_LOCK_POLL_INTERVAL_MS: u64 = 500;
28
29/// Maximum number of CLI instances running simultaneously.
30///
31/// Limits the counting
32/// semaphore in [`crate::lock`] to prevent memory overload when multiple parallel
33/// v1.0.75 (G18 solution): removed the rigid 4-slot ceiling. The adaptive
34/// `calculate_safe_concurrency` function in [`crate::lock`]` now reports
35/// the dynamic limit. This constant is preserved as a *legacy fallback*
36/// when the dynamic calculation cannot be performed (e.g. when `sysinfo`
37/// cannot read `/proc/meminfo`).
38///
39/// Operators should prefer passing `--max-concurrency` explicitly OR
40/// letting the runtime compute the limit. The default ceiling is intentionally
41/// higher (16) so the legacy 4-slot hard cap does not silently reappear.
42pub const MAX_CONCURRENT_CLI_INSTANCES: usize = 16;
43
44/// Memory assumed available when the LLM slot default is computed without
45/// `sysinfo` at hand.
46///
47/// Deliberately conservative. `lock::calculate_safe_concurrency` is the source
48/// of truth whenever exact memory data is available; this only keeps the
49/// fallback in the same order of magnitude.
50pub const LLM_SLOT_ASSUMED_AVAILABLE_MB: u32 = 4096;
51
52/// How long the LLM slot acquirer sleeps between polls while every slot is busy.
53///
54/// Short enough that a freed slot is picked up promptly, long enough that a
55/// waiting process does not spin on the lock.
56pub const LLM_SLOT_POLL_INTERVAL_MS: u64 = 100;
57
58/// G28-B (v1.0.68): polling interval in milliseconds used by
59/// `acquire_job_singleton` between retry attempts when another invocation
60/// already holds the singleton for `(job_type, namespace)`.
61pub const JOB_SINGLETON_POLL_INTERVAL_MS: u64 = 1000;
62
63/// Minimum available memory in MiB required before starting model loading.
64///
65/// If `sysinfo::System::available_memory() / 1_048_576` falls below this value,
66/// the invocation is aborted with [`crate::errors::AppError::LowMemory`]
67/// (exit code [`crate::constants::LOW_MEMORY_EXIT_CODE`]).
68pub const MIN_AVAILABLE_MEMORY_MB: u64 = 2_048;
69
70/// Maximum process RSS in MiB before aborting embedding operations.
71/// Users can override via `--max-rss-mb`. Set to 8 GiB by default.
72pub const DEFAULT_MAX_RSS_MB: u64 = 8_192;
73
74/// Maximum time in seconds an instance waits to acquire a concurrency slot.
75///
76/// Passed as the default for `--wait-lock` in the CLI. After exhausting this limit,
77/// the invocation returns [`crate::errors::AppError::AllSlotsFull`] with exit code
78/// [`crate::constants::CLI_LOCK_EXIT_CODE`] (75).
79pub const CLI_LOCK_DEFAULT_WAIT_SECS: u64 = 300;
80
81/// DEFAULT expected RSS, in MiB, budgeted for one LLM/REST worker.
82///
83/// # This number was NOT measured empirically
84///
85/// It is a v1.0.75 (G18 + G23) engineering estimate for a worker that used to
86/// spawn a subprocess, kept after the move to the OpenRouter REST client
87/// because the REST footprint is strictly smaller and the estimate therefore
88/// stays conservative. No benchmark, profile or RSS sample backs the exact
89/// value 350, and no test asserts it against a measurement. Treat it as a
90/// deliberately pessimistic budget, not as data.
91///
92/// It governs every concurrency ceiling derived from free memory
93/// ([`crate::memory_guard::calculate_safe_concurrency`],
94/// [`crate::llm_slots::default_max_concurrency`],
95/// [`crate::embedder::effective_permits`]), so an operator who has measured the
96/// real footprint on their host SHOULD override it rather than live with the
97/// estimate: read it through [`llm_worker_rss_mb`], never directly.
98pub const LLM_WORKER_RSS_MB: u64 = 350;
99
100/// Per-worker RSS budget in MiB: XDG `llm.worker_rss_mb` or
101/// [`LLM_WORKER_RSS_MB`].
102///
103/// The knob exists because the default is an estimate and not a measurement
104/// (see [`LLM_WORKER_RSS_MB`]). `0` is rejected in favour of the default: a zero
105/// budget would make every `available_mb / per_worker` division either panic or
106/// authorise unbounded concurrency.
107pub fn llm_worker_rss_mb() -> u64 {
108    crate::config::get_setting("llm.worker_rss_mb")
109        .ok()
110        .flatten()
111        .and_then(|v| v.parse::<u64>().ok())
112        .filter(|n| *n > 0)
113        .unwrap_or(LLM_WORKER_RSS_MB)
114}
115
116/// DEFAULT joint ceiling on `max_concurrency × llm_parallelism` for one host.
117///
118/// The two knobs are validated independently — `--max-concurrency` against
119/// `2 × nCPUs` and `--llm-parallelism` against 32 — so nothing used to stop
120/// their PRODUCT from authorising `2 × nCPUs × 32` in-flight workers, which on a
121/// 16-core host is 1024. This constant is the missing joint bound; the per-knob
122/// ceilings stay exactly as they are and this one only clamps the product.
123///
124/// Read it through [`max_total_llm_workers`], never directly.
125pub const MAX_TOTAL_LLM_WORKERS: usize = 64;
126
127/// Joint worker ceiling: XDG `parallelism.max_total_workers` or
128/// [`MAX_TOTAL_LLM_WORKERS`]. `0` falls back to the default.
129pub fn max_total_llm_workers() -> usize {
130    crate::config::get_setting("parallelism.max_total_workers")
131        .ok()
132        .flatten()
133        .and_then(|v| v.parse::<usize>().ok())
134        .filter(|n| *n > 0)
135        .unwrap_or(MAX_TOTAL_LLM_WORKERS)
136}
137
138/// Per-process fan-out width still allowed once `max_concurrency` processes are
139/// counted against [`max_total_llm_workers`].
140///
141/// Pure and total: `max_concurrency` of `0` is read as `1`, and the result never
142/// drops below `1` — a joint cap that forbade all work would be a deadlock, not
143/// a safety bound.
144pub fn joint_parallelism_ceiling_for(max_concurrency: usize) -> usize {
145    (max_total_llm_workers() / max_concurrency.max(1)).max(1)
146}
147
148/// Joint fan-out ceiling published by `main` once `--max-concurrency` resolves.
149///
150/// `0` means "never published", which is the case for every unit test and for
151/// any embedded consumer of the library that does not go through `main`.
152static JOINT_PARALLELISM_CEILING: std::sync::atomic::AtomicUsize =
153    std::sync::atomic::AtomicUsize::new(0);
154
155/// Publishes the joint fan-out ceiling derived from the resolved
156/// `--max-concurrency` (called once, from `main`).
157pub fn set_joint_parallelism_ceiling(ceiling: usize) {
158    JOINT_PARALLELISM_CEILING.store(ceiling.max(1), std::sync::atomic::Ordering::Release);
159}
160
161/// Joint fan-out ceiling in force for this process.
162///
163/// Falls back to [`max_total_llm_workers`] when `main` never published one, so a
164/// library consumer is bounded by the joint cap alone rather than by an
165/// accidental `1`.
166pub fn joint_parallelism_ceiling() -> usize {
167    let published = JOINT_PARALLELISM_CEILING.load(std::sync::atomic::Ordering::Acquire);
168    if published == 0 {
169        max_total_llm_workers()
170    } else {
171        published
172    }
173}
174
175/// Minimum interval, in seconds, between two `/proc/loadavg` reads.
176///
177/// The saturation check is consulted before every spawn decision, so an
178/// unthrottled read would issue one syscall per decision for a value that
179/// changes on a one-minute average. Throttle, not a deadline, so it takes no
180/// XDG key.
181pub const SYSTEM_LOAD_REFRESH_INTERVAL_SECS: u64 = 1;
182
183/// Deadline, in seconds, a drain keeps absorbing provider rate limits before
184/// giving up on the run.
185///
186/// One hour is long enough to ride out a provider quota window without a human,
187/// and short enough that a wedged run does not hold a job singleton overnight.
188/// Shared by `enrich` (serial and parallel drains) and `ingest-codex`, which had
189/// three independent copies of the same literal.
190///
191/// Operational policy, so it is configurable: XDG
192/// `enrich.rate_limit_deadline_secs`, resolved by
193/// [`crate::runtime_config::rate_limit_deadline_secs`].
194pub const DEFAULT_RATE_LIMIT_DEADLINE_SECS: u64 = 3_600;
195
196/// Deadline, in seconds, for reading a memory body from stdin.
197///
198/// The `stdin_helper` doc comment has promised "default 60s" since the module
199/// was written while every call site passed the literal; this constant makes
200/// the promise real. Sixty seconds is generous for a pipe that is already
201/// producing and short enough that a held-open pipe fails inside an agent turn.
202///
203/// Operational policy, so it is configurable: XDG `cli.stdin_timeout_secs`,
204/// resolved by [`crate::runtime_config::stdin_timeout_secs`].
205pub const DEFAULT_STDIN_READ_TIMEOUT_SECS: u64 = 60;
206
207/// Poll interval, in seconds, of the `deadlock-detection` watchdog thread.
208///
209/// Short enough to catch a deadlock inside an interactive test, long enough to
210/// keep tracing quiet during normal operation. Diagnostic scaffolding behind a
211/// cargo feature, never a production deadline, so it takes no XDG key.
212pub const DEADLOCK_CHECK_INTERVAL_SECS: u64 = 10;
213
214/// Pause, in milliseconds, appended to a cooperative yield between enrich
215/// batches (PRIO-05).
216///
217/// `yield_now` alone is advisory and some schedulers ignore it; one millisecond
218/// guarantees the descheduling without measurably slowing the drain. Scheduler
219/// hint, so it takes no XDG key.
220pub const COOPERATIVE_YIELD_SLEEP_MS: u64 = 1;