use std::collections::BTreeMap;
use std::fmt;
use std::time::Duration;
use futures::future::try_join_all;
use tracing::info;
use super::partition::{PartitionSpec, PartitionValue};
use super::serialization::{concat_batches, serialize_to_arrow_ipc};
use crate::kafka::producer::KafkaProducer;
use crate::types::arrow::{ArrowSerializable, FieldIdMap};
use crate::types::error::Result;
const PRODUCE_MAX_IN_FLIGHT: usize = 256;
pub struct IcebergRepository {
field_id_map: FieldIdMap,
partition_spec: PartitionSpec,
producer: KafkaProducer,
}
impl IcebergRepository {
#[must_use]
pub const fn field_id_map(&self) -> &FieldIdMap {
&self.field_id_map
}
pub fn flush(&self, timeout: Duration) -> Result<i32> {
self.producer.flush(timeout)
}
fn group_by_partition<'models, T, F>(
&self,
models: &'models [T],
partition_values_fn: F,
) -> Result<BTreeMap<Vec<u8>, Vec<&'models T>>>
where
F: Fn(&T) -> Vec<Option<PartitionValue>>,
{
let mut groups: BTreeMap<Vec<u8>, Vec<&T>> = BTreeMap::new();
for model in models {
let values = partition_values_fn(model);
let key = self.partition_spec.compute_key(&values)?;
groups.entry(key).or_default().push(model);
}
Ok(groups)
}
pub async fn insert<T: ArrowSerializable + Send + Sync>(
&self,
model: &T,
partition_values: &[Option<PartitionValue>],
) -> Result<()> {
let batch = model.to_record_batch(&self.field_id_map)?;
let payload = serialize_to_arrow_ipc(&[batch])?;
let key = self.partition_spec.compute_key(partition_values)?;
self.producer.produce(&key, &payload).await
}
pub async fn insert_batch<T, F>(&self, models: &[T], partition_values_fn: F) -> Result<()>
where
T: ArrowSerializable + Send + Sync,
F: Fn(&T) -> Vec<Option<PartitionValue>>,
{
self.insert_batch_concurrent(models, partition_values_fn)
.await
}
pub async fn insert_batch_concurrent<T, F>(
&self,
models: &[T],
partition_values_fn: F,
) -> Result<()>
where
T: ArrowSerializable + Send + Sync,
F: Fn(&T) -> Vec<Option<PartitionValue>>,
{
if models.is_empty() {
return Ok(());
}
let group_vec: Vec<(Vec<u8>, Vec<&T>)> = self
.group_by_partition(models, partition_values_fn)?
.into_iter()
.collect();
for window in group_vec.chunks(PRODUCE_MAX_IN_FLIGHT) {
let mut payloads: Vec<(Vec<u8>, Vec<u8>)> = Vec::with_capacity(window.len());
for (key, group_model_vec) in window {
payloads.push((key.clone(), self.serialize_group(group_model_vec)?));
}
try_join_all(
payloads
.iter()
.map(|(key, payload)| self.producer.produce(key, payload)),
)
.await?;
}
Ok(())
}
pub async fn insert_batch_serial<T, F>(
&self,
models: &[T],
partition_values_fn: F,
) -> Result<()>
where
T: ArrowSerializable + Send + Sync,
F: Fn(&T) -> Vec<Option<PartitionValue>>,
{
if models.is_empty() {
return Ok(());
}
let group_map = self.group_by_partition(models, partition_values_fn)?;
for (key, group_model_vec) in &group_map {
let payload = self.serialize_group(group_model_vec)?;
self.producer.produce(key, &payload).await?;
}
Ok(())
}
pub fn new(
producer: KafkaProducer,
field_id_map: FieldIdMap,
partition_spec: PartitionSpec,
) -> Self {
info!(
topic = producer.topic(),
table = field_id_map.table_name(),
"initialized IcebergRepository"
);
Self {
field_id_map,
partition_spec,
producer,
}
}
#[must_use]
pub const fn partition_spec(&self) -> &PartitionSpec {
&self.partition_spec
}
fn serialize_group<T: ArrowSerializable>(&self, group_models: &[&T]) -> Result<Vec<u8>> {
let batches = group_models
.iter()
.map(|m| m.to_record_batch(&self.field_id_map))
.collect::<Result<Vec<_>>>()?;
let combined = concat_batches(&batches)?;
serialize_to_arrow_ipc(&[combined])
}
}
impl fmt::Debug for IcebergRepository {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("IcebergRepository")
.field("topic", &self.producer.topic())
.field("table", &self.field_id_map.table_name())
.finish_non_exhaustive()
}
}