Skip to main content

arete_server/
projector.rs

1use crate::bus::{BusManager, BusMessage};
2use crate::cache::EntityCache;
3use crate::mutation_batch::{MutationBatch, SlotContext};
4use crate::view::{ViewIndex, ViewSpec};
5use crate::websocket::frame::{apply_wire_format, Mode, SourceFrame};
6use arete_interpreter::CanonicalLog;
7use bytes::Bytes;
8use serde_json::Value;
9use smallvec::SmallVec;
10use std::sync::Arc;
11use tokio::sync::mpsc;
12use tracing::{debug, error, instrument};
13
14#[cfg(feature = "otel")]
15use crate::metrics::Metrics;
16
17pub struct Projector {
18    view_index: Arc<ViewIndex>,
19    bus_manager: BusManager,
20    entity_cache: EntityCache,
21    mutations_rx: mpsc::Receiver<MutationBatch>,
22    snapshot_runtime: Option<crate::snapshot::SnapshotRuntime>,
23    #[cfg(feature = "otel")]
24    metrics: Option<Arc<Metrics>>,
25}
26
27impl Projector {
28    #[cfg(feature = "otel")]
29    pub fn new(
30        view_index: Arc<ViewIndex>,
31        bus_manager: BusManager,
32        entity_cache: EntityCache,
33        mutations_rx: mpsc::Receiver<MutationBatch>,
34        metrics: Option<Arc<Metrics>>,
35    ) -> Self {
36        Self {
37            view_index,
38            bus_manager,
39            entity_cache,
40            mutations_rx,
41            snapshot_runtime: None,
42            metrics,
43        }
44    }
45
46    #[cfg(not(feature = "otel"))]
47    pub fn new(
48        view_index: Arc<ViewIndex>,
49        bus_manager: BusManager,
50        entity_cache: EntityCache,
51        mutations_rx: mpsc::Receiver<MutationBatch>,
52    ) -> Self {
53        Self {
54            view_index,
55            bus_manager,
56            entity_cache,
57            mutations_rx,
58            snapshot_runtime: None,
59        }
60    }
61
62    /// Associate projection progress with one server's snapshot lifecycle.
63    pub fn with_snapshot_runtime(
64        mut self,
65        snapshot_runtime: crate::snapshot::SnapshotRuntime,
66    ) -> Self {
67        self.snapshot_runtime = Some(snapshot_runtime);
68        self
69    }
70
71    pub async fn run(mut self) {
72        debug!("Projector started");
73
74        let mut json_buffer = Vec::with_capacity(4096);
75
76        while let Some(mut batch) = self.mutations_rx.recv().await {
77            let _span_guard = batch.span.enter();
78
79            let mut log = CanonicalLog::new();
80            log.set("phase", "projector");
81
82            let batch_size = batch.len();
83            let slot_context = batch.slot_context;
84            let mut frames_published = 0u32;
85            let mut errors = 0u32;
86
87            if let Some(ctx) = batch.event_context.as_ref() {
88                log.set("program", &ctx.program)
89                    .set("event_kind", &ctx.event_kind)
90                    .set("event_type", &ctx.event_type)
91                    .set("account", &ctx.account)
92                    .set("accounts_count", ctx.accounts_count);
93            }
94
95            for mutation in std::mem::take(&mut batch.mutations).into_iter() {
96                #[cfg(feature = "otel")]
97                let export = mutation.export.clone();
98
99                match self
100                    .process_mutation(mutation, slot_context, &mut json_buffer)
101                    .await
102                {
103                    Ok(count) => frames_published += count,
104                    Err(e) => {
105                        error!("Failed to process mutation: {}", e);
106                        errors += 1;
107                    }
108                }
109
110                #[cfg(feature = "otel")]
111                if let Some(ref metrics) = self.metrics {
112                    metrics.record_mutation_processed(&export);
113                }
114            }
115
116            // The batch is now applied to the caches: advance the snapshot
117            // resume watermark, then release the processing guard transferred
118            // by the VM producer. An exclusive snapshot cut cannot begin until
119            // every earlier guarded batch reaches this point.
120            if batch_size > 0 {
121                if let Some(snapshot_runtime) = &self.snapshot_runtime {
122                    snapshot_runtime.record_applied_batch(slot_context.map(|ctx| ctx.slot));
123                }
124            }
125            drop(batch.snapshot_guard.take());
126
127            // Flush markers remain useful to non-snapshot callers that need
128            // to observe a drained projector queue.
129            if let Some(ack) = batch.flush_ack.take() {
130                let _ = ack.send(());
131            }
132
133            log.set("batch_size", batch_size)
134                .set("frames_published", frames_published)
135                .set("errors", errors);
136
137            #[cfg(feature = "otel")]
138            if let Some(ref metrics) = self.metrics {
139                metrics.record_projector_latency(log.duration_ms());
140            }
141
142            log.emit();
143        }
144
145        debug!("Projector stopped");
146    }
147
148    #[instrument(
149        name = "projector.mutation",
150        skip(self, mutation, slot_context, json_buffer),
151        fields(export = %mutation.export)
152    )]
153    async fn process_mutation(
154        &self,
155        mutation: arete_interpreter::Mutation,
156        slot_context: Option<SlotContext>,
157        json_buffer: &mut Vec<u8>,
158    ) -> anyhow::Result<u32> {
159        let specs = self.view_index.by_export(&mutation.export);
160
161        if specs.is_empty() {
162            return Ok(0);
163        }
164
165        let key = Self::extract_key(&mutation.key);
166        let arete_interpreter::Mutation {
167            mut patch, append, ..
168        } = mutation;
169
170        // Inject _seq for recency sorting if slot context is available
171        if let Some(ctx) = slot_context {
172            if let Value::Object(ref mut map) = patch {
173                map.insert("_seq".to_string(), Value::String(ctx.to_seq_string()));
174            }
175        }
176
177        let matching_specs: SmallVec<[&ViewSpec; 4]> = specs
178            .iter()
179            .filter(|spec| spec.filters.matches(&key))
180            .collect();
181
182        let match_count = matching_specs.len();
183        if match_count == 0 {
184            return Ok(0);
185        }
186
187        let mut frames_published = 0u32;
188
189        for (i, spec) in matching_specs.into_iter().enumerate() {
190            let is_last = i == match_count - 1;
191            let patch_data = if is_last {
192                std::mem::take(&mut patch)
193            } else {
194                patch.clone()
195            };
196
197            let projected = spec.projection.apply(patch_data);
198            let mut wire_data = projected.clone();
199            apply_wire_format(&mut wire_data, &spec.wire_format);
200
201            // Extract _seq from the patch data to include in the frame
202            let seq = slot_context.map(|ctx| ctx.to_seq_string());
203
204            let frame = SourceFrame {
205                mode: spec.mode,
206                export: spec.id.clone(),
207                op: "patch",
208                key: key.clone(),
209                data: wire_data,
210                append: append.clone(),
211                seq,
212            };
213
214            json_buffer.clear();
215            serde_json::to_writer(&mut *json_buffer, &frame)?;
216            let payload = Arc::new(Bytes::copy_from_slice(json_buffer));
217
218            self.entity_cache
219                .upsert_with_append(&spec.id, &key, projected, &frame.append)
220                .await;
221
222            if spec.mode == Mode::List {
223                self.update_derived_view_caches(&spec.id, &key).await;
224            }
225
226            let message = Arc::new(BusMessage {
227                key: key.clone(),
228                entity: spec.id.clone(),
229                payload,
230            });
231
232            self.publish_frame(spec, message).await;
233            frames_published += 1;
234
235            #[cfg(feature = "otel")]
236            if let Some(ref metrics) = self.metrics {
237                let mode_str = match spec.mode {
238                    Mode::List => "list",
239                    Mode::State => "state",
240                    Mode::Append => "append",
241                };
242                metrics.record_frame_published(mode_str, &spec.export);
243            }
244        }
245
246        Ok(frames_published)
247    }
248
249    fn extract_key(key: &serde_json::Value) -> String {
250        key.as_str()
251            .map(|s| s.to_string())
252            .or_else(|| key.as_u64().map(|n| n.to_string()))
253            .or_else(|| key.as_i64().map(|n| n.to_string()))
254            .or_else(|| {
255                key.as_array().and_then(|arr| {
256                    let bytes: Vec<u8> = arr
257                        .iter()
258                        .filter_map(|v| v.as_u64().map(|n| n as u8))
259                        .collect();
260                    if bytes.len() == arr.len() {
261                        Some(hex::encode(&bytes))
262                    } else {
263                        None
264                    }
265                })
266            })
267            .unwrap_or_else(|| key.to_string())
268    }
269
270    async fn update_derived_view_caches(&self, source_view_id: &str, entity_key: &str) {
271        let derived_views = self.view_index.get_derived_views_for_source(source_view_id);
272        if derived_views.is_empty() {
273            return;
274        }
275
276        let entity_data = match self.entity_cache.get(source_view_id, entity_key).await {
277            Some(data) => data,
278            None => return,
279        };
280
281        let sorted_caches = self.view_index.sorted_caches();
282        let mut caches = sorted_caches.write().await;
283
284        for derived_spec in derived_views {
285            if let Some(cache) = caches.get_mut(&derived_spec.id) {
286                cache.upsert(entity_key.to_string(), entity_data.clone());
287                debug!(
288                    "Updated sorted cache for derived view {} with key {}",
289                    derived_spec.id, entity_key
290                );
291            }
292        }
293    }
294
295    #[instrument(
296        name = "projector.publish",
297        skip(self, spec, message),
298        fields(view_id = %spec.id, mode = ?spec.mode)
299    )]
300    async fn publish_frame(&self, spec: &ViewSpec, message: Arc<BusMessage>) {
301        match spec.mode {
302            Mode::State => {
303                self.bus_manager
304                    .publish_state(&spec.id, &message.key, message.payload.clone())
305                    .await;
306            }
307            Mode::List | Mode::Append => {
308                self.bus_manager.publish_list(&spec.id, message).await;
309            }
310        }
311    }
312}