databuild/databuild/repositories/partitions/mod.rs

394 lines
No EOL
17 KiB
Rust

use crate::*;
use crate::event_log::{BuildEventLog, BuildEventLogError, Result};
use crate::status_utils::list_response_helpers;
use std::sync::Arc;
use std::collections::HashMap;
use serde::Serialize;
/// Repository for querying partition data from the build event log
pub struct PartitionsRepository {
event_log: Arc<dyn BuildEventLog>,
}
/// Summary of a partition's current state and history
#[derive(Debug, Clone, Serialize)]
pub struct PartitionInfo {
pub partition_ref: String,
pub current_status: PartitionStatus,
pub last_updated: i64,
pub builds_count: usize,
pub last_successful_build: Option<String>,
pub invalidation_count: usize,
}
/// Detailed partition status with timeline
#[derive(Debug, Clone, Serialize)]
pub struct PartitionStatusEvent {
pub timestamp: i64,
pub status: PartitionStatus,
pub message: String,
pub build_request_id: String,
pub job_run_id: Option<String>,
}
impl PartitionsRepository {
/// Create a new PartitionsRepository
pub fn new(event_log: Arc<dyn BuildEventLog>) -> Self {
Self { event_log }
}
/// List all partitions with their current status
///
/// Returns a list of all partitions that have been referenced in the build event log,
/// along with their current status and summary information.
pub async fn list(&self, limit: Option<usize>) -> Result<Vec<PartitionInfo>> {
// Get all partition events from the event log
let events = self.event_log.get_events_in_range(0, i64::MAX).await?;
let mut partition_data: HashMap<String, Vec<PartitionStatusEvent>> = HashMap::new();
// Collect all partition events
for event in events {
if let Some(build_event::EventType::PartitionEvent(p_event)) = &event.event_type {
if let Some(partition_ref) = &p_event.partition_ref {
let status = match p_event.status_code {
1 => PartitionStatus::PartitionRequested,
2 => PartitionStatus::PartitionAnalyzed,
3 => PartitionStatus::PartitionBuilding,
4 => PartitionStatus::PartitionAvailable,
5 => PartitionStatus::PartitionFailed,
6 => PartitionStatus::PartitionDelegated,
_ => PartitionStatus::PartitionUnknown,
};
let status_event = PartitionStatusEvent {
timestamp: event.timestamp,
status,
message: p_event.message.clone(),
build_request_id: event.build_request_id.clone(),
job_run_id: if p_event.job_run_id.is_empty() { None } else { Some(p_event.job_run_id.clone()) },
};
partition_data.entry(partition_ref.str.clone())
.or_insert_with(Vec::new)
.push(status_event);
}
}
// Also check for partition invalidation events
if let Some(build_event::EventType::PartitionInvalidationEvent(pi_event)) = &event.event_type {
if let Some(partition_ref) = &pi_event.partition_ref {
let status_event = PartitionStatusEvent {
timestamp: event.timestamp,
status: PartitionStatus::PartitionUnknown, // Invalidated
message: format!("Invalidated: {}", pi_event.reason),
build_request_id: event.build_request_id.clone(),
job_run_id: None,
};
partition_data.entry(partition_ref.str.clone())
.or_insert_with(Vec::new)
.push(status_event);
}
}
}
// Convert to PartitionInfo structs
let mut partition_infos: Vec<PartitionInfo> = partition_data.into_iter()
.map(|(partition_ref, mut events)| {
// Sort events by timestamp
events.sort_by_key(|e| e.timestamp);
// Get current status from latest event
let (current_status, last_updated) = events.last()
.map(|e| (e.status.clone(), e.timestamp))
.unwrap_or((PartitionStatus::PartitionUnknown, 0));
// Count builds and find last successful build
let builds: std::collections::HashSet<String> = events.iter()
.map(|e| e.build_request_id.clone())
.collect();
let last_successful_build = events.iter()
.rev()
.find(|e| e.status == PartitionStatus::PartitionAvailable)
.map(|e| e.build_request_id.clone());
// Count invalidations
let invalidation_count = events.iter()
.filter(|e| e.message.starts_with("Invalidated:"))
.count();
PartitionInfo {
partition_ref,
current_status,
last_updated,
builds_count: builds.len(),
last_successful_build,
invalidation_count,
}
})
.collect();
// Sort by most recently updated
partition_infos.sort_by(|a, b| b.last_updated.cmp(&a.last_updated));
// Apply limit if specified
if let Some(limit) = limit {
partition_infos.truncate(limit);
}
Ok(partition_infos)
}
/// Show detailed information about a specific partition
///
/// Returns the complete timeline of status changes for the specified partition,
/// including all builds that have referenced it.
pub async fn show(&self, partition_ref: &str) -> Result<Option<(PartitionInfo, Vec<PartitionStatusEvent>)>> {
// Get all events for this partition
let events = self.event_log.get_partition_events(partition_ref, None).await?;
if events.is_empty() {
return Ok(None);
}
let mut status_events = Vec::new();
let mut builds = std::collections::HashSet::new();
// Process partition events
for event in &events {
if let Some(build_event::EventType::PartitionEvent(p_event)) = &event.event_type {
let status = match p_event.status_code {
1 => PartitionStatus::PartitionRequested,
2 => PartitionStatus::PartitionAnalyzed,
3 => PartitionStatus::PartitionBuilding,
4 => PartitionStatus::PartitionAvailable,
5 => PartitionStatus::PartitionFailed,
6 => PartitionStatus::PartitionDelegated,
_ => PartitionStatus::PartitionUnknown,
};
status_events.push(PartitionStatusEvent {
timestamp: event.timestamp,
status,
message: p_event.message.clone(),
build_request_id: event.build_request_id.clone(),
job_run_id: if p_event.job_run_id.is_empty() { None } else { Some(p_event.job_run_id.clone()) },
});
builds.insert(event.build_request_id.clone());
}
}
// Also check for invalidation events in all events
let all_events = self.event_log.get_events_in_range(0, i64::MAX).await?;
let mut invalidation_count = 0;
for event in all_events {
if let Some(build_event::EventType::PartitionInvalidationEvent(pi_event)) = &event.event_type {
if let Some(partition) = &pi_event.partition_ref {
if partition.str == partition_ref {
status_events.push(PartitionStatusEvent {
timestamp: event.timestamp,
status: PartitionStatus::PartitionUnknown, // Invalidated
message: format!("Invalidated: {}", pi_event.reason),
build_request_id: event.build_request_id.clone(),
job_run_id: None,
});
invalidation_count += 1;
}
}
}
}
// Sort events by timestamp
status_events.sort_by_key(|e| e.timestamp);
// Get current status from latest event
let (current_status, last_updated) = status_events.last()
.map(|e| (e.status.clone(), e.timestamp))
.unwrap_or((PartitionStatus::PartitionUnknown, 0));
// Find last successful build
let last_successful_build = status_events.iter()
.rev()
.find(|e| e.status == PartitionStatus::PartitionAvailable)
.map(|e| e.build_request_id.clone());
let partition_info = PartitionInfo {
partition_ref: partition_ref.to_string(),
current_status,
last_updated,
builds_count: builds.len(),
last_successful_build,
invalidation_count,
};
Ok(Some((partition_info, status_events)))
}
/// Invalidate a partition with a reason
///
/// This method uses the EventWriter to write a partition invalidation event.
/// It validates that the partition exists before invalidating it.
pub async fn invalidate(&self, partition_ref: &str, reason: String, build_request_id: String) -> Result<()> {
// First check if the partition exists
let partition_exists = self.show(partition_ref).await?.is_some();
if !partition_exists {
return Err(BuildEventLogError::QueryError(
format!("Cannot invalidate non-existent partition: {}", partition_ref)
));
}
// Use EventWriter to write the invalidation event
let event_writer = crate::event_log::writer::EventWriter::new(self.event_log.clone());
let partition = PartitionRef { str: partition_ref.to_string() };
event_writer.invalidate_partition(build_request_id, partition, reason).await
}
/// List partitions returning protobuf response format with dual status fields
///
/// This method provides the unified CLI/Service response format with both
/// status codes (enum values) and status names (human-readable strings).
pub async fn list_protobuf(&self, request: PartitionsListRequest) -> Result<PartitionsListResponse> {
// Get legacy format data
let partition_infos = self.list(request.limit.map(|l| l as usize)).await?;
// Convert to protobuf format with dual status fields
let partitions: Vec<PartitionSummary> = partition_infos.into_iter()
.map(|info| {
list_response_helpers::create_partition_summary(
info.partition_ref,
info.current_status,
info.last_updated,
info.builds_count,
info.invalidation_count,
info.last_successful_build,
)
})
.collect();
// TODO: Implement proper pagination with offset and has_more
// For now, return simple response without full pagination support
let total_count = partitions.len() as u32;
let has_more = false; // This would be calculated based on actual total vs returned
Ok(PartitionsListResponse {
partitions,
total_count,
has_more,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event_log::mock::{MockBuildEventLog, test_events};
#[tokio::test]
async fn test_partitions_repository_list_empty() {
let mock_log = Arc::new(MockBuildEventLog::new().await.unwrap());
let repo = PartitionsRepository::new(mock_log);
let partitions = repo.list(None).await.unwrap();
assert!(partitions.is_empty());
}
#[tokio::test]
async fn test_partitions_repository_list_with_data() {
let build_id = "test-build-123".to_string();
let partition1 = PartitionRef { str: "data/users".to_string() };
let partition2 = PartitionRef { str: "data/orders".to_string() };
// Create events for multiple partitions
let events = vec![
test_events::build_request_received(Some(build_id.clone()), vec![partition1.clone(), partition2.clone()]),
test_events::partition_status(Some(build_id.clone()), partition1.clone(), PartitionStatus::PartitionBuilding, None),
test_events::partition_status(Some(build_id.clone()), partition1.clone(), PartitionStatus::PartitionAvailable, None),
test_events::partition_status(Some(build_id.clone()), partition2.clone(), PartitionStatus::PartitionBuilding, None),
test_events::partition_status(Some(build_id.clone()), partition2.clone(), PartitionStatus::PartitionFailed, None),
];
let mock_log = Arc::new(MockBuildEventLog::with_events(events).await.unwrap());
let repo = PartitionsRepository::new(mock_log);
let partitions = repo.list(None).await.unwrap();
assert_eq!(partitions.len(), 2);
// Find partitions by name
let users_partition = partitions.iter().find(|p| p.partition_ref == "data/users").unwrap();
let orders_partition = partitions.iter().find(|p| p.partition_ref == "data/orders").unwrap();
assert_eq!(users_partition.current_status, PartitionStatus::PartitionAvailable);
assert_eq!(orders_partition.current_status, PartitionStatus::PartitionFailed);
assert_eq!(users_partition.builds_count, 1);
assert_eq!(orders_partition.builds_count, 1);
}
#[tokio::test]
async fn test_partitions_repository_show() {
let build_id = "test-build-456".to_string();
let partition = PartitionRef { str: "analytics/metrics".to_string() };
let events = vec![
test_events::partition_status(Some(build_id.clone()), partition.clone(), PartitionStatus::PartitionRequested, None),
test_events::partition_status(Some(build_id.clone()), partition.clone(), PartitionStatus::PartitionBuilding, None),
test_events::partition_status(Some(build_id.clone()), partition.clone(), PartitionStatus::PartitionAvailable, None),
];
let mock_log = Arc::new(MockBuildEventLog::with_events(events).await.unwrap());
let repo = PartitionsRepository::new(mock_log);
let result = repo.show(&partition.str).await.unwrap();
assert!(result.is_some());
let (info, timeline) = result.unwrap();
assert_eq!(info.partition_ref, "analytics/metrics");
assert_eq!(info.current_status, PartitionStatus::PartitionAvailable);
assert_eq!(info.builds_count, 1);
assert_eq!(timeline.len(), 3);
// Verify timeline order
assert_eq!(timeline[0].status, PartitionStatus::PartitionRequested);
assert_eq!(timeline[1].status, PartitionStatus::PartitionBuilding);
assert_eq!(timeline[2].status, PartitionStatus::PartitionAvailable);
}
#[tokio::test]
async fn test_partitions_repository_show_nonexistent() {
let mock_log = Arc::new(MockBuildEventLog::new().await.unwrap());
let repo = PartitionsRepository::new(mock_log);
let result = repo.show("nonexistent/partition").await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn test_partitions_repository_invalidate() {
let build_id = "test-build-789".to_string();
let partition = PartitionRef { str: "temp/data".to_string() };
// Start with an existing partition
let events = vec![
test_events::partition_status(Some(build_id.clone()), partition.clone(), PartitionStatus::PartitionAvailable, None),
];
let mock_log = Arc::new(MockBuildEventLog::with_events(events).await.unwrap());
let repo = PartitionsRepository::new(mock_log.clone());
// Invalidate the partition
repo.invalidate(&partition.str, "Test invalidation".to_string(), build_id.clone()).await.unwrap();
// Verify the invalidation was recorded
// Note: This test demonstrates the pattern, but the MockBuildEventLog would need
// to be enhanced to properly store invalidation events for full verification
// Try to invalidate a non-existent partition
let result = repo.invalidate("nonexistent/partition", "Should fail".to_string(), build_id).await;
assert!(result.is_err());
}
}