408 lines
No EOL
18 KiB
Rust
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");
|
|
}
|
|
}
|
|
} |