1use std::fmt;
9use std::sync::Arc;
10use std::time::Duration;
11
12use crate::{
13 ExplorerError, ExplorerQuery, ExplorerRepository, FlowDecision, JobExecutionId,
14 JobExecutionProjection, JobInstanceId, JobInstanceProjection, JobName, MIN_UNRESOLVED_AGE,
15 OperatorRecord, Page, PageRequest, QueryWindow, RecoveryDecision, StepExecutionId,
16 StepExecutionProjection, StepPartitionProjection, TelemetryEventSink, TelemetryRecord,
17};
18use oxide_batch_repository::{page, resume_window, start_window};
19
20#[derive(Clone)]
25pub struct JobExplorer<S> {
26 source: S,
27 event_sinks: Vec<Arc<dyn TelemetryEventSink>>,
28}
29
30impl<S: fmt::Debug> fmt::Debug for JobExplorer<S> {
31 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32 formatter
33 .debug_struct("JobExplorer")
34 .field("source", &self.source)
35 .field("event_sinks", &self.event_sinks.len())
36 .finish()
37 }
38}
39
40impl<S: ExplorerRepository> JobExplorer<S> {
41 pub const fn new(source: S) -> Self {
43 Self {
44 source,
45 event_sinks: Vec::new(),
46 }
47 }
48
49 #[must_use]
51 pub fn with_event_sink(mut self, sink: Arc<dyn TelemetryEventSink>) -> Self {
52 self.event_sinks.push(sink);
53 self
54 }
55
56 pub const fn source(&self) -> &S {
58 &self.source
59 }
60
61 pub async fn list_job_names(
67 &self,
68 request: &PageRequest,
69 ) -> Result<Page<JobName>, ExplorerError> {
70 let query = ExplorerQuery::JobNames;
71 let window = self.window(&query, request).await?;
72 let rows = self.source.job_names(&window).await?;
73 self.finish_page(None, page(&query, request, window.ceiling(), rows))
74 }
75
76 pub async fn list_instances(
82 &self,
83 job_name: &JobName,
84 request: &PageRequest,
85 ) -> Result<Page<JobInstanceProjection>, ExplorerError> {
86 let query = ExplorerQuery::Instances {
87 job_name: job_name.clone(),
88 };
89 let window = self.window(&query, request).await?;
90 let rows = self.source.instances(job_name, &window).await?;
91 self.finish_page(None, page(&query, request, window.ceiling(), rows))
92 }
93
94 pub async fn list_executions(
100 &self,
101 job_instance_id: JobInstanceId,
102 request: &PageRequest,
103 ) -> Result<Page<JobExecutionProjection>, ExplorerError> {
104 let query = ExplorerQuery::Executions { job_instance_id };
105 let window = self.window(&query, request).await?;
106 let rows = self.source.executions(job_instance_id, &window).await?;
107 self.finish_page(None, page(&query, request, window.ceiling(), rows))
108 }
109
110 pub async fn get_execution(
116 &self,
117 job_execution_id: JobExecutionId,
118 ) -> Result<Option<JobExecutionProjection>, ExplorerError> {
119 self.source.execution(job_execution_id).await
120 }
121
122 pub async fn list_step_executions(
128 &self,
129 job_execution_id: JobExecutionId,
130 request: &PageRequest,
131 ) -> Result<Page<StepExecutionProjection>, ExplorerError> {
132 let query = ExplorerQuery::StepExecutions { job_execution_id };
133 let window = self.window(&query, request).await?;
134 let rows = self
135 .source
136 .step_executions(job_execution_id, &window)
137 .await?;
138 self.finish_page(
139 Some(job_execution_id),
140 page(&query, request, window.ceiling(), rows),
141 )
142 }
143
144 pub async fn list_unresolved_executions(
151 &self,
152 minimum_age: Duration,
153 request: &PageRequest,
154 ) -> Result<Page<JobExecutionProjection>, ExplorerError> {
155 if minimum_age < MIN_UNRESOLVED_AGE {
156 return Err(ExplorerError::AgeBoundTooSmall {
157 minimum: MIN_UNRESOLVED_AGE,
158 });
159 }
160 let query = ExplorerQuery::UnresolvedExecutions { minimum_age };
161 let window = self.window(&query, request).await?;
162 let rows = self
163 .source
164 .unresolved_executions(minimum_age, &window)
165 .await?;
166 self.finish_page(None, page(&query, request, window.ceiling(), rows))
167 }
168
169 pub async fn list_recovery_decisions(
175 &self,
176 job_execution_id: JobExecutionId,
177 request: &PageRequest,
178 ) -> Result<Page<RecoveryDecision>, ExplorerError> {
179 let query = ExplorerQuery::RecoveryDecisions { job_execution_id };
180 let window = self.window(&query, request).await?;
181 let rows = self
182 .source
183 .recovery_decisions(job_execution_id, &window)
184 .await?;
185 self.finish_page(
186 Some(job_execution_id),
187 page(&query, request, window.ceiling(), rows),
188 )
189 }
190
191 pub async fn list_flow_decisions(
197 &self,
198 job_execution_id: JobExecutionId,
199 request: &PageRequest,
200 ) -> Result<Page<FlowDecision>, ExplorerError> {
201 let query = ExplorerQuery::FlowDecisions { job_execution_id };
202 let window = self.window(&query, request).await?;
203 let rows = self
204 .source
205 .flow_decisions(job_execution_id, &window)
206 .await?;
207 self.finish_page(
208 Some(job_execution_id),
209 page(&query, request, window.ceiling(), rows),
210 )
211 }
212
213 pub async fn list_step_partitions(
219 &self,
220 step_execution_id: StepExecutionId,
221 request: &PageRequest,
222 ) -> Result<Page<StepPartitionProjection>, ExplorerError> {
223 let query = ExplorerQuery::StepPartitions { step_execution_id };
224 let window = self.window(&query, request).await?;
225 let rows = self
226 .source
227 .step_partitions(step_execution_id, &window)
228 .await?;
229 self.finish_page(None, page(&query, request, window.ceiling(), rows))
230 }
231
232 pub async fn list_operator_requests(
238 &self,
239 job_execution_id: JobExecutionId,
240 request: &PageRequest,
241 ) -> Result<Page<OperatorRecord>, ExplorerError> {
242 let query = ExplorerQuery::OperatorRequests { job_execution_id };
243 let window = self.window(&query, request).await?;
244 let rows = self
245 .source
246 .operator_requests(job_execution_id, &window)
247 .await?;
248 self.finish_page(
249 Some(job_execution_id),
250 page(&query, request, window.ceiling(), rows),
251 )
252 }
253
254 fn finish_page<T>(
255 &self,
256 execution_id: Option<JobExecutionId>,
257 result: Result<Page<T>, ExplorerError>,
258 ) -> Result<Page<T>, ExplorerError> {
259 if result.is_ok() {
260 let record = TelemetryRecord::explorer(execution_id);
261 for sink in &self.event_sinks {
262 crate::telemetry::emit_safely(Some(sink), &record);
263 }
264 }
265 result
266 }
267
268 async fn window(
269 &self,
270 query: &ExplorerQuery,
271 request: &PageRequest,
272 ) -> Result<QueryWindow, ExplorerError> {
273 match request.cursor() {
274 None => {
275 let ceiling = self.source.identity_ceiling(query).await?;
276 Ok(start_window(request, ceiling))
277 }
278 Some(cursor) => resume_window(cursor, query, request),
279 }
280 }
281}