1use std::collections::BTreeMap;
4use std::fmt;
5use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
6
7use chrono::{DateTime, Utc};
8use turnframe_core::case::CaseKey;
9use turnframe_core::error::ExecutionError;
10use turnframe_core::event::{EventRedaction, OutboxEntry};
11use turnframe_core::ids::{
12 AccountId, CaseRevision, CommandId, ConversationId, EventId, InteractionId, OptionId, OutboxId,
13 RedactionAuthority, TurnId,
14};
15use turnframe_core::interaction::{Interaction, InteractionStatus};
16use turnframe_core::replay::{ReplayRecord, TurnPhase};
17use turnframe_core::response::AssistantTurn;
18use turnframe_store::commit::{CommitBundle, CommitReceipt, CommitStore};
19use turnframe_store::conversation::{
20 ConversationReader, ConversationRecord, ConversationWriter, RecoveryScope, StoredTurn,
21 StoredUserTurn, TurnPhaseMarker,
22};
23use turnframe_store::error::StoreError;
24use turnframe_store::events::{
25 EventBatch, EventCursor, EventJournalReader, EventJournalWriter, EventPage, StoredEvent,
26};
27use turnframe_store::interaction::{
28 InteractionReader, InteractionRecord, InteractionWriter, InvalidationReason, ResolutionOutcome,
29};
30use turnframe_store::journal::{
31 CommandJournalEntry, CommandJournalReader, CommandJournalWriter, JournalAdmission,
32 JournalOutcome,
33};
34use turnframe_store::memory::MemoryStores;
35use turnframe_store::outbox::{OutboxReader, OutboxRecord, OutboxWriter};
36use turnframe_store::replay::{ReplayReader, ReplayWriter};
37
38#[derive(Default)]
43pub struct StoreCallCounter {
44 counts: Mutex<BTreeMap<&'static str, usize>>,
45}
46
47impl StoreCallCounter {
48 #[must_use]
50 pub fn new() -> Self {
51 Self::default()
52 }
53
54 pub fn record(&self, method: &'static str) {
56 *self.lock().entry(method).or_insert(0) += 1;
57 }
58
59 #[must_use]
61 pub fn count(&self, method: &str) -> usize {
62 self.lock().get(method).copied().unwrap_or(0)
63 }
64
65 #[must_use]
67 pub fn counts(&self) -> Vec<(&'static str, usize)> {
68 self.lock()
69 .iter()
70 .map(|(method, count)| (*method, *count))
71 .collect()
72 }
73
74 #[must_use]
76 pub fn total(&self) -> usize {
77 self.lock().values().sum()
78 }
79
80 pub fn reset(&self) {
82 self.lock().clear();
83 }
84
85 fn lock(&self) -> MutexGuard<'_, BTreeMap<&'static str, usize>> {
88 self.counts.lock().unwrap_or_else(PoisonError::into_inner)
89 }
90}
91
92impl fmt::Debug for StoreCallCounter {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 f.debug_struct("StoreCallCounter")
95 .field("total", &self.total())
96 .field("methods", &self.counts())
97 .finish()
98 }
99}
100
101#[derive(Debug)]
106pub struct CountingStores {
107 inner: Arc<MemoryStores>,
108 counter: Arc<StoreCallCounter>,
109}
110
111impl CountingStores {
112 #[must_use]
114 pub fn new(inner: Arc<MemoryStores>, counter: Arc<StoreCallCounter>) -> Self {
115 Self { inner, counter }
116 }
117
118 #[must_use]
120 pub fn inner(&self) -> &Arc<MemoryStores> {
121 &self.inner
122 }
123
124 #[must_use]
126 pub fn counter(&self) -> &Arc<StoreCallCounter> {
127 &self.counter
128 }
129}
130
131macro_rules! counting_impl {
146 (
147 $trait:path, $prefix:literal,
148 $( fn $name:ident ( $( $arg:ident : $ty:ty ),* $(,)? ) -> $ret:ty ; )*
149 ) => {
150 #[async_trait::async_trait]
151 impl $trait for CountingStores {
152 $(
153 async fn $name(&self, $( $arg : $ty ),*) -> $ret {
154 self.counter.record(concat!($prefix, ".", stringify!($name)));
155 <MemoryStores as $trait>::$name(&self.inner, $( $arg ),*).await
156 }
157 )*
158 }
159 };
160}
161
162counting_impl! {
163 ConversationReader, "conversations",
164 fn load_conversation(
165 account: &AccountId,
166 id: &ConversationId,
167 ) -> Result<ConversationRecord, StoreError>;
168 fn load_recent_turns(
169 account: &AccountId,
170 conversation: &ConversationId,
171 limit: usize,
172 ) -> Result<Vec<StoredTurn>, StoreError>;
173 fn load_turn(account: &AccountId, turn_id: &TurnId) -> Result<StoredTurn, StoreError>;
174 fn turn_phase(
175 account: &AccountId,
176 turn_id: &TurnId,
177 ) -> Result<TurnPhaseMarker, StoreError>;
178 fn list_unfinished_turns(
179 scope: RecoveryScope,
180 limit: usize,
181 ) -> Result<Vec<TurnPhaseMarker>, StoreError>;
182}
183
184counting_impl! {
185 ConversationWriter, "conversations",
186 fn create_conversation(record: ConversationRecord) -> Result<(), StoreError>;
187 fn append_user_turn(turn: StoredUserTurn) -> Result<(), StoreError>;
188 fn append_assistant_turn(
189 account: &AccountId,
190 turn: AssistantTurn,
191 ) -> Result<(), StoreError>;
192 fn set_turn_phase(
193 account: &AccountId,
194 turn_id: &TurnId,
195 phase: TurnPhase,
196 ) -> Result<TurnPhaseMarker, StoreError>;
197}
198
199counting_impl! {
200 InteractionReader, "interactions",
201 fn get(
202 account: &AccountId,
203 id: &InteractionId,
204 ) -> Result<InteractionRecord, StoreError>;
205 fn list_open_for_conversation(
206 account: &AccountId,
207 conversation: &ConversationId,
208 ) -> Result<Vec<Interaction>, StoreError>;
209 fn list_open_for_case(
210 account: &AccountId,
211 case_key: &CaseKey,
212 ) -> Result<Vec<Interaction>, StoreError>;
213 fn blocking_answered_at(
214 account: &AccountId,
215 case_key: &CaseKey,
216 revision: CaseRevision,
217 ) -> Result<bool, StoreError>;
218}
219
220counting_impl! {
221 InteractionWriter, "interactions",
222 fn insert(interaction: Interaction) -> Result<(), StoreError>;
223 fn insert_replacing_blocking(
224 interaction: Interaction,
225 ) -> Result<Vec<InteractionId>, StoreError>;
226 fn begin_resolution(
227 account: &AccountId,
228 id: &InteractionId,
229 expected_status: InteractionStatus,
230 option_id: OptionId,
231 resolved_by: TurnId,
232 ) -> Result<InteractionRecord, StoreError>;
233 fn finish_resolution(
234 account: &AccountId,
235 id: &InteractionId,
236 outcome: ResolutionOutcome,
237 ) -> Result<InteractionRecord, StoreError>;
238 fn invalidate_for_case(
239 account: &AccountId,
240 case_key: &CaseKey,
241 new_revision: CaseRevision,
242 reason: InvalidationReason,
243 ) -> Result<Vec<InteractionId>, StoreError>;
244 fn invalidate_case_cards(
245 account: &AccountId,
246 case_key: &CaseKey,
247 reason: InvalidationReason,
248 ) -> Result<Vec<InteractionId>, StoreError>;
249 fn expire_due(now: DateTime<Utc>) -> Result<Vec<InteractionId>, StoreError>;
250}
251
252counting_impl! {
253 CommandJournalReader, "journal",
254 fn get(
255 account: &AccountId,
256 command_id: &CommandId,
257 ) -> Result<CommandJournalEntry, StoreError>;
258 fn for_turn(
259 account: &AccountId,
260 turn_id: &TurnId,
261 ) -> Result<Vec<CommandJournalEntry>, StoreError>;
262 fn pending_for_turn(
263 account: &AccountId,
264 turn_id: &TurnId,
265 ) -> Result<Vec<CommandJournalEntry>, StoreError>;
266}
267
268counting_impl! {
269 CommandJournalWriter, "journal",
270 fn begin(entry: CommandJournalEntry) -> Result<JournalAdmission, StoreError>;
271 fn mark_executing(
272 account: &AccountId,
273 command_id: &CommandId,
274 ) -> Result<(), StoreError>;
275 fn complete(
276 account: &AccountId,
277 command_id: &CommandId,
278 outcome: JournalOutcome,
279 ) -> Result<(), StoreError>;
280 fn fail(
281 account: &AccountId,
282 command_id: &CommandId,
283 error: &ExecutionError,
284 ) -> Result<(), StoreError>;
285}
286
287counting_impl! {
288 EventJournalWriter, "events",
289 fn append(batch: EventBatch) -> Result<Vec<EventId>, StoreError>;
290 fn redact_payload(
291 account: &AccountId,
292 event_id: &EventId,
293 authority: &RedactionAuthority,
294 ) -> Result<EventRedaction, StoreError>;
295}
296
297counting_impl! {
298 EventJournalReader, "events",
299 fn list_since(
300 account: &AccountId,
301 case_key: &CaseKey,
302 since: CaseRevision,
303 limit: usize,
304 ) -> Result<Vec<StoredEvent>, StoreError>;
305 fn read_from(
306 account: &AccountId,
307 after: EventCursor,
308 limit: usize,
309 ) -> Result<EventPage, StoreError>;
310 fn get_by_ids(
311 account: &AccountId,
312 ids: &[EventId],
313 ) -> Result<Vec<StoredEvent>, StoreError>;
314 fn count(account: &AccountId, case_key: &CaseKey) -> Result<u64, StoreError>;
315}
316
317counting_impl! {
318 OutboxReader, "outbox",
319 fn get(outbox_id: &OutboxId) -> Result<OutboxRecord, StoreError>;
320 fn list_for_command(command_id: &CommandId) -> Result<Vec<OutboxRecord>, StoreError>;
321}
322
323counting_impl! {
324 OutboxWriter, "outbox",
325 fn enqueue(entry: OutboxEntry) -> Result<(), StoreError>;
326 fn claim_due(
327 now: DateTime<Utc>,
328 limit: usize,
329 worker_id: &str,
330 ) -> Result<Vec<OutboxEntry>, StoreError>;
331 fn mark_completed(outbox_id: &OutboxId) -> Result<(), StoreError>;
332 fn mark_failed(
333 outbox_id: &OutboxId,
334 reason: String,
335 retry_at: Option<DateTime<Utc>>,
336 ) -> Result<(), StoreError>;
337 fn mark_outcome_unknown(
338 outbox_id: &OutboxId,
339 remote_ref: Option<String>,
340 ) -> Result<(), StoreError>;
341 fn reschedule(
342 outbox_id: &OutboxId,
343 next_attempt_at: DateTime<Utc>,
344 ) -> Result<(), StoreError>;
345 fn release_expired_claims(
346 claimed_before: DateTime<Utc>,
347 ) -> Result<Vec<OutboxId>, StoreError>;
348}
349
350counting_impl! {
351 ReplayReader, "replay",
352 fn get(account: &AccountId, turn_id: &TurnId) -> Result<ReplayRecord, StoreError>;
353 fn list_for_conversation(
354 account: &AccountId,
355 conversation: &ConversationId,
356 limit: usize,
357 ) -> Result<Vec<ReplayRecord>, StoreError>;
358}
359
360counting_impl! {
361 ReplayWriter, "replay",
362 fn put(record: ReplayRecord) -> Result<(), StoreError>;
363}
364
365counting_impl! {
366 CommitStore, "commit",
367 fn commit(
368 account: &AccountId,
369 bundle: CommitBundle,
370 ) -> Result<CommitReceipt, StoreError>;
371}