1use super::{
4 SymbolBuildOptions, SymbolParseJob, SymbolParseOutcome, admit_symbol_job_bytes,
5 parse_admitted_symbol_job,
6};
7use crate::CliError;
8use projectatlas_cli::optional_parser_lifecycle::{
9 OptionalParserPackLifecycle, OptionalParserPackLifecycleError,
10 OptionalParserPackProjectSelection, OptionalParserPackSelectionKey,
11 VerifiedOptionalParserPackSelection,
12};
13use projectatlas_cli::parser_supervisor::ParserSupervisorError;
14use projectatlas_core::language::{BROAD_PARSER_PACK_ID, language_capability};
15use projectatlas_core::optional_parser_protocol::{
16 PARSER_MAX_NODE_COUNT, PARSER_MAX_OUTPUT_BYTES, PARSER_MAX_TREE_DEPTH, ParserRequestLimits,
17};
18use projectatlas_core::{IndexWorkControl, IndexWorkFailure, IndexWorkStage};
19use rayon::prelude::*;
20use std::path::Path;
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::sync::{Mutex, MutexGuard, OnceLock, TryLockError};
23use std::thread;
24use std::time::{Duration, Instant};
25
26const OPTIONAL_PARSE_TIMEOUT: Duration = Duration::from_secs(15);
28const OPTIONAL_PARSE_NO_PROGRESS_TIMEOUT: Duration = Duration::from_secs(5);
30const OPTIONAL_RUNTIME_ADMISSION_POLL: Duration = Duration::from_millis(10);
32const BUILT_IN_PARSE_BATCH_SIZE: usize = 64;
34
35static OPTIONAL_PARSER_RUNTIME: OnceLock<Mutex<OptionalParserRuntime>> = OnceLock::new();
37static OPTIONAL_PARSER_DEACTIVATION_REQUESTED: AtomicBool = AtomicBool::new(false);
39static OPTIONAL_PARSER_GROUP_ACTIVE: AtomicBool = AtomicBool::new(false);
41
42enum OptionalParserRuntimeState<T = Box<VerifiedOptionalParserPackSelection>> {
44 Inactive,
46 Resident {
48 verified: T,
50 },
51 UnavailableAfterCleanupFailure {
53 retained: Option<T>,
55 },
56}
57
58impl<T> OptionalParserRuntimeState<T> {
59 fn retain_after_cleanup_failure(&mut self) {
61 let previous = std::mem::replace(
62 self,
63 Self::UnavailableAfterCleanupFailure { retained: None },
64 );
65 let retained = match previous {
66 Self::Resident { verified } => Some(verified),
67 Self::UnavailableAfterCleanupFailure { retained } => retained,
68 Self::Inactive => None,
69 };
70 *self = Self::UnavailableAfterCleanupFailure { retained };
71 }
72}
73
74struct OptionalParserRuntime {
76 state: OptionalParserRuntimeState,
78}
79
80struct OptionalParserGroupLease {
82 active: bool,
84}
85
86impl OptionalParserGroupLease {
87 fn acquire() -> Self {
89 OPTIONAL_PARSER_GROUP_ACTIVE.store(true, Ordering::Release);
90 Self { active: true }
91 }
92
93 fn release(&mut self) {
95 OPTIONAL_PARSER_GROUP_ACTIVE.store(false, Ordering::Release);
96 self.active = false;
97 }
98}
99
100impl Drop for OptionalParserGroupLease {
101 fn drop(&mut self) {
102 if self.active {
103 OPTIONAL_PARSER_GROUP_ACTIVE.store(false, Ordering::Release);
104 }
105 }
106}
107
108impl OptionalParserRuntime {
109 const fn new() -> Self {
111 Self {
112 state: OptionalParserRuntimeState::Inactive,
113 }
114 }
115
116 fn activate(
118 &mut self,
119 verified: VerifiedOptionalParserPackSelection,
120 ) -> Result<(), ParserSupervisorError> {
121 let selection = verified.selection_key().clone();
122 if matches!(
123 &self.state,
124 OptionalParserRuntimeState::Resident {
125 verified: current,
126 } if current.selection_key() == &selection
127 ) {
128 return Ok(());
129 }
130 if matches!(
131 self.state,
132 OptionalParserRuntimeState::UnavailableAfterCleanupFailure { .. }
133 ) {
134 return Err(ParserSupervisorError::Cleanup {
135 message: "optional parser runtime is unavailable after an earlier cleanup failure"
136 .to_owned(),
137 });
138 }
139 let previous = std::mem::replace(&mut self.state, OptionalParserRuntimeState::Inactive);
140 if let OptionalParserRuntimeState::Resident { mut verified } = previous
141 && let Err(error) = verified.supervisor_mut().shutdown()
142 {
143 self.state = OptionalParserRuntimeState::UnavailableAfterCleanupFailure {
144 retained: Some(verified),
145 };
146 return Err(error);
147 }
148 self.state = OptionalParserRuntimeState::Resident {
149 verified: Box::new(verified),
150 };
151 Ok(())
152 }
153
154 fn deactivate(&mut self) -> Result<(), ParserSupervisorError> {
156 let previous = std::mem::replace(&mut self.state, OptionalParserRuntimeState::Inactive);
157 match previous {
158 OptionalParserRuntimeState::Resident { mut verified } => {
159 match verified.supervisor_mut().shutdown() {
160 Ok(()) => Ok(()),
161 Err(error) => {
162 self.state = OptionalParserRuntimeState::UnavailableAfterCleanupFailure {
163 retained: Some(verified),
164 };
165 Err(error)
166 }
167 }
168 }
169 OptionalParserRuntimeState::UnavailableAfterCleanupFailure { retained } => {
170 self.state =
171 OptionalParserRuntimeState::UnavailableAfterCleanupFailure { retained };
172 Ok(())
173 }
174 OptionalParserRuntimeState::Inactive => Ok(()),
175 }
176 }
177
178 fn quarantine(&mut self) -> Result<(), ParserSupervisorError> {
180 let cleanup = self.deactivate();
181 if cleanup.is_ok() {
182 self.state =
183 OptionalParserRuntimeState::UnavailableAfterCleanupFailure { retained: None };
184 }
185 cleanup
186 }
187}
188
189pub(super) fn parse_symbol_jobs_controlled(
191 project_root: &Path,
192 project_selection: &OptionalParserPackProjectSelection,
193 pool: &rayon::ThreadPool,
194 jobs: &[SymbolParseJob],
195 options: &SymbolBuildOptions,
196 control: &IndexWorkControl,
197) -> Result<Vec<SymbolParseOutcome>, CliError> {
198 if matches!(
199 project_selection,
200 OptionalParserPackProjectSelection::Inactive
201 ) {
202 deactivate_if_initialized(control)?;
203 let built_in = jobs.iter().collect::<Vec<_>>();
204 return parse_built_in_jobs(pool, &built_in, options, control);
205 }
206
207 let lifecycle = OptionalParserPackLifecycle::new(project_root, None)?;
208 let verified = lifecycle.resolve_selected_pack()?.ok_or_else(|| {
209 CliError::ParserPack(OptionalParserPackLifecycleError::InvalidData {
210 reason: "optional parser selection disappeared before source staging".to_owned(),
211 })
212 })?;
213 if project_selection.selection_key() != Some(verified.selection_key()) {
214 return Err(CliError::ParserPack(
215 OptionalParserPackLifecycleError::InvalidData {
216 reason: "optional parser selection changed before source staging".to_owned(),
217 },
218 ));
219 }
220 let expected_selection = verified.selection_key().clone();
221
222 let mut built_in = Vec::with_capacity(jobs.len());
223 let mut optional = Vec::new();
224 for job in jobs {
225 match optional_language_id(job) {
226 Some(language) if verified.accepts_language(language) => {
227 optional.push((language, job));
228 }
229 _ => built_in.push(job),
230 }
231 }
232 sort_optional_jobs(&mut optional);
233
234 let mut outcomes = parse_built_in_jobs(pool, &built_in, options, control)?;
235 if outcomes.iter().any(terminal_parse_outcome) {
236 return Ok(outcomes);
237 }
238
239 let mut runtime = lock_optional_runtime(control)?;
240 let mut group_lease = OptionalParserGroupLease::acquire();
241 let operation = (|| {
242 service_pending_deactivation(&mut runtime)?;
243 ensure_selection_current(&lifecycle, &expected_selection)?;
244 runtime.activate(verified).map_err(supervisor_error)?;
245 let limits = ParserRequestLimits::new(
246 PARSER_MAX_OUTPUT_BYTES,
247 PARSER_MAX_NODE_COUNT,
248 PARSER_MAX_TREE_DEPTH,
249 )
250 .map_err(ParserSupervisorError::from)
251 .map_err(supervisor_error)?;
252
253 for (language, job) in optional {
254 control.check(IndexWorkStage::SymbolParsing)?;
255 let content = match admit_symbol_job_bytes(job, options, control) {
256 Ok(bytes) => {
257 if let Ok(content) = String::from_utf8(bytes) {
258 content
259 } else {
260 outcomes.push(SymbolParseOutcome::BinaryOrNonUtf8 {
261 path: job.path.clone(),
262 });
263 continue;
264 }
265 }
266 Err(outcome) => {
267 outcomes.push(*outcome);
268 return Ok(outcomes);
269 }
270 };
271 ensure_selection_current(&lifecycle, &expected_selection)?;
272 let deadline = optional_parse_deadline(control)?;
273 let result = match &mut runtime.state {
274 OptionalParserRuntimeState::Resident { verified } => {
275 verified.supervisor_mut().parse(
276 language,
277 content.as_bytes(),
278 limits,
279 deadline,
280 OPTIONAL_PARSE_NO_PROGRESS_TIMEOUT,
281 control.cancellation(),
282 )
283 }
284 OptionalParserRuntimeState::Inactive
285 | OptionalParserRuntimeState::UnavailableAfterCleanupFailure { .. } => {
286 return Err(runtime_unavailable_error());
287 }
288 };
289 if let Err(error) = result {
290 retain_runtime_after_parser_failure(&mut runtime.state, &error);
291 return Err(supervisor_error(error));
292 }
293 let outcome = parse_admitted_symbol_job(
294 job,
295 &content,
296 Some(projectatlas_core::symbols::ParserKind::TreeSitter),
297 options,
298 control,
299 );
300 let terminal = terminal_parse_outcome(&outcome);
301 outcomes.push(outcome);
302 if terminal {
303 break;
304 }
305 }
306 Ok(outcomes)
307 })();
308 group_lease.release();
309 let cleanup = service_pending_deactivation(&mut runtime);
310 combine_optional_operation_and_cleanup(operation, cleanup)
311}
312
313fn retain_runtime_after_parser_failure<T>(
315 state: &mut OptionalParserRuntimeState<T>,
316 error: &ParserSupervisorError,
317) {
318 if error.has_mandatory_cleanup_failure() {
319 state.retain_after_cleanup_failure();
320 }
321}
322
323fn optional_language_id(job: &SymbolParseJob) -> Option<&str> {
325 let capability = language_capability(job.language.as_deref()?)?;
326 (capability.optional_pack == Some(BROAD_PARSER_PACK_ID)).then_some(capability.id)
327}
328
329fn sort_optional_jobs(jobs: &mut [(&str, &SymbolParseJob)]) {
331 jobs.sort_by(|(left_language, left_job), (right_language, right_job)| {
332 left_language
333 .cmp(right_language)
334 .then_with(|| left_job.path.cmp(&right_job.path))
335 });
336}
337
338fn parse_built_in_jobs(
340 pool: &rayon::ThreadPool,
341 jobs: &[&SymbolParseJob],
342 options: &SymbolBuildOptions,
343 control: &IndexWorkControl,
344) -> Result<Vec<SymbolParseOutcome>, CliError> {
345 let mut outcomes = Vec::with_capacity(jobs.len());
346 for batch in jobs.chunks(BUILT_IN_PARSE_BATCH_SIZE) {
347 control.check(IndexWorkStage::SymbolParsing)?;
348 outcomes.extend(pool.install(|| {
349 batch
350 .par_iter()
351 .map(|job| super::parse_symbol_job_controlled(job, options, control))
352 .collect::<Vec<_>>()
353 }));
354 }
355 Ok(outcomes)
356}
357
358fn terminal_parse_outcome(outcome: &SymbolParseOutcome) -> bool {
360 matches!(
361 outcome,
362 SymbolParseOutcome::SourceChanged { .. }
363 | SymbolParseOutcome::Io { .. }
364 | SymbolParseOutcome::InvalidInput { .. }
365 | SymbolParseOutcome::IndexWork(_)
366 )
367}
368
369fn lock_optional_runtime(
371 control: &IndexWorkControl,
372) -> Result<MutexGuard<'static, OptionalParserRuntime>, CliError> {
373 let runtime = OPTIONAL_PARSER_RUNTIME.get_or_init(|| Mutex::new(OptionalParserRuntime::new()));
374 lock_runtime(runtime, control)
375}
376
377fn lock_runtime<'a>(
379 runtime: &'a Mutex<OptionalParserRuntime>,
380 control: &IndexWorkControl,
381) -> Result<MutexGuard<'a, OptionalParserRuntime>, CliError> {
382 loop {
383 control.check(IndexWorkStage::SymbolParsing)?;
384 match runtime.try_lock() {
385 Ok(guard) => return Ok(guard),
386 Err(TryLockError::WouldBlock) => thread::park_timeout(OPTIONAL_RUNTIME_ADMISSION_POLL),
387 Err(TryLockError::Poisoned(poisoned)) => {
388 let mut guard = poisoned.into_inner();
389 if let Err(error) = guard.quarantine() {
390 return Err(supervisor_error(error));
391 }
392 return Err(runtime_unavailable_error());
393 }
394 }
395 }
396}
397
398fn deactivate_if_initialized(control: &IndexWorkControl) -> Result<(), CliError> {
400 let Some(runtime) = OPTIONAL_PARSER_RUNTIME.get() else {
401 return Ok(());
402 };
403 OPTIONAL_PARSER_DEACTIVATION_REQUESTED.store(true, Ordering::Release);
404 loop {
405 if OPTIONAL_PARSER_GROUP_ACTIVE.load(Ordering::Acquire) {
406 return Ok(());
407 }
408 control.check(IndexWorkStage::SymbolParsing)?;
409 match runtime.try_lock() {
410 Ok(mut guard) => return service_pending_deactivation(&mut guard),
411 Err(TryLockError::WouldBlock) => thread::park_timeout(OPTIONAL_RUNTIME_ADMISSION_POLL),
414 Err(TryLockError::Poisoned(poisoned)) => {
415 let mut guard = poisoned.into_inner();
416 OPTIONAL_PARSER_DEACTIVATION_REQUESTED.store(false, Ordering::Release);
417 return guard.quarantine().map_err(supervisor_error);
418 }
419 }
420 }
421}
422
423fn service_pending_deactivation(runtime: &mut OptionalParserRuntime) -> Result<(), CliError> {
425 if !OPTIONAL_PARSER_DEACTIVATION_REQUESTED.swap(false, Ordering::AcqRel) {
426 return Ok(());
427 }
428 runtime.deactivate().map_err(supervisor_error)
429}
430
431fn ensure_selection_current(
433 lifecycle: &OptionalParserPackLifecycle,
434 expected: &OptionalParserPackSelectionKey,
435) -> Result<(), CliError> {
436 let current = lifecycle.derive_project_selection()?;
437 if current.selection_key() == Some(expected) {
438 return Ok(());
439 }
440 Err(CliError::ParserPack(
441 OptionalParserPackLifecycleError::InvalidData {
442 reason: "optional parser selection changed before source transfer".to_owned(),
443 },
444 ))
445}
446
447fn combine_optional_operation_and_cleanup<T>(
449 operation: Result<T, CliError>,
450 cleanup: Result<(), CliError>,
451) -> Result<T, CliError> {
452 match (operation, cleanup) {
453 (Ok(value), Ok(())) => Ok(value),
454 (Err(operation), Ok(())) => Err(operation),
455 (Ok(_), Err(cleanup)) => Err(cleanup),
456 (Err(operation), Err(cleanup)) => Err(CliError::OptionalParserOperationAndCleanup {
457 operation: Box::new(operation),
458 cleanup: Box::new(cleanup),
459 }),
460 }
461}
462
463fn optional_parse_deadline(control: &IndexWorkControl) -> Result<Instant, CliError> {
465 control.check(IndexWorkStage::SymbolParsing)?;
466 let now = Instant::now();
467 let per_file = now.checked_add(OPTIONAL_PARSE_TIMEOUT).ok_or_else(|| {
468 CliError::IndexWork(IndexWorkFailure::DeadlineExceeded {
469 stage: IndexWorkStage::SymbolParsing,
470 })
471 })?;
472 Ok(control
473 .deadline()
474 .map_or(per_file, |deadline| deadline.min(per_file)))
475}
476
477fn supervisor_error(error: ParserSupervisorError) -> CliError {
479 match error {
480 ParserSupervisorError::Cancelled { .. } => {
481 CliError::IndexWork(IndexWorkFailure::Cancelled {
482 stage: IndexWorkStage::SymbolParsing,
483 })
484 }
485 ParserSupervisorError::DeadlineExceeded { .. } => {
486 CliError::IndexWork(IndexWorkFailure::DeadlineExceeded {
487 stage: IndexWorkStage::SymbolParsing,
488 })
489 }
490 other => CliError::ParserPack(OptionalParserPackLifecycleError::Supervisor(other)),
491 }
492}
493
494fn runtime_unavailable_error() -> CliError {
496 CliError::ParserPack(OptionalParserPackLifecycleError::InvalidData {
497 reason: "optional parser runtime is unavailable until process restart".to_owned(),
498 })
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504 use std::sync::atomic::AtomicUsize;
505
506 #[test]
507 fn optional_jobs_sort_by_canonical_grammar_then_path() {
508 let job = |path: &str, language: &str| SymbolParseJob {
509 path: path.to_owned(),
510 native_path: path.into(),
511 expected_content_hash: "a".repeat(64),
512 language: Some(language.to_owned()),
513 fallback_summary: None,
514 purpose_needs_suggestion: false,
515 };
516 let jobs = [
517 job("z.zig", "zig"),
518 job("z.awk", "awk"),
519 job("a.zig", "zig"),
520 job("a.awk", "awk"),
521 ];
522 let mut scheduled = [
523 ("zig", &jobs[0]),
524 ("awk", &jobs[1]),
525 ("zig", &jobs[2]),
526 ("awk", &jobs[3]),
527 ];
528 sort_optional_jobs(&mut scheduled);
529 assert_eq!(
530 scheduled
531 .iter()
532 .map(|(language, job)| (*language, job.path.as_str()))
533 .collect::<Vec<_>>(),
534 [
535 ("awk", "a.awk"),
536 ("awk", "z.awk"),
537 ("zig", "a.zig"),
538 ("zig", "z.zig"),
539 ]
540 );
541 }
542
543 #[test]
544 fn cleanup_failure_quarantines_the_concrete_runtime_state() {
545 let mut runtime = OptionalParserRuntime::new();
546 runtime.state =
547 OptionalParserRuntimeState::UnavailableAfterCleanupFailure { retained: None };
548 assert!(runtime.deactivate().is_ok());
549 assert!(matches!(
550 runtime.state,
551 OptionalParserRuntimeState::UnavailableAfterCleanupFailure { .. }
552 ));
553 }
554
555 #[test]
556 fn operation_and_cleanup_failures_remain_distinct_at_the_runtime_boundary() {
557 let operation = CliError::ParserPack(OptionalParserPackLifecycleError::InvalidData {
558 reason: "synthetic operation failure".to_owned(),
559 });
560 let cleanup = CliError::ParserPack(OptionalParserPackLifecycleError::Supervisor(
561 ParserSupervisorError::Cleanup {
562 message: "synthetic cleanup failure".to_owned(),
563 },
564 ));
565
566 let result = combine_optional_operation_and_cleanup::<()>(Err(operation), Err(cleanup));
567 assert!(matches!(
568 result,
569 Err(CliError::OptionalParserOperationAndCleanup { .. })
570 ));
571 }
572
573 #[test]
574 fn cleanup_failure_retains_execution_owner_until_runtime_state_drops() {
575 struct DropProbe<'a>(&'a AtomicUsize);
576
577 impl Drop for DropProbe<'_> {
578 fn drop(&mut self) {
579 self.0.fetch_add(1, Ordering::Relaxed);
580 }
581 }
582
583 let drops = AtomicUsize::new(0);
584 {
585 let mut state = OptionalParserRuntimeState::Resident {
586 verified: DropProbe(&drops),
587 };
588 state.retain_after_cleanup_failure();
589 assert!(matches!(
590 &state,
591 OptionalParserRuntimeState::UnavailableAfterCleanupFailure { retained: Some(_) }
592 ));
593 assert_eq!(drops.load(Ordering::Relaxed), 0);
594 }
595 assert_eq!(drops.load(Ordering::Relaxed), 1);
596 }
597
598 #[test]
599 fn combined_parser_failure_quarantines_and_retains_the_execution_owner() {
600 struct DropProbe<'a>(&'a AtomicUsize);
601
602 impl Drop for DropProbe<'_> {
603 fn drop(&mut self) {
604 self.0.fetch_add(1, Ordering::Relaxed);
605 }
606 }
607
608 let drops = AtomicUsize::new(0);
609 let mut state = OptionalParserRuntimeState::Resident {
610 verified: DropProbe(&drops),
611 };
612 let error = ParserSupervisorError::OperationAndCleanup {
613 operation: Box::new(ParserSupervisorError::Cancelled { phase: "test" }),
614 cleanup: Box::new(ParserSupervisorError::Cleanup {
615 message: "synthetic cleanup failure".to_owned(),
616 }),
617 };
618
619 retain_runtime_after_parser_failure(&mut state, &error);
620 assert!(matches!(
621 &state,
622 OptionalParserRuntimeState::UnavailableAfterCleanupFailure { retained: Some(_) }
623 ));
624 assert_eq!(drops.load(Ordering::Relaxed), 0);
625 drop(state);
626 assert_eq!(drops.load(Ordering::Relaxed), 1);
627 }
628
629 #[test]
630 fn pending_deactivation_is_consumed_under_the_runtime_lease() {
631 OPTIONAL_PARSER_DEACTIVATION_REQUESTED.store(true, Ordering::Release);
632 let mut runtime = OptionalParserRuntime::new();
633 assert!(service_pending_deactivation(&mut runtime).is_ok());
634 assert!(!OPTIONAL_PARSER_DEACTIVATION_REQUESTED.load(Ordering::Acquire));
635 assert!(matches!(
636 runtime.state,
637 OptionalParserRuntimeState::Inactive
638 ));
639 }
640
641 #[test]
642 fn group_activity_is_cleared_when_the_lease_drops() {
643 OPTIONAL_PARSER_GROUP_ACTIVE.store(false, Ordering::Release);
644 {
645 let _lease = OptionalParserGroupLease::acquire();
646 assert!(OPTIONAL_PARSER_GROUP_ACTIVE.load(Ordering::Acquire));
647 }
648 assert!(!OPTIONAL_PARSER_GROUP_ACTIVE.load(Ordering::Acquire));
649 }
650
651 #[test]
652 fn cancellation_interrupts_global_admission_wait() {
653 let runtime = Mutex::new(OptionalParserRuntime::new());
654 let _holder = runtime
655 .lock()
656 .unwrap_or_else(std::sync::PoisonError::into_inner);
657 let control = IndexWorkControl::new(projectatlas_core::IndexCancellation::new(), None);
658 control.cancel();
659 let result = lock_runtime(&runtime, &control);
660 assert!(matches!(
661 result,
662 Err(CliError::IndexWork(IndexWorkFailure::Cancelled {
663 stage: IndexWorkStage::SymbolParsing,
664 }))
665 ));
666 }
667}