821 lines
No EOL
35 KiB
Rust
821 lines
No EOL
35 KiB
Rust
use super::*;
|
|
use async_trait::async_trait;
|
|
use rusqlite::{params, Connection, Row};
|
|
use serde_json;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
// Helper functions to convert integer values back to enum values
|
|
fn int_to_build_request_status(i: i32) -> BuildRequestStatus {
|
|
match i {
|
|
0 => BuildRequestStatus::BuildRequestUnknown,
|
|
1 => BuildRequestStatus::BuildRequestReceived,
|
|
2 => BuildRequestStatus::BuildRequestPlanning,
|
|
3 => BuildRequestStatus::BuildRequestExecuting,
|
|
4 => BuildRequestStatus::BuildRequestCompleted,
|
|
5 => BuildRequestStatus::BuildRequestFailed,
|
|
6 => BuildRequestStatus::BuildRequestCancelled,
|
|
_ => BuildRequestStatus::BuildRequestUnknown,
|
|
}
|
|
}
|
|
|
|
fn int_to_partition_status(i: i32) -> PartitionStatus {
|
|
match i {
|
|
0 => PartitionStatus::PartitionUnknown,
|
|
1 => PartitionStatus::PartitionRequested,
|
|
2 => PartitionStatus::PartitionAnalyzed,
|
|
3 => PartitionStatus::PartitionBuilding,
|
|
4 => PartitionStatus::PartitionAvailable,
|
|
5 => PartitionStatus::PartitionFailed,
|
|
6 => PartitionStatus::PartitionDelegated,
|
|
_ => PartitionStatus::PartitionUnknown,
|
|
}
|
|
}
|
|
|
|
pub struct SqliteBuildEventLog {
|
|
connection: Arc<Mutex<Connection>>,
|
|
}
|
|
|
|
impl SqliteBuildEventLog {
|
|
pub async fn new(path: &str) -> Result<Self> {
|
|
let conn = Connection::open(path)
|
|
.map_err(|e| BuildEventLogError::ConnectionError(e.to_string()))?;
|
|
|
|
Ok(Self {
|
|
connection: Arc::new(Mutex::new(conn)),
|
|
})
|
|
}
|
|
|
|
// Proper event reconstruction from joined query results
|
|
fn row_to_build_event_from_join(row: &Row) -> rusqlite::Result<BuildEvent> {
|
|
let event_id: String = row.get(0)?;
|
|
let timestamp: i64 = row.get(1)?;
|
|
let build_request_id: String = row.get(2)?;
|
|
let event_type_name: String = row.get(3)?;
|
|
|
|
// Read the actual event data from the joined columns
|
|
let event_type = match event_type_name.as_str() {
|
|
"build_request" => {
|
|
// Read from build_request_events columns (indices 4, 5, 6)
|
|
let status_str: String = row.get(4)?;
|
|
let requested_partitions_json: String = row.get(5)?;
|
|
let message: String = row.get(6)?;
|
|
|
|
let status = status_str.parse::<i32>().unwrap_or(0);
|
|
let requested_partitions: Vec<PartitionRef> = serde_json::from_str(&requested_partitions_json)
|
|
.unwrap_or_default();
|
|
|
|
Some(crate::build_event::EventType::BuildRequestEvent(BuildRequestEvent {
|
|
status,
|
|
requested_partitions,
|
|
message,
|
|
}))
|
|
}
|
|
"partition" => {
|
|
// Read from partition_events columns (indices 4, 5, 6, 7)
|
|
let partition_ref: String = row.get(4)?;
|
|
let status_str: String = row.get(5)?;
|
|
let message: String = row.get(6)?;
|
|
let job_run_id: String = row.get(7).unwrap_or_default();
|
|
|
|
let status = status_str.parse::<i32>().unwrap_or(0);
|
|
|
|
Some(crate::build_event::EventType::PartitionEvent(PartitionEvent {
|
|
partition_ref: Some(PartitionRef { str: partition_ref }),
|
|
status,
|
|
message,
|
|
job_run_id,
|
|
}))
|
|
}
|
|
"job" => {
|
|
// Read from job_events columns (indices 4-10)
|
|
let job_run_id: String = row.get(4)?;
|
|
let job_label: String = row.get(5)?;
|
|
let target_partitions_json: String = row.get(6)?;
|
|
let status_str: String = row.get(7)?;
|
|
let message: String = row.get(8)?;
|
|
let config_json: Option<String> = row.get(9).ok();
|
|
let manifests_json: String = row.get(10)?;
|
|
|
|
let status = status_str.parse::<i32>().unwrap_or(0);
|
|
let target_partitions: Vec<PartitionRef> = serde_json::from_str(&target_partitions_json)
|
|
.unwrap_or_default();
|
|
let config: Option<JobConfig> = config_json
|
|
.and_then(|json| serde_json::from_str(&json).ok());
|
|
let manifests: Vec<PartitionManifest> = serde_json::from_str(&manifests_json)
|
|
.unwrap_or_default();
|
|
|
|
Some(crate::build_event::EventType::JobEvent(JobEvent {
|
|
job_run_id,
|
|
job_label: Some(JobLabel { label: job_label }),
|
|
target_partitions,
|
|
status,
|
|
message,
|
|
config,
|
|
manifests,
|
|
}))
|
|
}
|
|
"delegation" => {
|
|
// Read from delegation_events columns (indices 4, 5, 6)
|
|
let partition_ref: String = row.get(4)?;
|
|
let delegated_to_build_request_id: String = row.get(5)?;
|
|
let message: String = row.get(6)?;
|
|
|
|
Some(crate::build_event::EventType::DelegationEvent(DelegationEvent {
|
|
partition_ref: Some(PartitionRef { str: partition_ref }),
|
|
delegated_to_build_request_id,
|
|
message,
|
|
}))
|
|
}
|
|
_ => None,
|
|
};
|
|
|
|
Ok(BuildEvent {
|
|
event_id,
|
|
timestamp,
|
|
build_request_id,
|
|
event_type,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl BuildEventLog for SqliteBuildEventLog {
|
|
async fn append_event(&self, event: BuildEvent) -> Result<()> {
|
|
let conn = self.connection.lock().unwrap();
|
|
|
|
// First insert into build_events table
|
|
conn.execute(
|
|
"INSERT INTO build_events (event_id, timestamp, build_request_id, event_type) VALUES (?1, ?2, ?3, ?4)",
|
|
params![
|
|
event.event_id,
|
|
event.timestamp,
|
|
event.build_request_id,
|
|
match &event.event_type {
|
|
Some(crate::build_event::EventType::BuildRequestEvent(_)) => "build_request",
|
|
Some(crate::build_event::EventType::PartitionEvent(_)) => "partition",
|
|
Some(crate::build_event::EventType::JobEvent(_)) => "job",
|
|
Some(crate::build_event::EventType::DelegationEvent(_)) => "delegation",
|
|
None => "unknown",
|
|
}
|
|
],
|
|
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
|
|
|
|
// Insert into specific event type table
|
|
match &event.event_type {
|
|
Some(crate::build_event::EventType::BuildRequestEvent(br_event)) => {
|
|
let partitions_json = serde_json::to_string(&br_event.requested_partitions)
|
|
.map_err(|e| BuildEventLogError::SerializationError(e.to_string()))?;
|
|
|
|
conn.execute(
|
|
"INSERT INTO build_request_events (event_id, status, requested_partitions, message) VALUES (?1, ?2, ?3, ?4)",
|
|
params![
|
|
event.event_id,
|
|
br_event.status.to_string(),
|
|
partitions_json,
|
|
br_event.message
|
|
],
|
|
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
|
|
}
|
|
Some(crate::build_event::EventType::PartitionEvent(p_event)) => {
|
|
conn.execute(
|
|
"INSERT INTO partition_events (event_id, partition_ref, status, message, job_run_id) VALUES (?1, ?2, ?3, ?4, ?5)",
|
|
params![
|
|
event.event_id,
|
|
p_event.partition_ref.as_ref().map(|r| &r.str).unwrap_or(&String::new()),
|
|
p_event.status.to_string(),
|
|
p_event.message,
|
|
if p_event.job_run_id.is_empty() { None } else { Some(&p_event.job_run_id) }
|
|
],
|
|
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
|
|
}
|
|
Some(crate::build_event::EventType::JobEvent(j_event)) => {
|
|
let partitions_json = serde_json::to_string(&j_event.target_partitions)
|
|
.map_err(|e| BuildEventLogError::SerializationError(e.to_string()))?;
|
|
let config_json = j_event.config.as_ref()
|
|
.map(|c| serde_json::to_string(c))
|
|
.transpose()
|
|
.map_err(|e| BuildEventLogError::SerializationError(e.to_string()))?;
|
|
let manifests_json = serde_json::to_string(&j_event.manifests)
|
|
.map_err(|e| BuildEventLogError::SerializationError(e.to_string()))?;
|
|
|
|
conn.execute(
|
|
"INSERT INTO job_events (event_id, job_run_id, job_label, target_partitions, status, message, config_json, manifests_json) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
|
params![
|
|
event.event_id,
|
|
j_event.job_run_id,
|
|
j_event.job_label.as_ref().map(|l| &l.label).unwrap_or(&String::new()),
|
|
partitions_json,
|
|
j_event.status.to_string(),
|
|
j_event.message,
|
|
config_json,
|
|
manifests_json
|
|
],
|
|
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
|
|
}
|
|
Some(crate::build_event::EventType::DelegationEvent(d_event)) => {
|
|
conn.execute(
|
|
"INSERT INTO delegation_events (event_id, partition_ref, delegated_to_build_request_id, message) VALUES (?1, ?2, ?3, ?4)",
|
|
params![
|
|
event.event_id,
|
|
d_event.partition_ref.as_ref().map(|r| &r.str).unwrap_or(&String::new()),
|
|
d_event.delegated_to_build_request_id,
|
|
d_event.message
|
|
],
|
|
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
|
|
}
|
|
None => {}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_build_request_events(
|
|
&self,
|
|
build_request_id: &str,
|
|
since: Option<i64>
|
|
) -> Result<Vec<BuildEvent>> {
|
|
let conn = self.connection.lock().unwrap();
|
|
|
|
// Use a UNION query to get all event types with their specific data
|
|
let base_query = "
|
|
SELECT be.event_id, be.timestamp, be.build_request_id, be.event_type,
|
|
bre.status, bre.requested_partitions, bre.message, NULL, NULL, NULL, NULL
|
|
FROM build_events be
|
|
LEFT JOIN build_request_events bre ON be.event_id = bre.event_id
|
|
WHERE be.build_request_id = ? AND be.event_type = 'build_request'
|
|
UNION ALL
|
|
SELECT be.event_id, be.timestamp, be.build_request_id, be.event_type,
|
|
pe.partition_ref, pe.status, pe.message, pe.job_run_id, NULL, NULL, NULL
|
|
FROM build_events be
|
|
LEFT JOIN partition_events pe ON be.event_id = pe.event_id
|
|
WHERE be.build_request_id = ? AND be.event_type = 'partition'
|
|
UNION ALL
|
|
SELECT be.event_id, be.timestamp, be.build_request_id, be.event_type,
|
|
je.job_run_id, je.job_label, je.target_partitions, je.status, je.message, je.config_json, je.manifests_json
|
|
FROM build_events be
|
|
LEFT JOIN job_events je ON be.event_id = je.event_id
|
|
WHERE be.build_request_id = ? AND be.event_type = 'job'
|
|
UNION ALL
|
|
SELECT be.event_id, be.timestamp, be.build_request_id, be.event_type,
|
|
de.partition_ref, de.delegated_to_build_request_id, de.message, NULL, NULL, NULL, NULL
|
|
FROM build_events be
|
|
LEFT JOIN delegation_events de ON be.event_id = de.event_id
|
|
WHERE be.build_request_id = ? AND be.event_type = 'delegation'
|
|
";
|
|
|
|
let query = if since.is_some() {
|
|
format!("{} AND be.timestamp > ? ORDER BY be.timestamp", base_query)
|
|
} else {
|
|
format!("{} ORDER BY be.timestamp", base_query)
|
|
};
|
|
|
|
let mut stmt = conn.prepare(&query)
|
|
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
let rows = if let Some(since_timestamp) = since {
|
|
// We need 5 parameters: build_request_id for each UNION + since_timestamp
|
|
stmt.query_map(params![build_request_id, build_request_id, build_request_id, build_request_id, since_timestamp], Self::row_to_build_event_from_join)
|
|
} else {
|
|
// We need 4 parameters: build_request_id for each UNION
|
|
stmt.query_map(params![build_request_id, build_request_id, build_request_id, build_request_id], Self::row_to_build_event_from_join)
|
|
}.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
let mut events = Vec::new();
|
|
for row in rows {
|
|
events.push(row.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?);
|
|
}
|
|
|
|
Ok(events)
|
|
}
|
|
|
|
async fn get_partition_events(
|
|
&self,
|
|
partition_ref: &str,
|
|
since: Option<i64>
|
|
) -> Result<Vec<BuildEvent>> {
|
|
// First get the build request IDs (release the connection lock quickly)
|
|
let build_ids: Vec<String> = {
|
|
let conn = self.connection.lock().unwrap();
|
|
|
|
// Get all events for builds that included this partition
|
|
// First find all build request IDs that have events for this partition
|
|
let build_ids_query = if since.is_some() {
|
|
"SELECT DISTINCT be.build_request_id
|
|
FROM build_events be
|
|
JOIN partition_events pe ON be.event_id = pe.event_id
|
|
WHERE pe.partition_ref = ? AND be.timestamp > ?"
|
|
} else {
|
|
"SELECT DISTINCT be.build_request_id
|
|
FROM build_events be
|
|
JOIN partition_events pe ON be.event_id = pe.event_id
|
|
WHERE pe.partition_ref = ?"
|
|
};
|
|
|
|
let mut stmt = conn.prepare(build_ids_query)
|
|
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
let row_mapper = |row: &Row| -> rusqlite::Result<String> {
|
|
Ok(row.get::<_, String>(0)?)
|
|
};
|
|
|
|
let build_ids_result: Vec<String> = if let Some(since_timestamp) = since {
|
|
stmt.query_map(params![partition_ref, since_timestamp], row_mapper)
|
|
} else {
|
|
stmt.query_map(params![partition_ref], row_mapper)
|
|
}.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?
|
|
.collect::<std::result::Result<Vec<_>, _>>()
|
|
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
build_ids_result
|
|
}; // Connection lock is released here
|
|
|
|
// Now get all events for those build requests (this gives us complete event reconstruction)
|
|
let mut all_events = Vec::new();
|
|
for build_id in build_ids {
|
|
let events = self.get_build_request_events(&build_id, since).await?;
|
|
all_events.extend(events);
|
|
}
|
|
|
|
// Sort events by timestamp
|
|
all_events.sort_by_key(|e| e.timestamp);
|
|
|
|
Ok(all_events)
|
|
}
|
|
|
|
async fn get_job_run_events(
|
|
&self,
|
|
_job_run_id: &str
|
|
) -> Result<Vec<BuildEvent>> {
|
|
// This method is not implemented because it would require complex joins
|
|
// to reconstruct complete event data. Use get_build_request_events instead
|
|
// which properly reconstructs all event types for a build request.
|
|
Err(BuildEventLogError::QueryError(
|
|
"get_job_run_events is not implemented - use get_build_request_events to get complete event data".to_string()
|
|
))
|
|
}
|
|
|
|
async fn get_events_in_range(
|
|
&self,
|
|
_start_time: i64,
|
|
_end_time: i64
|
|
) -> Result<Vec<BuildEvent>> {
|
|
// This method is not implemented because it would require complex joins
|
|
// to reconstruct complete event data. Use get_build_request_events instead
|
|
// which properly reconstructs all event types for a build request.
|
|
Err(BuildEventLogError::QueryError(
|
|
"get_events_in_range is not implemented - use get_build_request_events to get complete event data".to_string()
|
|
))
|
|
}
|
|
|
|
async fn execute_query(&self, query: &str) -> Result<QueryResult> {
|
|
let conn = self.connection.lock().unwrap();
|
|
|
|
let mut stmt = conn.prepare(query)
|
|
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
let column_count = stmt.column_count();
|
|
let columns: Vec<String> = (0..column_count)
|
|
.map(|i| stmt.column_name(i).unwrap_or("unknown").to_string())
|
|
.collect();
|
|
|
|
let rows = stmt.query_map([], |row| {
|
|
let mut row_data = Vec::new();
|
|
for i in 0..column_count {
|
|
// Try to get as different types and convert to string
|
|
let value: String = if let Ok(int_val) = row.get::<_, i64>(i) {
|
|
int_val.to_string()
|
|
} else if let Ok(float_val) = row.get::<_, f64>(i) {
|
|
float_val.to_string()
|
|
} else if let Ok(str_val) = row.get::<_, String>(i) {
|
|
str_val
|
|
} else if let Ok(str_val) = row.get::<_, Option<String>>(i) {
|
|
str_val.unwrap_or_default()
|
|
} else {
|
|
String::new()
|
|
};
|
|
row_data.push(value);
|
|
}
|
|
Ok(row_data)
|
|
}).map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
let mut result_rows = Vec::new();
|
|
for row in rows {
|
|
result_rows.push(row.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?);
|
|
}
|
|
|
|
Ok(QueryResult {
|
|
columns,
|
|
rows: result_rows,
|
|
})
|
|
}
|
|
async fn get_latest_partition_status(
|
|
&self,
|
|
partition_ref: &str
|
|
) -> Result<Option<(PartitionStatus, i64)>> {
|
|
match self.get_meaningful_partition_status(partition_ref).await? {
|
|
Some((status, timestamp, _build_request_id)) => Ok(Some((status, timestamp))),
|
|
None => Ok(None),
|
|
}
|
|
}
|
|
|
|
async fn get_active_builds_for_partition(
|
|
&self,
|
|
partition_ref: &str
|
|
) -> Result<Vec<String>> {
|
|
let conn = self.connection.lock().unwrap();
|
|
|
|
// Look for build requests that are actively building this partition
|
|
// A build is considered active if:
|
|
// 1. It has scheduled/building events for this partition, AND
|
|
// 2. The build request itself has not completed (status 4=COMPLETED or 5=FAILED)
|
|
let query = "SELECT DISTINCT be.build_request_id
|
|
FROM partition_events pe
|
|
JOIN build_events be ON pe.event_id = be.event_id
|
|
WHERE pe.partition_ref = ?1
|
|
AND pe.status IN ('2', '3') -- PARTITION_ANALYZED or PARTITION_BUILDING
|
|
AND be.build_request_id NOT IN (
|
|
SELECT DISTINCT be3.build_request_id
|
|
FROM build_request_events bre
|
|
JOIN build_events be3 ON bre.event_id = be3.event_id
|
|
WHERE bre.status IN ('4', '5') -- BUILD_REQUEST_COMPLETED or BUILD_REQUEST_FAILED
|
|
)";
|
|
|
|
let mut stmt = conn.prepare(query)
|
|
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
let rows = stmt.query_map([partition_ref], |row| {
|
|
let build_request_id: String = row.get(0)?;
|
|
Ok(build_request_id)
|
|
}).map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
let mut build_request_ids = Vec::new();
|
|
for row in rows {
|
|
build_request_ids.push(row.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?);
|
|
}
|
|
|
|
Ok(build_request_ids)
|
|
}
|
|
|
|
async fn list_build_requests(
|
|
&self,
|
|
limit: u32,
|
|
offset: u32,
|
|
status_filter: Option<BuildRequestStatus>,
|
|
) -> Result<(Vec<BuildRequestSummary>, u32)> {
|
|
let conn = self.connection.lock().unwrap();
|
|
|
|
// Build query based on status filter
|
|
let (where_clause, count_where_clause) = match status_filter {
|
|
Some(_) => (" WHERE bre.status = ?1", " WHERE bre.status = ?1"),
|
|
None => ("", ""),
|
|
};
|
|
|
|
let query = format!(
|
|
"SELECT DISTINCT be.build_request_id, bre.status, bre.requested_partitions,
|
|
MIN(be.timestamp) as created_at, MAX(be.timestamp) as updated_at
|
|
FROM build_events be
|
|
JOIN build_request_events bre ON be.event_id = bre.event_id{}
|
|
GROUP BY be.build_request_id
|
|
ORDER BY created_at DESC
|
|
LIMIT {} OFFSET {}",
|
|
where_clause, limit, offset
|
|
);
|
|
|
|
let count_query = format!(
|
|
"SELECT COUNT(DISTINCT be.build_request_id)
|
|
FROM build_events be
|
|
JOIN build_request_events bre ON be.event_id = bre.event_id{}",
|
|
count_where_clause
|
|
);
|
|
|
|
// Execute count query first
|
|
let total_count: u32 = if let Some(status) = status_filter {
|
|
let status_str = format!("{:?}", status);
|
|
conn.query_row(&count_query, params![status_str], |row| row.get(0))
|
|
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?
|
|
} else {
|
|
conn.query_row(&count_query, [], |row| row.get(0))
|
|
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?
|
|
};
|
|
|
|
// Execute main query
|
|
let mut stmt = conn.prepare(&query)
|
|
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
let build_row_mapper = |row: &Row| -> rusqlite::Result<BuildRequestSummary> {
|
|
let status_str: String = row.get(1)?;
|
|
let status = status_str.parse::<i32>()
|
|
.map(int_to_build_request_status)
|
|
.unwrap_or(BuildRequestStatus::BuildRequestUnknown);
|
|
|
|
Ok(BuildRequestSummary {
|
|
build_request_id: row.get(0)?,
|
|
status,
|
|
requested_partitions: serde_json::from_str(&row.get::<_, String>(2)?).unwrap_or_default(),
|
|
created_at: row.get(3)?,
|
|
updated_at: row.get(4)?,
|
|
})
|
|
};
|
|
|
|
let rows = if let Some(status) = status_filter {
|
|
let status_str = format!("{:?}", status);
|
|
stmt.query_map(params![status_str], build_row_mapper)
|
|
} else {
|
|
stmt.query_map([], build_row_mapper)
|
|
}.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
let mut summaries = Vec::new();
|
|
for row in rows {
|
|
summaries.push(row.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?);
|
|
}
|
|
|
|
Ok((summaries, total_count))
|
|
}
|
|
|
|
async fn list_recent_partitions(
|
|
&self,
|
|
limit: u32,
|
|
offset: u32,
|
|
status_filter: Option<PartitionStatus>,
|
|
) -> Result<(Vec<PartitionSummary>, u32)> {
|
|
// Get all unique partition refs first, ordered by most recent activity
|
|
let (total_count, partition_refs) = {
|
|
let conn = self.connection.lock().unwrap();
|
|
|
|
let count_query = "SELECT COUNT(DISTINCT pe.partition_ref)
|
|
FROM partition_events pe";
|
|
let total_count: u32 = conn.query_row(count_query, [], |row| row.get(0))
|
|
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
let refs_query = "SELECT DISTINCT pe.partition_ref
|
|
FROM partition_events pe
|
|
JOIN build_events be ON pe.event_id = be.event_id
|
|
GROUP BY pe.partition_ref
|
|
ORDER BY MAX(be.timestamp) DESC
|
|
LIMIT ? OFFSET ?";
|
|
|
|
let mut stmt = conn.prepare(refs_query)
|
|
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
let rows = stmt.query_map([limit, offset], |row| {
|
|
let partition_ref: String = row.get(0)?;
|
|
Ok(partition_ref)
|
|
}).map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
let mut partition_refs = Vec::new();
|
|
for row in rows {
|
|
partition_refs.push(row.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?);
|
|
}
|
|
|
|
(total_count, partition_refs)
|
|
};
|
|
|
|
// Get meaningful status for each partition using shared helper
|
|
let mut summaries = Vec::new();
|
|
for partition_ref in partition_refs {
|
|
if let Some((status, updated_at, build_request_id)) = self.get_meaningful_partition_status(&partition_ref).await? {
|
|
// Apply status filter if specified
|
|
if let Some(filter_status) = status_filter {
|
|
if status != filter_status {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
summaries.push(PartitionSummary {
|
|
partition_ref,
|
|
status,
|
|
updated_at,
|
|
build_request_id: Some(build_request_id),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Sort by updated_at descending (most recent first)
|
|
summaries.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
|
|
|
|
Ok((summaries, total_count))
|
|
}
|
|
|
|
async fn get_activity_summary(&self) -> Result<ActivitySummary> {
|
|
// First get the simple counts without holding the lock across awaits
|
|
let (active_builds_count, total_partitions_count) = {
|
|
let conn = self.connection.lock().unwrap();
|
|
|
|
// Get active builds count (builds that are not completed, failed, or cancelled)
|
|
let active_builds_count: u32 = conn.query_row(
|
|
"SELECT COUNT(DISTINCT be.build_request_id)
|
|
FROM build_events be
|
|
JOIN build_request_events bre ON be.event_id = bre.event_id
|
|
WHERE bre.status IN ('BuildRequestReceived', 'BuildRequestPlanning', 'BuildRequestExecuting')",
|
|
[],
|
|
|row| row.get(0)
|
|
).map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
// Get total partitions count
|
|
let total_partitions_count: u32 = conn.query_row(
|
|
"SELECT COUNT(DISTINCT pe.partition_ref)
|
|
FROM partition_events pe
|
|
JOIN build_events be ON pe.event_id = be.event_id",
|
|
[],
|
|
|row| row.get(0)
|
|
).map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
(active_builds_count, total_partitions_count)
|
|
};
|
|
|
|
// Get recent builds (limit to 5 for summary)
|
|
let (recent_builds, _) = self.list_build_requests(5, 0, None).await?;
|
|
|
|
// Get recent partitions (limit to 5 for summary)
|
|
let (recent_partitions, _) = self.list_recent_partitions(5, 0, None).await?;
|
|
|
|
Ok(ActivitySummary {
|
|
active_builds_count,
|
|
recent_builds,
|
|
recent_partitions,
|
|
total_partitions_count,
|
|
})
|
|
}
|
|
|
|
async fn initialize(&self) -> Result<()> {
|
|
let conn = self.connection.lock().unwrap();
|
|
|
|
// Create tables
|
|
conn.execute(
|
|
"CREATE TABLE IF NOT EXISTS build_events (
|
|
event_id TEXT PRIMARY KEY,
|
|
timestamp INTEGER NOT NULL,
|
|
build_request_id TEXT NOT NULL,
|
|
event_type TEXT NOT NULL
|
|
)",
|
|
[],
|
|
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
|
|
|
|
conn.execute(
|
|
"CREATE TABLE IF NOT EXISTS build_request_events (
|
|
event_id TEXT PRIMARY KEY REFERENCES build_events(event_id),
|
|
status TEXT NOT NULL,
|
|
requested_partitions TEXT NOT NULL,
|
|
message TEXT
|
|
)",
|
|
[],
|
|
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
|
|
|
|
conn.execute(
|
|
"CREATE TABLE IF NOT EXISTS partition_events (
|
|
event_id TEXT PRIMARY KEY REFERENCES build_events(event_id),
|
|
partition_ref TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
message TEXT,
|
|
job_run_id TEXT
|
|
)",
|
|
[],
|
|
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
|
|
|
|
conn.execute(
|
|
"CREATE TABLE IF NOT EXISTS job_events (
|
|
event_id TEXT PRIMARY KEY REFERENCES build_events(event_id),
|
|
job_run_id TEXT NOT NULL,
|
|
job_label TEXT NOT NULL,
|
|
target_partitions TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
message TEXT,
|
|
config_json TEXT,
|
|
manifests_json TEXT
|
|
)",
|
|
[],
|
|
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
|
|
|
|
conn.execute(
|
|
"CREATE TABLE IF NOT EXISTS delegation_events (
|
|
event_id TEXT PRIMARY KEY REFERENCES build_events(event_id),
|
|
partition_ref TEXT NOT NULL,
|
|
delegated_to_build_request_id TEXT NOT NULL,
|
|
message TEXT
|
|
)",
|
|
[],
|
|
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
|
|
|
|
// Create indexes
|
|
conn.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_build_events_build_request ON build_events(build_request_id, timestamp)",
|
|
[],
|
|
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
|
|
|
|
conn.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_build_events_timestamp ON build_events(timestamp)",
|
|
[],
|
|
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
|
|
|
|
conn.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_partition_events_partition ON partition_events(partition_ref)",
|
|
[],
|
|
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
|
|
|
|
conn.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_job_events_job_run ON job_events(job_run_id)",
|
|
[],
|
|
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_build_request_for_available_partition(
|
|
&self,
|
|
partition_ref: &str
|
|
) -> Result<Option<String>> {
|
|
let conn = self.connection.lock().unwrap();
|
|
|
|
// Find the most recent PARTITION_AVAILABLE event for this partition
|
|
let query = "SELECT be.build_request_id
|
|
FROM partition_events pe
|
|
JOIN build_events be ON pe.event_id = be.event_id
|
|
WHERE pe.partition_ref = ?1 AND pe.status = '4'
|
|
ORDER BY be.timestamp DESC
|
|
LIMIT 1";
|
|
|
|
let result = conn.query_row(query, [partition_ref], |row| {
|
|
let build_request_id: String = row.get(0)?;
|
|
Ok(build_request_id)
|
|
});
|
|
|
|
match result {
|
|
Ok(build_request_id) => Ok(Some(build_request_id)),
|
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
|
Err(e) => Err(BuildEventLogError::QueryError(e.to_string())),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SqliteBuildEventLog {
|
|
// Shared helper method to get the meaningful partition status for build coordination and display
|
|
// This implements the "delegation-friendly" logic: if a partition was ever available, it remains available
|
|
async fn get_meaningful_partition_status(
|
|
&self,
|
|
partition_ref: &str
|
|
) -> Result<Option<(PartitionStatus, i64, String)>> { // (status, timestamp, build_request_id)
|
|
let conn = self.connection.lock().unwrap();
|
|
|
|
// Check for ANY historical completion first - this is resilient to later events being added
|
|
let available_query = "SELECT pe.status, be.timestamp, be.build_request_id
|
|
FROM partition_events pe
|
|
JOIN build_events be ON pe.event_id = be.event_id
|
|
WHERE pe.partition_ref = ?1 AND pe.status = '4'
|
|
ORDER BY be.timestamp DESC
|
|
LIMIT 1";
|
|
|
|
let mut available_stmt = conn.prepare(available_query)
|
|
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
let available_result = available_stmt.query_row([partition_ref], |row| {
|
|
let status_str: String = row.get(0)?;
|
|
let timestamp: i64 = row.get(1)?;
|
|
let build_request_id: String = row.get(2)?;
|
|
let status = status_str.parse::<i32>()
|
|
.map_err(|_e| rusqlite::Error::InvalidColumnType(0, status_str.clone(), rusqlite::types::Type::Integer))?;
|
|
Ok((status, timestamp, build_request_id))
|
|
});
|
|
|
|
match available_result {
|
|
Ok((status, timestamp, build_request_id)) => {
|
|
let partition_status = PartitionStatus::try_from(status)
|
|
.map_err(|_| BuildEventLogError::QueryError(format!("Invalid partition status: {}", status)))?;
|
|
return Ok(Some((partition_status, timestamp, build_request_id)));
|
|
}
|
|
Err(rusqlite::Error::QueryReturnedNoRows) => {
|
|
// No available partition found, fall back to latest status
|
|
}
|
|
Err(e) => return Err(BuildEventLogError::QueryError(e.to_string())),
|
|
}
|
|
|
|
// Fall back to latest status if no available partition found
|
|
let latest_query = "SELECT pe.status, be.timestamp, be.build_request_id
|
|
FROM partition_events pe
|
|
JOIN build_events be ON pe.event_id = be.event_id
|
|
WHERE pe.partition_ref = ?1
|
|
ORDER BY be.timestamp DESC
|
|
LIMIT 1";
|
|
|
|
let mut latest_stmt = conn.prepare(latest_query)
|
|
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
|
|
|
|
let result = latest_stmt.query_row([partition_ref], |row| {
|
|
let status_str: String = row.get(0)?;
|
|
let timestamp: i64 = row.get(1)?;
|
|
let build_request_id: String = row.get(2)?;
|
|
let status = status_str.parse::<i32>()
|
|
.map_err(|_e| rusqlite::Error::InvalidColumnType(0, status_str.clone(), rusqlite::types::Type::Integer))?;
|
|
Ok((status, timestamp, build_request_id))
|
|
});
|
|
|
|
match result {
|
|
Ok((status, timestamp, build_request_id)) => {
|
|
let partition_status = PartitionStatus::try_from(status)
|
|
.map_err(|_| BuildEventLogError::QueryError(format!("Invalid partition status: {}", status)))?;
|
|
Ok(Some((partition_status, timestamp, build_request_id)))
|
|
}
|
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
|
Err(e) => Err(BuildEventLogError::QueryError(e.to_string())),
|
|
}
|
|
}
|
|
} |