databuild/databuild/repositories/builds/mod.rs
2025-08-20 23:34:37 -07:00

408 lines
No EOL
18 KiB
Rust

use crate::*;
use crate::event_log::{BuildEventLogError, Result};
use crate::event_log::query_engine::BELQueryEngine;
use crate::{BuildDetailResponse, BuildTimelineEvent as ServiceBuildTimelineEvent};
use std::sync::Arc;
// use std::collections::HashMap; // Commented out since not used with new query engine
use serde::Serialize;
/// Repository for querying build data from the build event log
pub struct BuildsRepository {
query_engine: Arc<BELQueryEngine>,
}
/// Summary of a build request and its current status
#[derive(Debug, Clone, Serialize)]
pub struct BuildInfo {
pub build_request_id: String,
pub status: BuildRequestStatus,
pub requested_partitions: Vec<PartitionRef>,
pub requested_at: i64,
pub started_at: Option<i64>,
pub completed_at: Option<i64>,
pub duration_ms: Option<i64>,
pub total_jobs: usize,
pub completed_jobs: usize,
pub failed_jobs: usize,
pub cancelled_jobs: usize,
pub cancelled: bool,
pub cancel_reason: Option<String>,
}
/// Detailed timeline of a build's execution events
#[derive(Debug, Clone, Serialize)]
pub struct BuildEvent {
pub timestamp: i64,
pub event_type: String,
pub status: Option<BuildRequestStatus>,
pub message: String,
pub cancel_reason: Option<String>,
}
impl BuildsRepository {
/// Create a new BuildsRepository
pub fn new(query_engine: Arc<BELQueryEngine>) -> Self {
Self { query_engine }
}
/// List all builds with their current status
///
/// Returns a list of all build requests that have been made,
/// including their current status and execution details.
pub async fn list(&self, limit: Option<usize>) -> Result<Vec<BuildInfo>> {
// Use query engine to list builds with the protobuf request format
let request = BuildsListRequest {
limit: limit.map(|l| l as u32),
offset: Some(0),
status_filter: None,
};
let response = self.query_engine.list_build_requests(request).await?;
// Convert from protobuf BuildSummary to repository BuildInfo
let builds = response.builds.into_iter().map(|build| {
BuildInfo {
build_request_id: build.build_request_id,
status: build.status.clone().unwrap_or(BuildRequestStatusCode::BuildRequestUnknown.status()),
requested_partitions: build.requested_partitions,
requested_at: build.requested_at,
started_at: build.started_at,
completed_at: build.completed_at,
duration_ms: build.duration_ms,
total_jobs: build.total_jobs as usize,
completed_jobs: build.completed_jobs as usize,
failed_jobs: build.failed_jobs as usize,
cancelled_jobs: build.cancelled_jobs as usize,
cancelled: build.cancelled,
cancel_reason: None, // TODO: Add cancel reason to BuildSummary if needed
}
}).collect();
Ok(builds)
}
/// Show detailed information about a specific build
///
/// Returns the complete timeline of events for the specified build,
/// including all status changes and any cancellation events.
pub async fn show(&self, build_request_id: &str) -> Result<Option<(BuildInfo, Vec<BuildEvent>)>> {
// Use query engine to get build summary
let summary_result = self.query_engine.get_build_request_summary(build_request_id).await;
match summary_result {
Ok(summary) => {
// Convert BuildRequestSummary to BuildInfo
let build_info = BuildInfo {
build_request_id: summary.build_request_id,
status: summary.status,
requested_partitions: summary.requested_partitions.into_iter()
.map(|s| PartitionRef { str: s })
.collect(),
requested_at: summary.created_at,
started_at: None, // TODO: Track started_at in query engine
completed_at: Some(summary.updated_at),
duration_ms: None, // TODO: Calculate duration in query engine
total_jobs: 0, // TODO: Implement job counting in query engine
completed_jobs: 0,
failed_jobs: 0,
cancelled_jobs: 0,
cancelled: false, // TODO: Track cancellation in query engine
cancel_reason: None,
};
// Get all events for this build to create a proper timeline
let all_events = self.query_engine.get_build_request_events(build_request_id, None).await?;
// Create timeline from build request events
let mut timeline = Vec::new();
for event in all_events {
if let Some(crate::build_event::EventType::BuildRequestEvent(br_event)) = &event.event_type {
if let Some(status) = br_event.clone().status {
timeline.push(BuildEvent {
timestamp: event.timestamp,
event_type: "build_status".to_string(),
status: Some(status),
message: br_event.message.clone(),
cancel_reason: None,
});
}
}
}
// Sort timeline by timestamp
timeline.sort_by_key(|e| e.timestamp);
Ok(Some((build_info, timeline)))
}
Err(_) => {
// Build not found
Ok(None)
}
}
}
/// Show detailed information about a specific build using protobuf response format
///
/// Returns the complete build details with dual status fields and timeline events.
pub async fn show_protobuf(&self, build_request_id: &str) -> Result<Option<BuildDetailResponse>> {
// Get build info and timeline using existing show method
if let Some((build_info, timeline)) = self.show(build_request_id).await? {
// Convert timeline events to protobuf format
let protobuf_timeline: Vec<ServiceBuildTimelineEvent> = timeline
.into_iter()
.map(|event| ServiceBuildTimelineEvent {
timestamp: event.timestamp,
status: event.status,
message: event.message,
event_type: event.event_type,
cancel_reason: event.cancel_reason,
})
.collect();
let response = BuildDetailResponse {
build_request_id: build_info.build_request_id,
status: Some(build_info.status),
requested_partitions: build_info.requested_partitions,
total_jobs: build_info.total_jobs as u32,
completed_jobs: build_info.completed_jobs as u32,
failed_jobs: build_info.failed_jobs as u32,
cancelled_jobs: build_info.cancelled_jobs as u32,
requested_at: build_info.requested_at,
started_at: build_info.started_at,
completed_at: build_info.completed_at,
duration_ms: build_info.duration_ms,
cancelled: build_info.cancelled,
cancel_reason: build_info.cancel_reason,
timeline: protobuf_timeline,
};
Ok(Some(response))
} else {
Ok(None)
}
}
/// Cancel a build with a reason
///
/// This method uses the EventWriter to write a build cancellation event.
/// It validates that the build exists and is in a cancellable state.
pub async fn cancel(&self, build_request_id: &str, _reason: String) -> Result<()> {
// First check if the build exists and get its current status
let build_info = self.show(build_request_id).await?;
if build_info.is_none() {
return Err(BuildEventLogError::QueryError(
format!("Cannot cancel non-existent build: {}", build_request_id)
));
}
let (build, _timeline) = build_info.unwrap();
// Check if build is in a cancellable state
match BuildRequestStatusCode::try_from(build.status.code) {
Ok(BuildRequestStatusCode::BuildRequestCompleted) => {
return Err(BuildEventLogError::QueryError(
format!("Cannot cancel completed build: {}", build_request_id)
));
}
Ok(BuildRequestStatusCode::BuildRequestFailed) => {
return Err(BuildEventLogError::QueryError(
format!("Cannot cancel failed build: {}", build_request_id)
));
}
Ok(BuildRequestStatusCode::BuildRequestCancelled) => {
return Err(BuildEventLogError::QueryError(
format!("Build already cancelled: {}", build_request_id)
));
}
_ => {}
}
// Create a build cancellation event
use crate::event_log::{create_build_event, current_timestamp_nanos, generate_event_id};
let cancel_event = create_build_event(
build_request_id.to_string(),
crate::build_event::EventType::BuildRequestEvent(crate::BuildRequestEvent {
status: Some(BuildRequestStatusCode::BuildRequestCancelled.status()),
requested_partitions: build.requested_partitions,
message: format!("Build cancelled"),
comment: None,
want_id: None,
})
);
// Append the cancellation event
self.query_engine.append_event(cancel_event).await?;
Ok(())
}
/// List builds using protobuf response format with dual status fields
///
/// Returns BuildSummary protobuf messages with status_code and status_name.
pub async fn list_protobuf(&self, limit: Option<usize>) -> Result<Vec<crate::BuildSummary>> {
// Get build info using existing list method
let builds = self.list(limit).await?;
// Convert to protobuf format
let protobuf_builds: Vec<crate::BuildSummary> = builds
.into_iter()
.map(|build| crate::BuildSummary {
build_request_id: build.build_request_id,
status: Some(build.status),
requested_partitions: build.requested_partitions.into_iter().map(|p| crate::PartitionRef { str: p.str }).collect(),
total_jobs: build.total_jobs as u32,
completed_jobs: build.completed_jobs as u32,
failed_jobs: build.failed_jobs as u32,
cancelled_jobs: build.cancelled_jobs as u32,
requested_at: build.requested_at,
started_at: build.started_at,
completed_at: build.completed_at,
duration_ms: build.duration_ms,
cancelled: build.cancelled,
comment: None,
})
.collect();
Ok(protobuf_builds)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event_log::mock::{create_mock_bel_query_engine, create_mock_bel_query_engine_with_events, test_events};
#[tokio::test]
async fn test_builds_repository_list_empty() {
let query_engine = create_mock_bel_query_engine().await.unwrap();
let repo = BuildsRepository::new(query_engine);
let builds = repo.list(None).await.unwrap();
assert!(builds.is_empty());
}
#[tokio::test]
async fn test_builds_repository_list_with_data() {
let build_id1 = "build-123".to_string();
let build_id2 = "build-456".to_string();
let partition1 = PartitionRef { str: "data/users".to_string() };
let partition2 = PartitionRef { str: "data/orders".to_string() };
// Create events for multiple builds
let events = vec![
test_events::build_request_event(Some(build_id1.clone()), vec![partition1.clone()], BuildRequestStatusCode::BuildRequestReceived.status()),
test_events::build_request_event(Some(build_id1.clone()), vec![partition1.clone()], BuildRequestStatusCode::BuildRequestCompleted.status()),
test_events::build_request_event(Some(build_id2.clone()), vec![partition2.clone()], BuildRequestStatusCode::BuildRequestReceived.status()),
test_events::build_request_event(Some(build_id2.clone()), vec![partition2.clone()], BuildRequestStatusCode::BuildRequestFailed.status()),
];
let query_engine = create_mock_bel_query_engine_with_events(events).await.unwrap();
let repo = BuildsRepository::new(query_engine);
let builds = repo.list(None).await.unwrap();
assert_eq!(builds.len(), 2);
// Find builds by id
let build1 = builds.iter().find(|b| b.build_request_id == build_id1).unwrap();
let build2 = builds.iter().find(|b| b.build_request_id == build_id2).unwrap();
assert_eq!(build1.status, BuildRequestStatusCode::BuildRequestCompleted.status());
assert_eq!(build1.requested_partitions.len(), 1);
assert!(!build1.cancelled);
assert_eq!(build2.status, BuildRequestStatusCode::BuildRequestFailed.status());
assert_eq!(build2.requested_partitions.len(), 1);
assert!(!build2.cancelled);
}
#[tokio::test]
async fn test_builds_repository_show() {
let build_id = "build-789".to_string();
let partition = PartitionRef { str: "analytics/daily".to_string() };
let events = vec![
test_events::build_request_event(Some(build_id.clone()), vec![partition.clone()], BuildRequestStatusCode::BuildRequestReceived.status()),
test_events::build_request_event(Some(build_id.clone()), vec![partition.clone()], BuildRequestStatusCode::BuildRequestPlanning.status()),
test_events::build_request_event(Some(build_id.clone()), vec![partition.clone()], BuildRequestStatusCode::BuildRequestExecuting.status()),
test_events::build_request_event(Some(build_id.clone()), vec![partition.clone()], BuildRequestStatusCode::BuildRequestCompleted.status()),
];
let query_engine = create_mock_bel_query_engine_with_events(events).await.unwrap();
let repo = BuildsRepository::new(query_engine);
let result = repo.show(&build_id).await.unwrap();
assert!(result.is_some());
let (info, timeline) = result.unwrap();
assert_eq!(info.build_request_id, build_id);
assert_eq!(info.status, BuildRequestStatusCode::BuildRequestCompleted.status());
assert!(!info.cancelled);
assert_eq!(timeline.len(), 4);
assert_eq!(timeline[0].status, Some(BuildRequestStatusCode::BuildRequestReceived.status()));
assert_eq!(timeline[1].status, Some(BuildRequestStatusCode::BuildRequestPlanning.status()));
assert_eq!(timeline[2].status, Some(BuildRequestStatusCode::BuildRequestExecuting.status()));
assert_eq!(timeline[3].status, Some(BuildRequestStatusCode::BuildRequestCompleted.status()));
}
#[tokio::test]
async fn test_builds_repository_show_nonexistent() {
let query_engine = create_mock_bel_query_engine().await.unwrap();
let repo = BuildsRepository::new(query_engine);
let result = repo.show("nonexistent-build").await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn test_builds_repository_cancel() {
let build_id = "build-cancel-test".to_string();
let partition = PartitionRef { str: "test/data".to_string() };
// Start with a running build
let events = vec![
test_events::build_request_event(Some(build_id.clone()), vec![partition.clone()], BuildRequestStatusCode::BuildRequestReceived.status()),
test_events::build_request_event(Some(build_id.clone()), vec![partition.clone()], BuildRequestStatusCode::BuildRequestExecuting.status()),
];
let query_engine = create_mock_bel_query_engine_with_events(events).await.unwrap();
let repo = BuildsRepository::new(query_engine.clone());
// Cancel the build
repo.cancel(&build_id, "User requested cancellation".to_string()).await.unwrap();
// Verify the cancellation was recorded
// Note: This test demonstrates the pattern, but the MockBELStorage would need
// to be enhanced to properly store build cancel events for full verification
// Try to cancel a non-existent build
let result = repo.cancel("nonexistent-build", "Should fail".to_string()).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_builds_repository_cancel_completed_build() {
let build_id = "completed-build".to_string();
let partition = PartitionRef { str: "test/data".to_string() };
// Create a completed build
let events = vec![
test_events::build_request_event(Some(build_id.clone()), vec![partition.clone()], BuildRequestStatusCode::BuildRequestReceived.status()),
test_events::build_request_event(Some(build_id.clone()), vec![partition.clone()], BuildRequestStatusCode::BuildRequestCompleted.status()),
];
let query_engine = create_mock_bel_query_engine_with_events(events).await.unwrap();
let repo = BuildsRepository::new(query_engine);
// Try to cancel the completed build - should fail
let result = repo.cancel(&build_id, "Should fail".to_string()).await;
assert!(result.is_err());
if let Err(BuildEventLogError::QueryError(msg)) = result {
assert!(msg.contains("Cannot cancel completed build"));
} else {
panic!("Expected QueryError for completed build cancellation");
}
}
}