1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3use std::time::{Duration, Instant};
4
5use unicode_normalization::UnicodeNormalization;
6
7use crate::lsp::diagnostics::{DiagnosticSeverity, StoredDiagnostic};
8use crate::lsp::roots::ServerKey;
9
10pub const ALERT_SESSION_IDLE_TTL: Duration = Duration::from_secs(30 * 60);
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
19pub struct ProducerKey(String);
20
21impl ProducerKey {
22 pub fn new(value: impl Into<String>) -> Self {
23 Self(value.into())
24 }
25
26 pub fn from_server_key(server: &ServerKey) -> Self {
27 Self(format!(
28 "{}@{}",
29 server.kind.id_str(),
30 canonicalize_for_alert(&server.root).display()
31 ))
32 }
33
34 pub fn as_str(&self) -> &str {
35 &self.0
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
45pub struct DiagnosticIdentity {
46 pub file: PathBuf,
47 pub line: u32,
48 pub column: u32,
49 pub end_line: u32,
50 pub end_column: u32,
51 pub severity: String,
52 pub source: Option<String>,
53 pub code: Option<String>,
54 pub message: String,
55}
56
57impl DiagnosticIdentity {
58 pub fn from_stored(canonical_root: &Path, diagnostic: &StoredDiagnostic) -> Option<Self> {
62 (diagnostic.severity == DiagnosticSeverity::Error).then(|| Self {
63 file: canonical_root_relative_file(canonical_root, &diagnostic.file),
64 line: diagnostic.line,
65 column: diagnostic.column,
66 end_line: diagnostic.end_line,
67 end_column: diagnostic.end_column,
68 severity: diagnostic.severity.as_str().to_owned(),
69 source: diagnostic.source.clone(),
70 code: diagnostic.code.clone(),
71 message: normalize_diagnostic_message(&diagnostic.message),
72 })
73 }
74}
75
76pub fn normalize_diagnostic_message(message: &str) -> String {
82 message
83 .lines()
84 .next()
85 .unwrap_or_default()
86 .split_whitespace()
87 .collect::<Vec<_>>()
88 .join(" ")
89 .nfc()
90 .collect()
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct AcceptedDiagnosticSnapshot {
98 pub server_key: ServerKey,
99 pub document_version: i32,
100 pub diagnostics: Vec<StoredDiagnostic>,
101}
102
103impl AcceptedDiagnosticSnapshot {
104 pub fn new(
105 server_key: ServerKey,
106 document_version: i32,
107 diagnostics: Vec<StoredDiagnostic>,
108 ) -> Self {
109 Self {
110 server_key,
111 document_version,
112 diagnostics,
113 }
114 }
115
116 pub fn producer_key(&self) -> ProducerKey {
117 ProducerKey::from_server_key(&self.server_key)
118 }
119
120 pub fn is_empty(&self) -> bool {
121 self.diagnostics.is_empty()
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct AcceptedObservation {
130 pub session_id: String,
131 pub canonical_root: PathBuf,
132 pub producer_key: ProducerKey,
133 pub accepted_document_version: i32,
134 pub diagnostics: Vec<StoredDiagnostic>,
135}
136
137impl AcceptedObservation {
138 pub fn new(
139 session_id: impl Into<String>,
140 canonical_root: impl AsRef<Path>,
141 producer_key: ProducerKey,
142 accepted_document_version: i32,
143 diagnostics: Vec<StoredDiagnostic>,
144 ) -> Self {
145 Self {
146 session_id: session_id.into(),
147 canonical_root: canonicalize_for_alert(canonical_root.as_ref()),
148 producer_key,
149 accepted_document_version,
150 diagnostics,
151 }
152 }
153
154 pub fn from_snapshot(
155 session_id: impl Into<String>,
156 canonical_root: impl AsRef<Path>,
157 snapshot: AcceptedDiagnosticSnapshot,
158 ) -> Self {
159 Self::new(
160 session_id,
161 canonical_root,
162 snapshot.producer_key(),
163 snapshot.document_version,
164 snapshot.diagnostics,
165 )
166 }
167
168 pub fn partition_key(&self) -> AlertPartitionKey {
169 AlertPartitionKey {
170 session_id: self.session_id.clone(),
171 canonical_root: self.canonical_root.clone(),
172 producer_key: self.producer_key.clone(),
173 }
174 }
175
176 pub fn is_empty(&self) -> bool {
177 self.diagnostics.is_empty()
178 }
179}
180
181#[derive(Debug, Clone, Default, PartialEq, Eq)]
185pub struct AcceptedObservationBatch {
186 observations: Vec<AcceptedObservation>,
187}
188
189impl AcceptedObservationBatch {
190 pub fn from_diagnostic_snapshots(
194 session_id: impl Into<String>,
195 canonical_root: impl AsRef<Path>,
196 snapshots: impl IntoIterator<Item = AcceptedDiagnosticSnapshot>,
197 ) -> Result<Self, ObservationError> {
198 let session_id = session_id.into();
199 let canonical_root = canonicalize_for_alert(canonical_root.as_ref());
200 Self::new(
201 snapshots
202 .into_iter()
203 .map(|snapshot| {
204 AcceptedObservation::from_snapshot(
205 session_id.clone(),
206 canonical_root.clone(),
207 snapshot,
208 )
209 })
210 .collect(),
211 )
212 }
213
214 pub fn new(observations: Vec<AcceptedObservation>) -> Result<Self, ObservationError> {
215 let mut partitions = HashSet::new();
216 for observation in &observations {
217 let key = observation.partition_key();
218 if !partitions.insert(key.clone()) {
219 return Err(ObservationError::DuplicatePartition(key));
220 }
221 }
222 Ok(Self { observations })
223 }
224
225 pub fn observations(&self) -> &[AcceptedObservation] {
226 &self.observations
227 }
228
229 pub fn is_empty(&self) -> bool {
230 self.observations.is_empty()
231 }
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, Hash)]
235pub struct AlertPartitionKey {
236 pub session_id: String,
237 pub canonical_root: PathBuf,
238 pub producer_key: ProducerKey,
239}
240
241impl AlertPartitionKey {
242 pub fn new(
243 session_id: impl Into<String>,
244 canonical_root: impl AsRef<Path>,
245 producer_key: ProducerKey,
246 ) -> Self {
247 Self {
248 session_id: session_id.into(),
249 canonical_root: canonicalize_for_alert(canonical_root.as_ref()),
250 producer_key,
251 }
252 }
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
256pub struct LifecycleEpisodeId(u64);
257
258impl LifecycleEpisodeId {
259 pub fn get(self) -> u64 {
260 self.0
261 }
262}
263
264#[derive(Debug, Clone, Default, PartialEq, Eq)]
268pub struct AlertPartitionState {
269 pub baseline_established: bool,
270 pub live: HashMap<DiagnosticIdentity, LifecycleEpisodeId>,
271 pub rendered: HashSet<DiagnosticIdentity>,
272 pub closed_episodes: HashSet<LifecycleEpisodeId>,
273}
274
275impl AlertPartitionState {
276 pub fn episode_for(&self, identity: &DiagnosticIdentity) -> Option<LifecycleEpisodeId> {
277 self.live.get(identity).copied()
278 }
279}
280
281#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct ClosedIdentity {
283 pub identity: DiagnosticIdentity,
284 pub episode_id: LifecycleEpisodeId,
285}
286
287#[derive(Debug, Clone, PartialEq, Eq)]
288pub struct EnteredIdentity {
289 pub identity: DiagnosticIdentity,
290 pub episode_id: LifecycleEpisodeId,
291}
292
293#[derive(Debug, Clone, PartialEq, Eq)]
297pub struct AcceptedObservationResult {
298 pub partition: AlertPartitionKey,
299 pub accepted_document_version: i32,
300 pub accepted_empty_snapshot: bool,
301 pub baseline_established_now: bool,
302 pub baselined: Vec<EnteredIdentity>,
303 pub entered: Vec<EnteredIdentity>,
304 pub closed: Vec<ClosedIdentity>,
305}
306
307#[derive(Debug, Clone, PartialEq, Eq)]
308pub enum ObservationError {
309 DuplicatePartition(AlertPartitionKey),
310}
311
312impl std::fmt::Display for ObservationError {
313 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314 match self {
315 Self::DuplicatePartition(key) => write!(
316 formatter,
317 "accepted observation batch contains duplicate partition ({}, {}, {})",
318 key.session_id,
319 key.canonical_root.display(),
320 key.producer_key.as_str()
321 ),
322 }
323 }
324}
325
326impl std::error::Error for ObservationError {}
327
328#[derive(Debug, Clone, Default)]
332pub struct AlertDeltaState {
333 partitions: HashMap<AlertPartitionKey, AlertPartitionState>,
334 session_last_touched: HashMap<String, Instant>,
335 next_episode_id: u64,
336}
337
338impl AlertDeltaState {
339 pub fn accept_batch(
340 &mut self,
341 batch: &AcceptedObservationBatch,
342 ) -> Result<Vec<AcceptedObservationResult>, ObservationError> {
343 self.accept_batch_at(batch, Instant::now())
344 }
345
346 pub fn accept_batch_at(
350 &mut self,
351 batch: &AcceptedObservationBatch,
352 now: Instant,
353 ) -> Result<Vec<AcceptedObservationResult>, ObservationError> {
354 validate_batch(batch)?;
358
359 let mut staged = self.clone();
360 let mut results = Vec::with_capacity(batch.observations.len());
361 for observation in &batch.observations {
362 results.push(staged.accept_observation(observation, now));
363 }
364 *self = staged;
365 Ok(results)
366 }
367
368 pub fn partition(&self, key: &AlertPartitionKey) -> Option<&AlertPartitionState> {
369 self.partitions.get(key)
370 }
371
372 pub fn partitions_for_session<'a>(
373 &'a self,
374 session_id: &'a str,
375 ) -> impl Iterator<Item = (&'a AlertPartitionKey, &'a AlertPartitionState)> + 'a {
376 self.partitions
377 .iter()
378 .filter(move |(key, _)| key.session_id == session_id)
379 }
380
381 pub fn remove_session(&mut self, session_id: &str) -> bool {
382 let before = self.partitions.len();
383 self.partitions
384 .retain(|key, _| key.session_id != session_id);
385 let touched = self.session_last_touched.remove(session_id).is_some();
386 touched || self.partitions.len() != before
387 }
388
389 pub fn reap_idle_sessions_at(&mut self, now: Instant, idle_for: Duration) -> Vec<String> {
393 let mut reaped = self
394 .session_last_touched
395 .iter()
396 .filter_map(|(session_id, last_touched)| {
397 (now.saturating_duration_since(*last_touched) >= idle_for)
398 .then(|| session_id.clone())
399 })
400 .collect::<Vec<_>>();
401 reaped.sort();
402 for session_id in &reaped {
403 self.remove_session(session_id);
404 }
405 reaped
406 }
407
408 pub fn reap_idle_sessions(&mut self) -> Vec<String> {
409 self.reap_idle_sessions_at(Instant::now(), ALERT_SESSION_IDLE_TTL)
410 }
411
412 fn accept_observation(
413 &mut self,
414 observation: &AcceptedObservation,
415 now: Instant,
416 ) -> AcceptedObservationResult {
417 let partition_key = observation.partition_key();
418 self.session_last_touched
419 .insert(observation.session_id.clone(), now);
420
421 let current = observation
422 .diagnostics
423 .iter()
424 .filter_map(|diagnostic| {
425 DiagnosticIdentity::from_stored(&observation.canonical_root, diagnostic)
426 })
427 .collect::<HashSet<_>>();
428 let baseline_established = self
429 .partitions
430 .get(&partition_key)
431 .is_some_and(|partition| partition.baseline_established);
432
433 if !baseline_established {
434 let mut baselined = current
435 .into_iter()
436 .map(|identity| EnteredIdentity {
437 episode_id: self.mint_episode(),
438 identity,
439 })
440 .collect::<Vec<_>>();
441 baselined.sort_by(|left, right| left.identity.cmp(&right.identity));
442 let partition = self.partitions.entry(partition_key.clone()).or_default();
443 for entered in &baselined {
444 partition
445 .live
446 .insert(entered.identity.clone(), entered.episode_id);
447 partition.rendered.insert(entered.identity.clone());
448 }
449 partition.baseline_established = true;
450 return AcceptedObservationResult {
451 partition: partition_key,
452 accepted_document_version: observation.accepted_document_version,
453 accepted_empty_snapshot: observation.is_empty(),
454 baseline_established_now: true,
455 baselined,
456 entered: Vec::new(),
457 closed: Vec::new(),
458 };
459 }
460
461 let previous = self
462 .partitions
463 .get(&partition_key)
464 .map(|partition| partition.live.keys().cloned().collect::<HashSet<_>>())
465 .unwrap_or_default();
466 let closed_identities = previous.difference(¤t).cloned().collect::<Vec<_>>();
467 let mut entered = current
468 .difference(&previous)
469 .cloned()
470 .map(|identity| EnteredIdentity {
471 episode_id: self.mint_episode(),
472 identity,
473 })
474 .collect::<Vec<_>>();
475 entered.sort_by(|left, right| left.identity.cmp(&right.identity));
476
477 let partition = self.partitions.entry(partition_key.clone()).or_default();
478 let mut closed = closed_identities
479 .into_iter()
480 .filter_map(|identity| {
481 partition.live.remove(&identity).map(|episode_id| {
482 partition.rendered.remove(&identity);
483 partition.closed_episodes.insert(episode_id);
484 ClosedIdentity {
485 identity,
486 episode_id,
487 }
488 })
489 })
490 .collect::<Vec<_>>();
491 closed.sort_by(|left, right| left.identity.cmp(&right.identity));
492 for item in &entered {
493 partition
494 .live
495 .insert(item.identity.clone(), item.episode_id);
496 }
497
498 AcceptedObservationResult {
499 partition: partition_key,
500 accepted_document_version: observation.accepted_document_version,
501 accepted_empty_snapshot: observation.is_empty(),
502 baseline_established_now: false,
503 baselined: Vec::new(),
504 entered,
505 closed,
506 }
507 }
508
509 fn mint_episode(&mut self) -> LifecycleEpisodeId {
510 self.next_episode_id = self.next_episode_id.wrapping_add(1).max(1);
511 LifecycleEpisodeId(self.next_episode_id)
512 }
513}
514
515fn validate_batch(batch: &AcceptedObservationBatch) -> Result<(), ObservationError> {
516 let mut partitions = HashSet::new();
517 for observation in &batch.observations {
518 let key = observation.partition_key();
519 if !partitions.insert(key.clone()) {
520 return Err(ObservationError::DuplicatePartition(key));
521 }
522 }
523 Ok(())
524}
525
526fn canonical_root_relative_file(canonical_root: &Path, file: &Path) -> PathBuf {
527 let root = canonicalize_for_alert(canonical_root);
528 let file = canonicalize_for_alert(file);
529 file.strip_prefix(&root).unwrap_or(&file).to_path_buf()
530}
531
532fn canonicalize_for_alert(path: &Path) -> PathBuf {
533 std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
534}