1use super::catalog::ChunkCatalogSnapshot;
9#[cfg(feature = "zvec-rust-fts")]
10use super::chunk::{chunk_id, digest_content};
11use super::lexical::LexicalSearchRequest;
12#[cfg(feature = "zvec-rust-fts")]
13use super::lexical::{path_matches, query_terms};
14#[cfg(feature = "zvec-rust-fts")]
15use super::types::WorkspaceChunk;
16use super::types::{WorkspaceIndexError, WorkspaceIndexResult, WorkspaceLexicalEngine};
17use serde::{Deserialize, Serialize};
18#[cfg(feature = "zvec-rust-fts")]
19use std::collections::HashSet;
20#[cfg(feature = "zvec-rust-fts")]
21use std::fs;
22#[cfg(feature = "zvec-rust-fts")]
23use std::io::Read;
24use std::path::{Path, PathBuf};
25#[cfg(feature = "zvec-rust-fts")]
26use std::sync::atomic::{AtomicBool, Ordering};
27use std::sync::Arc;
28#[cfg(feature = "zvec-rust-fts")]
29use std::sync::{Condvar, Mutex, RwLock};
30
31#[cfg(feature = "zvec-rust-fts")]
32const MANIFEST_SCHEMA_VERSION: u32 = 2;
33#[cfg(feature = "zvec-rust-fts")]
34const CURRENT_FILE: &str = "CURRENT";
35#[cfg(feature = "zvec-rust-fts")]
36const MANIFEST_FILE: &str = "manifest.json";
37#[cfg(feature = "zvec-rust-fts")]
38const STORAGE_LOCK_FILE: &str = ".index.lock";
39
40#[cfg(feature = "zvec-rust-fts")]
41#[derive(Clone, Debug, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase", deny_unknown_fields)]
43struct PersistedChunk {
44 id: String,
45 path: String,
46 language: Option<String>,
47 start_line: usize,
48 end_line: usize,
49 start_byte: usize,
50 end_byte: usize,
51 content_digest: String,
52 text_digest: String,
56 source_revision: u64,
57 text: String,
58}
59
60#[cfg(feature = "zvec-rust-fts")]
61impl PersistedChunk {
62 fn from_chunk(chunk: &WorkspaceChunk) -> Self {
63 Self {
64 id: chunk.id.as_str().to_owned(),
65 path: chunk.path.to_string(),
66 language: chunk.language.as_deref().map(str::to_owned),
67 start_line: chunk.start_line,
68 end_line: chunk.end_line,
69 start_byte: chunk.start_byte,
70 end_byte: chunk.end_byte,
71 content_digest: chunk.content_digest.to_string(),
72 text_digest: digest_content(chunk.text.as_ref()).to_string(),
73 source_revision: chunk.source_revision,
74 text: chunk.text.to_string(),
75 }
76 }
77
78 fn into_chunk(self) -> WorkspaceChunk {
79 WorkspaceChunk {
80 id: super::types::WorkspaceChunkId::new(self.id),
81 path: Arc::from(self.path),
82 language: self.language.map(Arc::from),
83 start_line: self.start_line,
84 end_line: self.end_line,
85 start_byte: self.start_byte,
86 end_byte: self.end_byte,
87 content_digest: Arc::from(self.content_digest),
88 source_revision: self.source_revision,
89 text: Arc::from(self.text),
90 }
91 }
92}
93
94#[cfg(feature = "zvec-rust-fts")]
95#[derive(Clone, Debug, Serialize, Deserialize)]
96#[serde(rename_all = "camelCase", deny_unknown_fields)]
97struct GenerationManifest {
98 schema_version: u32,
99 lexical_engine: String,
100 catalog_revision: u64,
101 source_revision: u64,
102 chunks: Vec<PersistedChunk>,
103}
104
105#[cfg(feature = "zvec-rust-fts")]
106struct PersistentState {
107 generation: String,
108 catalog_revision: u64,
109 source_revision: u64,
110 indexed_chunks: Arc<[Arc<WorkspaceChunk>]>,
111 indexed_files: usize,
112 index: super::zvec_rust::ZvecRustLexicalIndex,
113}
114
115#[cfg(feature = "zvec-rust-fts")]
116struct PersistentOperationGuard {
117 active: Arc<(Mutex<usize>, Condvar)>,
118}
119
120#[cfg(feature = "zvec-rust-fts")]
121impl Drop for PersistentOperationGuard {
122 fn drop(&mut self) {
123 let (lock, wake) = &*self.active;
124 if let Ok(mut count) = lock.lock() {
125 *count = count.saturating_sub(1);
126 wake.notify_all();
127 }
128 }
129}
130
131#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
132#[serde(rename_all = "snake_case")]
133pub enum WorkspacePersistentIndexPhase {
134 Absent,
135 Building,
136 Ready,
137}
138
139#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct WorkspacePersistentIndexStatus {
142 pub phase: WorkspacePersistentIndexPhase,
143 pub generation: Option<String>,
144 pub catalog_revision: u64,
145 pub source_revision: u64,
146 pub indexed_chunks: usize,
147}
148
149pub struct WorkspacePersistentIndex {
152 root: PathBuf,
153 engine: WorkspaceLexicalEngine,
154 #[cfg(feature = "zvec-rust-fts")]
155 writer: Mutex<()>,
156 #[cfg(feature = "zvec-rust-fts")]
157 active_operations: Arc<(Mutex<usize>, Condvar)>,
158 #[cfg(feature = "zvec-rust-fts")]
159 building: AtomicBool,
160 #[cfg(feature = "zvec-rust-fts")]
161 state: RwLock<Option<PersistentState>>,
162}
163
164#[cfg(feature = "zvec-rust-fts")]
165struct BuildActivityGuard<'a>(&'a AtomicBool);
166
167#[cfg(feature = "zvec-rust-fts")]
168impl Drop for BuildActivityGuard<'_> {
169 fn drop(&mut self) {
170 self.0.store(false, Ordering::Release);
171 }
172}
173
174impl std::fmt::Debug for WorkspacePersistentIndex {
175 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 formatter
177 .debug_struct("WorkspacePersistentIndex")
178 .field("root", &self.root)
179 .field("engine", &self.engine)
180 .finish_non_exhaustive()
181 }
182}
183
184impl WorkspacePersistentIndex {
185 pub fn open(
186 root: impl Into<PathBuf>,
187 engine: WorkspaceLexicalEngine,
188 ) -> WorkspaceIndexResult<Arc<Self>> {
189 let root = strip_windows_verbatim_prefix(root.into());
193 #[cfg(not(feature = "zvec-rust-fts"))]
194 {
195 let _ = (&root, engine);
196 Err(WorkspaceIndexError::InvalidConfig(
197 "persistent workspace indexing requires the zvec-rust-fts feature".to_owned(),
198 ))
199 }
200
201 #[cfg(feature = "zvec-rust-fts")]
202 {
203 if engine != WorkspaceLexicalEngine::ZvecRust {
204 return Err(WorkspaceIndexError::InvalidConfig(
205 "persistent workspace indexing currently requires the zvec-rust lexical engine"
206 .to_owned(),
207 ));
208 }
209 fs::create_dir_all(&root).map_err(|error| WorkspaceIndexError::ReadFailed {
210 path: root.display().to_string(),
211 message: error.to_string(),
212 })?;
213 let index = Arc::new(Self {
214 root,
215 engine,
216 writer: Mutex::new(()),
217 active_operations: Arc::new((Mutex::new(0), Condvar::new())),
218 building: AtomicBool::new(false),
219 state: RwLock::new(None),
220 });
221 let storage_guard = index.acquire_storage_lock()?;
225 if let Err(error) = index.load_current() {
226 tracing::warn!(%error, path = %index.root.display(), "persistent workspace index will be rebuilt");
227 } else if let Some(generation) = index.status().generation {
228 if let Err(error) = gc_generations(&index.root, &generation) {
229 tracing::warn!(%error, path = %index.root.display(), "persistent workspace generation cleanup failed");
230 }
231 }
232 drop(storage_guard);
233 Ok(index)
234 }
235 }
236
237 pub fn root(&self) -> &Path {
238 &self.root
239 }
240
241 pub fn engine(&self) -> WorkspaceLexicalEngine {
242 self.engine
243 }
244
245 pub fn is_ready(&self) -> bool {
246 #[cfg(feature = "zvec-rust-fts")]
247 {
248 self.state
249 .read()
250 .map(|state| state.is_some())
251 .unwrap_or(false)
252 }
253 #[cfg(not(feature = "zvec-rust-fts"))]
254 {
255 false
256 }
257 }
258
259 pub fn status(&self) -> WorkspacePersistentIndexStatus {
260 #[cfg(feature = "zvec-rust-fts")]
261 {
262 let building = self.building.load(Ordering::Acquire);
263 self.state
264 .read()
265 .ok()
266 .and_then(|state| {
267 state.as_ref().map(|state| WorkspacePersistentIndexStatus {
268 phase: if building {
269 WorkspacePersistentIndexPhase::Building
270 } else {
271 WorkspacePersistentIndexPhase::Ready
272 },
273 generation: Some(state.generation.clone()),
274 catalog_revision: state.catalog_revision,
275 source_revision: state.source_revision,
276 indexed_chunks: state.indexed_chunks.len(),
277 })
278 })
279 .unwrap_or(WorkspacePersistentIndexStatus {
280 phase: if building {
281 WorkspacePersistentIndexPhase::Building
282 } else {
283 WorkspacePersistentIndexPhase::Absent
284 },
285 generation: None,
286 catalog_revision: 0,
287 source_revision: 0,
288 indexed_chunks: 0,
289 })
290 }
291 #[cfg(not(feature = "zvec-rust-fts"))]
292 {
293 WorkspacePersistentIndexStatus {
294 phase: WorkspacePersistentIndexPhase::Absent,
295 generation: None,
296 catalog_revision: 0,
297 source_revision: 0,
298 indexed_chunks: 0,
299 }
300 }
301 }
302
303 pub fn rebuild(&self, snapshot: &ChunkCatalogSnapshot) -> WorkspaceIndexResult<()> {
304 self.sync_snapshot(snapshot)
305 }
306
307 pub fn drop_index(&self) -> WorkspaceIndexResult<()> {
308 #[cfg(not(feature = "zvec-rust-fts"))]
309 {
310 Err(WorkspaceIndexError::InvalidConfig(
311 "persistent workspace indexing requires the zvec-rust-fts feature".to_owned(),
312 ))
313 }
314
315 #[cfg(feature = "zvec-rust-fts")]
316 {
317 let _storage_guard = self.acquire_storage_lock()?;
318 let _write_guard = self
319 .writer
320 .lock()
321 .map_err(|_| WorkspaceIndexError::LockPoisoned)?;
322 self.wait_for_idle();
323 let previous = self
324 .state
325 .write()
326 .map_err(|_| WorkspaceIndexError::LockPoisoned)?
327 .take();
328 drop(previous);
329 let current = self.root.join(CURRENT_FILE);
330 if let Err(error) = fs::remove_file(¤t) {
331 if error.kind() != std::io::ErrorKind::NotFound {
332 return Err(WorkspaceIndexError::ReadFailed {
333 path: current.display().to_string(),
334 message: error.to_string(),
335 });
336 }
337 }
338 for entry in
339 fs::read_dir(&self.root).map_err(|error| WorkspaceIndexError::ReadFailed {
340 path: self.root.display().to_string(),
341 message: error.to_string(),
342 })?
343 {
344 let path = entry
345 .map_err(|error| WorkspaceIndexError::ReadFailed {
346 path: self.root.display().to_string(),
347 message: error.to_string(),
348 })?
349 .path();
350 let name = path
351 .file_name()
352 .and_then(|name| name.to_str())
353 .unwrap_or_default();
354 if name.starts_with("generation-") || name.starts_with(".generation-") {
355 remove_path_if_exists(&path)?;
356 }
357 }
358 Ok(())
359 }
360 }
361
362 pub fn sync_snapshot(&self, snapshot: &ChunkCatalogSnapshot) -> WorkspaceIndexResult<()> {
363 #[cfg(not(feature = "zvec-rust-fts"))]
364 {
365 let _ = snapshot;
366 Err(WorkspaceIndexError::InvalidConfig(
367 "persistent workspace indexing requires the zvec-rust-fts feature".to_owned(),
368 ))
369 }
370
371 #[cfg(feature = "zvec-rust-fts")]
372 {
373 let _operation = self.begin_operation()?;
374 self.building.store(true, Ordering::Release);
375 let _building = BuildActivityGuard(&self.building);
376 let _storage_guard = self.acquire_storage_lock()?;
380 let _write_guard = self
381 .writer
382 .lock()
383 .map_err(|_| WorkspaceIndexError::LockPoisoned)?;
384 let persisted = match self.read_current_manifest() {
385 Ok(manifest) => manifest,
386 Err(error) => {
387 tracing::warn!(%error, path = %self.root.display(), "ignoring unreadable persistent CURRENT while rebuilding");
388 None
389 }
390 };
391 if let Some((_, manifest)) = persisted {
392 let requested = (snapshot.source_revision(), snapshot.revision());
393 let on_disk = (manifest.source_revision, manifest.catalog_revision);
394 if on_disk > requested {
395 return Err(WorkspaceIndexError::StaleRevision {
396 requested: snapshot.source_revision(),
397 current: manifest.source_revision,
398 });
399 }
400 }
401 let current_state = self
402 .state
403 .read()
404 .map_err(|_| WorkspaceIndexError::LockPoisoned)?;
405 if current_state
406 .as_ref()
407 .is_some_and(|state| state.source_revision > snapshot.source_revision())
408 {
409 let current = current_state
410 .as_ref()
411 .map(|state| state.source_revision)
412 .unwrap_or_default();
413 return Err(WorkspaceIndexError::StaleRevision {
414 requested: snapshot.source_revision(),
415 current,
416 });
417 }
418 if current_state.as_ref().is_some_and(|state| {
419 state.catalog_revision == snapshot.revision()
420 && state.source_revision == snapshot.source_revision()
421 }) {
422 return Ok(());
423 }
424 let chunks = snapshot.chunks();
425 let indexed_chunks: Arc<[Arc<WorkspaceChunk>]> = Arc::from(
426 chunks
427 .iter()
428 .filter(|chunk| !super::lexical::tokenize(chunk.text.as_ref()).is_empty())
429 .cloned()
430 .collect::<Vec<_>>(),
431 );
432
433 if let Some(state) = current_state.as_ref() {
443 if indexed_chunks_match(&state.indexed_chunks, &indexed_chunks) {
444 let generation = state.generation.clone();
445 drop(current_state);
446 let mut current = self
447 .state
448 .write()
449 .map_err(|_| WorkspaceIndexError::LockPoisoned)?;
450 let Some(current) = current.as_mut() else {
451 return Err(WorkspaceIndexError::InvalidConfig(
452 "persistent workspace index disappeared during reuse".to_owned(),
453 ));
454 };
455 if current.generation != generation {
456 return Err(WorkspaceIndexError::StaleRevision {
457 requested: snapshot.source_revision(),
458 current: current.source_revision,
459 });
460 }
461 current.catalog_revision = snapshot.revision();
462 current.source_revision = snapshot.source_revision();
463 current.indexed_files = distinct_path_count(&indexed_chunks);
464 current.indexed_chunks = indexed_chunks;
465 drop(_operation);
466 if let Err(error) = gc_generations(&self.root, &generation) {
467 tracing::warn!(%error, path = %self.root.display(), "persistent workspace generation cleanup failed");
468 }
469 return Ok(());
470 }
471 }
472 drop(current_state);
473
474 let generation = format!("generation-{}", snapshot.revision());
475 let staging = self.root.join(format!(".{generation}.staging"));
476 let destination = self.root.join(&generation);
477 remove_path_if_exists(&staging)?;
478 remove_path_if_exists(&destination)?;
479 fs::create_dir_all(&staging).map_err(|error| WorkspaceIndexError::ReadFailed {
480 path: staging.display().to_string(),
481 message: error.to_string(),
482 })?;
483
484 let collection_path = staging.join("collection");
485 let mut index = super::zvec_rust::ZvecRustLexicalIndex::build_at_path(
486 &collection_path,
487 indexed_chunks
488 .iter()
489 .map(|chunk| (chunk.id.as_str(), chunk.text.as_ref())),
490 )
491 .map_err(|error| {
492 WorkspaceIndexError::InvalidConfig(format!("persistent zvec index failed: {error}"))
493 })?;
494 let manifest = GenerationManifest {
495 schema_version: MANIFEST_SCHEMA_VERSION,
496 lexical_engine: self.engine.stable_id().to_owned(),
497 catalog_revision: snapshot.revision(),
498 source_revision: snapshot.source_revision(),
499 chunks: chunks
500 .iter()
501 .map(|chunk| PersistedChunk::from_chunk(chunk))
502 .collect(),
503 };
504 write_json_atomic(&staging.join(MANIFEST_FILE), &manifest)?;
505 fs::rename(&staging, &destination).map_err(|error| {
506 WorkspaceIndexError::ReadFailed {
507 path: destination.display().to_string(),
508 message: error.to_string(),
509 }
510 })?;
511 index.relocate_collection_path(destination.join("collection"));
512 write_current(&self.root, &generation)?;
513 let indexed_files = distinct_path_count(&indexed_chunks);
514
515 let next = PersistentState {
516 generation: generation.clone(),
517 catalog_revision: manifest.catalog_revision,
518 source_revision: manifest.source_revision,
519 indexed_chunks,
520 indexed_files,
521 index,
522 };
523 *self
524 .state
525 .write()
526 .map_err(|_| WorkspaceIndexError::LockPoisoned)? = Some(next);
527 drop(_operation);
528 if let Err(error) = gc_generations(&self.root, &generation) {
529 tracing::warn!(%error, path = %self.root.display(), "persistent workspace generation cleanup failed");
530 }
531 Ok(())
532 }
533 }
534
535 pub fn search(
536 &self,
537 request: &LexicalSearchRequest,
538 ) -> WorkspaceIndexResult<super::lexical::LexicalSearchResult> {
539 #[cfg(not(feature = "zvec-rust-fts"))]
540 {
541 let _ = request;
542 Err(WorkspaceIndexError::InvalidConfig(
543 "persistent workspace indexing requires the zvec-rust-fts feature".to_owned(),
544 ))
545 }
546
547 #[cfg(feature = "zvec-rust-fts")]
548 {
549 let _operation = self.begin_operation()?;
550 super::lexical::validate_request(request)?;
551 let _storage_guard = self.acquire_storage_read_lock()?;
556 let terms = query_terms(request.query.trim(), 32);
557 if terms.is_empty() {
558 return Err(WorkspaceIndexError::InvalidQuery(
559 "query must contain a letter, number, underscore, or CJK character".to_owned(),
560 ));
561 }
562 let glob = request
563 .glob
564 .as_deref()
565 .map(glob::Pattern::new)
566 .transpose()
567 .map_err(|error| WorkspaceIndexError::InvalidQuery(error.to_string()))?;
568 let state_guard = self
569 .state
570 .read()
571 .map_err(|_| WorkspaceIndexError::LockPoisoned)?;
572 let Some(state) = state_guard.as_ref() else {
573 return Ok(super::lexical::LexicalSearchResult {
574 catalog_revision: 0,
575 source_revision: 0,
576 lexical_engine: self.engine,
577 query_terms: terms,
578 matching_files: 0,
579 selected_files: 0,
580 scored_chunks: 0,
581 candidate_truncated: false,
582 hits: Vec::new(),
583 });
584 };
585
586 let has_path_filter = !request.path.is_root() || glob.is_some();
587 let matching_files = if has_path_filter {
588 state
589 .indexed_chunks
590 .iter()
591 .filter(|chunk| path_matches(chunk.path.as_ref(), &request.path, glob.as_ref()))
592 .map(|chunk| chunk.path.clone())
593 .collect::<HashSet<_>>()
594 .len()
595 } else {
596 state.indexed_files
597 };
598 let query_limit = request
599 .limit
600 .saturating_mul(request.max_candidate_files.max(1))
601 .min(state.index.document_count().max(request.limit));
602 let ranked = state.index.search(&terms, query_limit).map_err(|error| {
603 WorkspaceIndexError::InvalidQuery(format!("zvec-rust FTS search failed: {error}"))
604 })?;
605 let mut selected_files = HashSet::new();
606 let mut hits = Vec::new();
607 let mut per_file = std::collections::HashMap::<Arc<str>, usize>::new();
608 for (ordinal, score) in ranked {
609 let Some(chunk) = state.indexed_chunks.get(ordinal).cloned() else {
610 continue;
611 };
612 if !path_matches(chunk.path.as_ref(), &request.path, glob.as_ref()) {
613 continue;
614 }
615 if !selected_files.contains(&chunk.path)
616 && selected_files.len() >= request.max_candidate_files
617 {
618 continue;
619 }
620 selected_files.insert(chunk.path.clone());
621 let count = per_file.entry(Arc::clone(&chunk.path)).or_default();
622 if *count >= request.max_results_per_file {
623 continue;
624 }
625 *count += 1;
626 hits.push(super::lexical::LexicalSearchHit { chunk, score });
627 if hits.len() >= request.limit {
628 break;
629 }
630 }
631 let candidate_truncated = matching_files > request.max_candidate_files;
632 Ok(super::lexical::LexicalSearchResult {
633 catalog_revision: state.catalog_revision,
634 source_revision: state.source_revision,
635 lexical_engine: self.engine,
636 query_terms: terms,
637 matching_files,
638 selected_files: selected_files.len(),
639 scored_chunks: state.indexed_chunks.len(),
640 candidate_truncated,
641 hits,
642 })
643 }
644 }
645
646 pub(crate) fn wait_for_idle(&self) {
652 #[cfg(feature = "zvec-rust-fts")]
653 {
654 let (lock, wake) = &*self.active_operations;
655 let Ok(mut count) = lock.lock() else {
656 return;
657 };
658 while *count != 0 {
659 match wake.wait(count) {
660 Ok(next) => count = next,
661 Err(_) => return,
662 }
663 }
664 }
665 }
666
667 #[cfg(feature = "zvec-rust-fts")]
668 fn begin_operation(&self) -> WorkspaceIndexResult<PersistentOperationGuard> {
669 let (lock, _) = &*self.active_operations;
670 let mut count = lock.lock().map_err(|_| WorkspaceIndexError::LockPoisoned)?;
671 *count = count.checked_add(1).ok_or_else(|| {
672 WorkspaceIndexError::InvalidConfig("persistent operation count overflow".to_owned())
673 })?;
674 Ok(PersistentOperationGuard {
675 active: Arc::clone(&self.active_operations),
676 })
677 }
678
679 #[cfg(feature = "zvec-rust-fts")]
680 fn acquire_storage_lock(&self) -> WorkspaceIndexResult<std::fs::File> {
681 use fs2::FileExt;
682
683 let file = self.open_storage_lock_file()?;
684 let path = self.root.join(STORAGE_LOCK_FILE);
685 file.lock_exclusive()
686 .map_err(|error| WorkspaceIndexError::ReadFailed {
687 path: path.display().to_string(),
688 message: format!("failed to lock persistent index: {error}"),
689 })?;
690 Ok(file)
691 }
692
693 #[cfg(feature = "zvec-rust-fts")]
694 fn acquire_storage_read_lock(&self) -> WorkspaceIndexResult<std::fs::File> {
695 let file = self.open_storage_lock_file()?;
696 let path = self.root.join(STORAGE_LOCK_FILE);
697 fs2::FileExt::lock_shared(&file).map_err(|error| WorkspaceIndexError::ReadFailed {
698 path: path.display().to_string(),
699 message: format!("failed to share-lock persistent index: {error}"),
700 })?;
701 Ok(file)
702 }
703
704 #[cfg(feature = "zvec-rust-fts")]
705 fn open_storage_lock_file(&self) -> WorkspaceIndexResult<std::fs::File> {
706 let path = self.root.join(STORAGE_LOCK_FILE);
707 let file = fs::OpenOptions::new()
708 .create(true)
709 .truncate(false)
710 .read(true)
711 .write(true)
712 .open(&path)
713 .map_err(|error| WorkspaceIndexError::ReadFailed {
714 path: path.display().to_string(),
715 message: format!("failed to open persistent index lock: {error}"),
716 })?;
717 Ok(file)
718 }
719
720 #[cfg(feature = "zvec-rust-fts")]
721 fn load_current(&self) -> WorkspaceIndexResult<()> {
722 let Some((generation, manifest)) = self.read_current_manifest()? else {
723 return Ok(());
724 };
725 let generation_root = self.root.join(&generation);
726 let chunks: Arc<[Arc<WorkspaceChunk>]> = Arc::from(
727 manifest
728 .chunks
729 .into_iter()
730 .map(PersistedChunk::into_chunk)
731 .map(Arc::new)
732 .collect::<Vec<_>>(),
733 );
734 let indexed_chunks: Arc<[Arc<WorkspaceChunk>]> = Arc::from(
735 chunks
736 .iter()
737 .filter(|chunk| !super::lexical::tokenize(chunk.text.as_ref()).is_empty())
738 .cloned()
739 .collect::<Vec<_>>(),
740 );
741 let index = super::zvec_rust::ZvecRustLexicalIndex::open_persistent(
742 generation_root.join("collection"),
743 indexed_chunks
744 .iter()
745 .map(|chunk| (chunk.id.as_str(), chunk.text.as_ref())),
746 )
747 .map_err(|error| {
748 WorkspaceIndexError::InvalidConfig(format!(
749 "persistent zvec index failed to open: {error}"
750 ))
751 })?;
752 let indexed_files = distinct_path_count(&indexed_chunks);
753 *self
754 .state
755 .write()
756 .map_err(|_| WorkspaceIndexError::LockPoisoned)? = Some(PersistentState {
757 generation,
758 catalog_revision: manifest.catalog_revision,
759 source_revision: manifest.source_revision,
760 indexed_chunks,
761 indexed_files,
762 index,
763 });
764 Ok(())
765 }
766
767 #[cfg(feature = "zvec-rust-fts")]
768 fn read_current_manifest(&self) -> WorkspaceIndexResult<Option<(String, GenerationManifest)>> {
769 let current = self.root.join(CURRENT_FILE);
770 let generation = match fs::read_to_string(¤t) {
771 Ok(value) => value.trim().to_owned(),
772 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
773 Err(error) => {
774 return Err(WorkspaceIndexError::ReadFailed {
775 path: current.display().to_string(),
776 message: error.to_string(),
777 })
778 }
779 };
780 if generation.is_empty() || !safe_generation_name(&generation) {
781 return Err(WorkspaceIndexError::InvalidConfig(
782 "persistent zvec CURRENT contains an invalid generation name".to_owned(),
783 ));
784 }
785 let generation_root = self.root.join(&generation);
786 let manifest: GenerationManifest = read_json(&generation_root.join(MANIFEST_FILE))?;
787 if manifest.schema_version != MANIFEST_SCHEMA_VERSION
788 || manifest.lexical_engine != self.engine.stable_id()
789 {
790 return Err(WorkspaceIndexError::InvalidConfig(
791 "persistent zvec index schema or lexical engine is incompatible".to_owned(),
792 ));
793 }
794 validate_persisted_chunks(&manifest.chunks)?;
795 Ok(Some((generation, manifest)))
796 }
797}
798
799#[cfg(feature = "zvec-rust-fts")]
800fn safe_generation_name(value: &str) -> bool {
801 !value.is_empty()
802 && value.chars().all(|character| {
803 character.is_ascii_alphanumeric() || character == '-' || character == '_'
804 })
805}
806
807#[cfg(feature = "zvec-rust-fts")]
808fn gc_generations(root: &Path, current_generation: &str) -> WorkspaceIndexResult<()> {
809 if !safe_generation_name(current_generation) {
810 return Err(WorkspaceIndexError::InvalidConfig(
811 "persistent zvec cleanup received an invalid current generation".to_owned(),
812 ));
813 }
814 for entry in fs::read_dir(root).map_err(|error| WorkspaceIndexError::ReadFailed {
815 path: root.display().to_string(),
816 message: error.to_string(),
817 })? {
818 let path = entry
819 .map_err(|error| WorkspaceIndexError::ReadFailed {
820 path: root.display().to_string(),
821 message: error.to_string(),
822 })?
823 .path();
824 let name = path
825 .file_name()
826 .and_then(|name| name.to_str())
827 .unwrap_or_default();
828 if name == current_generation {
829 continue;
830 }
831 if name.starts_with("generation-") || name.starts_with(".generation-") {
832 remove_path_if_exists(&path)?;
833 }
834 }
835 Ok(())
836}
837
838#[cfg(feature = "zvec-rust-fts")]
839fn remove_path_if_exists(path: &Path) -> WorkspaceIndexResult<()> {
840 match fs::remove_dir_all(path) {
841 Ok(()) => Ok(()),
842 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
843 Err(error) => Err(WorkspaceIndexError::ReadFailed {
844 path: path.display().to_string(),
845 message: error.to_string(),
846 }),
847 }
848}
849
850fn strip_windows_verbatim_prefix(path: PathBuf) -> PathBuf {
855 #[cfg(windows)]
856 {
857 let value = path.to_string_lossy();
858 if let Some(stripped) = value.strip_prefix(r"\\?\UNC\") {
859 return PathBuf::from(format!(r"\\{stripped}"));
860 }
861 if let Some(stripped) = value.strip_prefix(r"\\?\") {
862 return PathBuf::from(stripped);
863 }
864 }
865 path
866}
867
868#[cfg(feature = "zvec-rust-fts")]
869fn write_current(root: &Path, generation: &str) -> WorkspaceIndexResult<()> {
870 let temporary = root.join(".CURRENT.tmp");
871 fs::write(&temporary, format!("{generation}\n")).map_err(|error| {
872 WorkspaceIndexError::ReadFailed {
873 path: temporary.display().to_string(),
874 message: error.to_string(),
875 }
876 })?;
877 #[cfg(windows)]
878 let _ = fs::remove_file(root.join(CURRENT_FILE));
879 fs::rename(&temporary, root.join(CURRENT_FILE)).map_err(|error| {
880 WorkspaceIndexError::ReadFailed {
881 path: root.join(CURRENT_FILE).display().to_string(),
882 message: error.to_string(),
883 }
884 })
885}
886
887#[cfg(feature = "zvec-rust-fts")]
888fn write_json_atomic<T: Serialize>(path: &Path, value: &T) -> WorkspaceIndexResult<()> {
889 let temporary = path.with_extension("json.tmp");
890 let bytes = serde_json::to_vec(value).map_err(|error| {
891 WorkspaceIndexError::InvalidConfig(format!(
892 "persistent zvec manifest serialization failed: {error}"
893 ))
894 })?;
895 fs::write(&temporary, bytes).map_err(|error| WorkspaceIndexError::ReadFailed {
896 path: temporary.display().to_string(),
897 message: error.to_string(),
898 })?;
899 fs::rename(&temporary, path).map_err(|error| WorkspaceIndexError::ReadFailed {
900 path: path.display().to_string(),
901 message: error.to_string(),
902 })
903}
904
905#[cfg(feature = "zvec-rust-fts")]
906fn read_json<T: for<'de> Deserialize<'de>>(path: &Path) -> WorkspaceIndexResult<T> {
907 const MAX_MANIFEST_BYTES: u64 = 256 * 1024 * 1024;
908 let file = fs::File::open(path).map_err(|error| WorkspaceIndexError::ReadFailed {
909 path: path.display().to_string(),
910 message: error.to_string(),
911 })?;
912 let declared_len = file
913 .metadata()
914 .map_err(|error| WorkspaceIndexError::ReadFailed {
915 path: path.display().to_string(),
916 message: error.to_string(),
917 })?
918 .len();
919 if declared_len > MAX_MANIFEST_BYTES {
920 return Err(WorkspaceIndexError::InvalidConfig(format!(
921 "persistent zvec manifest exceeds the {MAX_MANIFEST_BYTES} byte limit"
922 )));
923 }
924 let mut reader = file.take(MAX_MANIFEST_BYTES + 1);
925 let mut bytes = Vec::with_capacity(
926 usize::try_from(declared_len)
927 .unwrap_or(usize::MAX)
928 .min(1024 * 1024),
929 );
930 reader
931 .read_to_end(&mut bytes)
932 .map_err(|error| WorkspaceIndexError::ReadFailed {
933 path: path.display().to_string(),
934 message: error.to_string(),
935 })?;
936 if bytes.len() as u64 > MAX_MANIFEST_BYTES {
937 return Err(WorkspaceIndexError::InvalidConfig(format!(
938 "persistent zvec manifest exceeds the {MAX_MANIFEST_BYTES} byte limit"
939 )));
940 }
941 serde_json::from_slice(&bytes).map_err(|error| {
942 WorkspaceIndexError::InvalidConfig(format!("persistent zvec manifest is invalid: {error}"))
943 })
944}
945
946#[cfg(feature = "zvec-rust-fts")]
947fn validate_persisted_chunks(chunks: &[PersistedChunk]) -> WorkspaceIndexResult<()> {
948 let mut ids = HashSet::with_capacity(chunks.len());
949 for (index, chunk) in chunks.iter().enumerate() {
950 if chunk.path.is_empty() {
951 return Err(WorkspaceIndexError::InvalidConfig(format!(
952 "persistent zvec manifest chunk {index} has an empty path"
953 )));
954 }
955 if chunk.start_byte >= chunk.end_byte {
956 return Err(WorkspaceIndexError::InvalidConfig(format!(
957 "persistent zvec manifest chunk {index} has an invalid byte range"
958 )));
959 }
960 if chunk.start_line == 0 || chunk.start_line > chunk.end_line {
961 return Err(WorkspaceIndexError::InvalidConfig(format!(
962 "persistent zvec manifest chunk {index} has an invalid line range"
963 )));
964 }
965 if !valid_sha256_digest(&chunk.content_digest) || !valid_sha256_digest(&chunk.text_digest) {
966 return Err(WorkspaceIndexError::InvalidConfig(format!(
967 "persistent zvec manifest chunk {index} has a non-canonical digest"
968 )));
969 }
970 let expected_text_digest = digest_content(&chunk.text);
971 if chunk.text.is_empty() || expected_text_digest.as_ref() != chunk.text_digest {
972 return Err(WorkspaceIndexError::InvalidConfig(format!(
973 "persistent zvec manifest chunk {index} text digest does not match its payload"
974 )));
975 }
976 let expected_id = chunk_id(
977 &chunk.path,
978 &chunk.content_digest,
979 chunk.start_byte,
980 chunk.end_byte,
981 );
982 if chunk.id != expected_id.as_str() {
983 return Err(WorkspaceIndexError::InvalidConfig(format!(
984 "persistent zvec manifest chunk {index} id does not bind its metadata"
985 )));
986 }
987 if !ids.insert(chunk.id.as_str()) {
988 return Err(WorkspaceIndexError::InvalidConfig(format!(
989 "persistent zvec manifest contains duplicate chunk id at index {index}"
990 )));
991 }
992 }
993 Ok(())
994}
995
996#[cfg(feature = "zvec-rust-fts")]
997fn valid_sha256_digest(value: &str) -> bool {
998 value.strip_prefix("sha256:").is_some_and(|hex| {
999 hex.len() == 64
1000 && hex
1001 .bytes()
1002 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
1003 })
1004}
1005
1006#[cfg(feature = "zvec-rust-fts")]
1007fn indexed_chunks_match(left: &[Arc<WorkspaceChunk>], right: &[Arc<WorkspaceChunk>]) -> bool {
1008 left.len() == right.len()
1009 && left.iter().zip(right).all(|(left, right)| {
1010 left.id.as_str() == right.id.as_str()
1011 && left.path == right.path
1012 && left.language == right.language
1013 && left.start_line == right.start_line
1014 && left.end_line == right.end_line
1015 && left.start_byte == right.start_byte
1016 && left.end_byte == right.end_byte
1017 && left.content_digest == right.content_digest
1018 })
1019}
1020
1021#[cfg(feature = "zvec-rust-fts")]
1022fn distinct_path_count(chunks: &[Arc<WorkspaceChunk>]) -> usize {
1023 chunks
1024 .iter()
1025 .map(|chunk| Arc::clone(&chunk.path))
1026 .collect::<HashSet<_>>()
1027 .len()
1028}
1029
1030#[cfg(all(test, feature = "zvec-rust-fts"))]
1031mod tests {
1032 use super::{strip_windows_verbatim_prefix, WorkspacePersistentIndex, MANIFEST_FILE};
1033 use crate::workspace::retrieval::{
1034 ChunkCatalogLimits, ChunkingConfig, LexicalSearchRequest, WorkspaceChunkCatalog,
1035 WorkspaceLexicalEngine,
1036 };
1037 use crate::workspace::WorkspacePath;
1038 use std::path::PathBuf;
1039
1040 #[test]
1041 fn strip_windows_verbatim_prefix_removes_extended_length_form() {
1042 let stripped = strip_windows_verbatim_prefix(PathBuf::from(
1043 r"\\?\C:\Users\runneradmin\AppData\Local\Temp\.tmpIndex\.a3s-code\index",
1044 ));
1045 #[cfg(windows)]
1046 assert_eq!(
1047 stripped,
1048 PathBuf::from(r"C:\Users\runneradmin\AppData\Local\Temp\.tmpIndex\.a3s-code\index")
1049 );
1050 #[cfg(not(windows))]
1051 assert_eq!(
1052 stripped,
1053 PathBuf::from(r"\\?\C:\Users\runneradmin\AppData\Local\Temp\.tmpIndex\.a3s-code\index")
1054 );
1055 }
1056
1057 #[cfg(feature = "zvec-rust-fts")]
1058 #[test]
1059 fn persistent_index_opens_under_windows_verbatim_prefix() {
1060 let directory = tempfile::tempdir().expect("temporary index directory");
1061 let root = directory.path().join(".a3s-code").join("index");
1062 std::fs::create_dir_all(&root).expect("index root");
1063 let open_root = {
1064 #[cfg(windows)]
1065 {
1066 let canonical = std::fs::canonicalize(&root).expect("canonicalize index root");
1067 if canonical.to_string_lossy().starts_with(r"\\?\") {
1068 canonical
1069 } else {
1070 PathBuf::from(format!(r"\\?\{}", canonical.display()))
1071 }
1072 }
1073 #[cfg(not(windows))]
1074 {
1075 root.clone()
1076 }
1077 };
1078 let catalog = WorkspaceChunkCatalog::new_with_engine(
1079 ChunkingConfig::default(),
1080 ChunkCatalogLimits::default(),
1081 WorkspaceLexicalEngine::ZvecRust,
1082 )
1083 .expect("catalog");
1084 catalog
1085 .replace_file(
1086 &WorkspacePath::from_normalized("src/lib.rs"),
1087 Some("rust"),
1088 1,
1089 "verbatim path persistent sentinel\n",
1090 )
1091 .expect("catalog replacement");
1092 let index = WorkspacePersistentIndex::open(open_root, WorkspaceLexicalEngine::ZvecRust)
1093 .expect("persistent index under verbatim prefix");
1094 index
1095 .sync_snapshot(&catalog.snapshot().expect("snapshot"))
1096 .expect("generation write under verbatim prefix");
1097 assert!(index.is_ready());
1098 assert!(
1099 !index.root().to_string_lossy().starts_with(r"\\?\"),
1100 "persistent index root should strip the Windows verbatim prefix"
1101 );
1102 }
1103
1104 #[test]
1105 fn persistent_generation_survives_reopen_and_replaces_removed_content() {
1106 let directory = tempfile::tempdir().expect("temporary index directory");
1107 let catalog = WorkspaceChunkCatalog::new_with_engine(
1108 ChunkingConfig::default(),
1109 ChunkCatalogLimits::default(),
1110 WorkspaceLexicalEngine::ZvecRust,
1111 )
1112 .expect("catalog");
1113 catalog
1114 .replace_file(
1115 &WorkspacePath::from_normalized("src/lib.rs"),
1116 Some("rust"),
1117 1,
1118 "persistent workspace search sentinel\n",
1119 )
1120 .expect("catalog replacement");
1121
1122 let index =
1123 WorkspacePersistentIndex::open(directory.path(), WorkspaceLexicalEngine::ZvecRust)
1124 .expect("persistent index");
1125 index
1126 .sync_snapshot(&catalog.snapshot().expect("snapshot"))
1127 .expect("generation write");
1128 assert!(index.is_ready());
1129
1130 let request = LexicalSearchRequest::new("workspace sentinel");
1131 let result = index.search(&request).expect("persistent query");
1132 assert_eq!(result.hits.len(), 1);
1133 assert_eq!(result.hits[0].chunk.path.as_ref(), "src/lib.rs");
1134 drop(index);
1135
1136 let reopened =
1137 WorkspacePersistentIndex::open(directory.path(), WorkspaceLexicalEngine::ZvecRust)
1138 .expect("reopen persistent index");
1139 let result = reopened.search(&request).expect("reopened query");
1140 assert_eq!(result.hits.len(), 1);
1141 assert_eq!(reopened.status().indexed_chunks, 1);
1142
1143 catalog
1144 .remove_file(&WorkspacePath::from_normalized("src/lib.rs"), 2)
1145 .expect("catalog removal");
1146 reopened
1147 .sync_snapshot(&catalog.snapshot().expect("empty snapshot"))
1148 .expect("replacement generation");
1149 assert!(reopened
1150 .search(&request)
1151 .expect("empty query")
1152 .hits
1153 .is_empty());
1154 reopened.drop_index().expect("drop persistent index");
1155 assert_eq!(
1156 reopened.status().phase,
1157 super::WorkspacePersistentIndexPhase::Absent
1158 );
1159 }
1160
1161 #[test]
1162 fn metadata_only_revision_reuses_the_published_generation() {
1163 let directory = tempfile::tempdir().expect("temporary index directory");
1164 let catalog = WorkspaceChunkCatalog::new_with_engine(
1165 ChunkingConfig::default(),
1166 ChunkCatalogLimits::default(),
1167 WorkspaceLexicalEngine::ZvecRust,
1168 )
1169 .expect("catalog");
1170 let path = WorkspacePath::from_normalized("src/lib.rs");
1171 catalog
1172 .replace_file(&path, Some("rust"), 1, "stable content sentinel\n")
1173 .expect("first replacement");
1174 let index =
1175 WorkspacePersistentIndex::open(directory.path(), WorkspaceLexicalEngine::ZvecRust)
1176 .expect("persistent index");
1177 index
1178 .sync_snapshot(&catalog.snapshot().expect("first snapshot"))
1179 .expect("first generation");
1180 let first = index.status();
1181
1182 catalog
1186 .replace_file(&path, Some("rust"), 2, "stable content sentinel\n")
1187 .expect("same-content replacement");
1188 index
1189 .sync_snapshot(&catalog.snapshot().expect("second snapshot"))
1190 .expect("metadata-only update");
1191 let second = index.status();
1192 assert_eq!(second.generation, first.generation);
1193 assert_eq!(second.source_revision, 2);
1194 assert_eq!(second.catalog_revision, first.catalog_revision + 1);
1195 assert_eq!(second.indexed_chunks, first.indexed_chunks);
1196 assert_eq!(
1197 index
1198 .search(&LexicalSearchRequest::new("stable sentinel"))
1199 .expect("reused query")
1200 .hits
1201 .len(),
1202 1
1203 );
1204 }
1205
1206 #[test]
1207 fn published_replacement_collects_old_generations() {
1208 let directory = tempfile::tempdir().expect("temporary index directory");
1209 let catalog = WorkspaceChunkCatalog::new_with_engine(
1210 ChunkingConfig::default(),
1211 ChunkCatalogLimits::default(),
1212 WorkspaceLexicalEngine::ZvecRust,
1213 )
1214 .expect("catalog");
1215 let path = WorkspacePath::from_normalized("src/lib.rs");
1216 let index =
1217 WorkspacePersistentIndex::open(directory.path(), WorkspaceLexicalEngine::ZvecRust)
1218 .expect("persistent index");
1219
1220 catalog
1221 .replace_file(&path, Some("rust"), 1, "generation one sentinel\n")
1222 .expect("first replacement");
1223 index
1224 .sync_snapshot(&catalog.snapshot().expect("first snapshot"))
1225 .expect("first generation");
1226 let first_generation = index.status().generation.expect("first generation name");
1227
1228 catalog
1229 .replace_file(&path, Some("rust"), 2, "generation two sentinel\n")
1230 .expect("second replacement");
1231 index
1232 .sync_snapshot(&catalog.snapshot().expect("second snapshot"))
1233 .expect("second generation");
1234 let second_generation = index.status().generation.expect("second generation name");
1235 assert_ne!(first_generation, second_generation);
1236
1237 let generations = std::fs::read_dir(directory.path())
1238 .expect("persistent index directory")
1239 .filter_map(Result::ok)
1240 .filter(|entry| {
1241 entry
1242 .file_name()
1243 .to_str()
1244 .is_some_and(|name| name.starts_with("generation-"))
1245 })
1246 .map(|entry| entry.file_name())
1247 .collect::<Vec<_>>();
1248 assert_eq!(
1249 generations,
1250 vec![std::ffi::OsString::from(&second_generation)]
1251 );
1252 assert_eq!(
1253 std::fs::read_to_string(directory.path().join("CURRENT")).expect("CURRENT"),
1254 format!("{}\n", second_generation)
1255 );
1256 }
1257
1258 #[test]
1259 fn corrupted_generation_manifest_is_rejected_before_query() {
1260 let directory = tempfile::tempdir().expect("temporary index directory");
1261 let catalog = WorkspaceChunkCatalog::new_with_engine(
1262 ChunkingConfig::default(),
1263 ChunkCatalogLimits::default(),
1264 WorkspaceLexicalEngine::ZvecRust,
1265 )
1266 .expect("catalog");
1267 catalog
1268 .replace_file(
1269 &WorkspacePath::from_normalized("src/lib.rs"),
1270 Some("rust"),
1271 1,
1272 "durable manifest integrity sentinel\n",
1273 )
1274 .expect("catalog replacement");
1275
1276 let index =
1277 WorkspacePersistentIndex::open(directory.path(), WorkspaceLexicalEngine::ZvecRust)
1278 .expect("persistent index");
1279 let snapshot = catalog.snapshot().expect("snapshot");
1280 index.sync_snapshot(&snapshot).expect("generation write");
1281 let generation = index.status().generation.expect("generation");
1282 drop(index);
1283
1284 let manifest_path = directory.path().join(generation).join(MANIFEST_FILE);
1285 let bytes = std::fs::read(&manifest_path).expect("manifest");
1286 let mut manifest: serde_json::Value = serde_json::from_slice(&bytes).expect("json");
1287 manifest["chunks"][0]["text"] = serde_json::Value::String("tampered\n".to_owned());
1288 std::fs::write(
1289 &manifest_path,
1290 serde_json::to_vec(&manifest).expect("manifest json"),
1291 )
1292 .expect("tampered manifest");
1293
1294 let reopened =
1295 WorkspacePersistentIndex::open(directory.path(), WorkspaceLexicalEngine::ZvecRust)
1296 .expect("reopen should remain recoverable");
1297 assert!(!reopened.is_ready());
1298 assert_eq!(
1299 reopened.status().phase,
1300 super::WorkspacePersistentIndexPhase::Absent
1301 );
1302
1303 reopened
1304 .sync_snapshot(&snapshot)
1305 .expect("catalog snapshot rebuilds corrupted generation");
1306 let result = reopened
1307 .search(&LexicalSearchRequest::new("manifest integrity sentinel"))
1308 .expect("rebuilt query");
1309 assert_eq!(result.hits.len(), 1);
1310 assert_eq!(
1311 result.hits[0].chunk.text.as_ref(),
1312 "durable manifest integrity sentinel\n"
1313 );
1314 }
1315
1316 #[test]
1317 fn status_reports_building_without_hiding_the_last_published_generation() {
1318 let directory = tempfile::tempdir().expect("temporary index directory");
1319 let catalog = WorkspaceChunkCatalog::new_with_engine(
1320 ChunkingConfig::default(),
1321 ChunkCatalogLimits::default(),
1322 WorkspaceLexicalEngine::ZvecRust,
1323 )
1324 .expect("catalog");
1325 catalog
1326 .replace_file(
1327 &WorkspacePath::from_normalized("src/lib.rs"),
1328 Some("rust"),
1329 1,
1330 "published status sentinel\n",
1331 )
1332 .expect("catalog replacement");
1333 let index =
1334 WorkspacePersistentIndex::open(directory.path(), WorkspaceLexicalEngine::ZvecRust)
1335 .expect("persistent index");
1336 index
1337 .sync_snapshot(&catalog.snapshot().expect("snapshot"))
1338 .expect("generation");
1339 let ready = index.status();
1340 index
1341 .building
1342 .store(true, std::sync::atomic::Ordering::Release);
1343 let building = index.status();
1344 assert_eq!(
1345 building.phase,
1346 super::WorkspacePersistentIndexPhase::Building
1347 );
1348 assert_eq!(building.generation, ready.generation);
1349 assert_eq!(building.indexed_chunks, ready.indexed_chunks);
1350 index
1351 .building
1352 .store(false, std::sync::atomic::Ordering::Release);
1353 }
1354}