1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
//! Keep embedding a memory store that a failed backfill left at zero (#1069).
//!
//! Embedding ran in exactly two places: once at startup, inside `reindex`, and
//! on the write path for the one document being written. Neither retries. So a
//! backfill that failed for a transient reason (a bad key, an endpoint that was
//! down, a rate limit) left every document unembedded until someone restarted
//! the process, and nothing said so. Search degraded to keyword-only FTS and
//! kept answering, which is why an install ran 94 days with one vectorised
//! chunk out of 589 and nobody noticed.
//!
//! This is the retry under that. A timer, not a queue: the store already knows
//! what needs embedding (`get_hashes_needing_embedding`), so there is no state
//! to keep and nothing to lose across a restart.
//!
//! ## Why config is re-read every tick
//!
//! `read_memory_config` parses config.toml on each call, so a key fixed
//! mid-session is picked up on the next tick with no cache to invalidate and no
//! reload plumbing. That is also why the interval is resolved per tick rather
//! than captured once: changing it in config.toml takes effect within one
//! period instead of at the next restart.
//!
//! ## Single flight
//!
//! A tick that lands while the previous one is still embedding skips instead of
//! stacking. Non-negotiable on the local path: `freshness.rs` documents that
//! llama-cpp GGML can segfault under contention, and a sweep is precisely a
//! second embedder entering alongside the first.
use Mutex;
use ;
use Duration;
use Store;
/// Set while a sweep is embedding, so the next tick skips rather than running
/// a second embedder alongside it.
static SWEEPING: AtomicBool = new;
/// The sweep period for a given config, or `None` when the sweep is off.
///
/// Off means either an explicit `backfill_interval_secs = 0` or
/// `vector_enabled = false`. The second case matters: #1062 established that
/// disabling vectors skips embedding work entirely, local and API alike, and a
/// timer that wakes every five minutes to decide it has nothing to do is still
/// work the user asked not to happen.
///
/// Takes the config rather than reading it so the decision can be tested
/// without a config.toml on disk.
pub
/// The configured sweep period for this install.
/// Start the periodic backfill sweep for this process.
///
/// Call once at boot, after the startup reindex, so the first tick can never
/// race the backfill that reindex already runs.
/// One sweep pass. Public to the crate so the interval logic and the skip
/// behaviour can be exercised without waiting on a timer.
pub async
/// Claim the single-flight slot. `false` means another sweep holds it and this
/// tick must skip rather than embed alongside it.
///
/// Split out from [`run_once`] so the skip can be tested without a store: the
/// alternative is a test that actually embeds, which needs a model or a live
/// endpoint and would prove nothing about the guard.
pub
/// Release the single-flight slot.
pub
async