blob: 0cf501cb45b10affa854c06acfb9e1052f3cfa4a [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::client::write::batch::WriteBatch::{ArrowLog, Kv};
use crate::client::write::batch::{ArrowLogWriteBatch, KvWriteBatch, WriteBatch};
use crate::client::{LogWriteRecord, Record, ResultHandle, WriteRecord};
use crate::cluster::{BucketLocation, Cluster, ServerNode};
use crate::config::Config;
use crate::error::Result;
use crate::metadata::{PhysicalTablePath, TableBucket};
use crate::util::current_time_ms;
use crate::{BucketId, PartitionId, TableId};
use dashmap::DashMap;
use parking_lot::Mutex;
use parking_lot::RwLock;
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
use std::sync::atomic::{AtomicI32, AtomicI64, Ordering};
// Type alias to simplify complex nested types
type BucketBatches = Vec<(BucketId, Arc<Mutex<VecDeque<WriteBatch>>>)>;
#[allow(dead_code)]
pub struct RecordAccumulator {
config: Config,
write_batches: DashMap<Arc<PhysicalTablePath>, BucketAndWriteBatches>,
// batch_id -> complete callback
incomplete_batches: RwLock<HashMap<i64, ResultHandle>>,
batch_timeout_ms: i64,
closed: bool,
flushes_in_progress: AtomicI32,
appends_in_progress: i32,
nodes_drain_index: Mutex<HashMap<i32, usize>>,
batch_id: AtomicI64,
}
impl RecordAccumulator {
pub fn new(config: Config) -> Self {
let batch_timeout_ms = config.writer_batch_timeout_ms;
RecordAccumulator {
config,
write_batches: Default::default(),
incomplete_batches: Default::default(),
batch_timeout_ms,
closed: Default::default(),
flushes_in_progress: Default::default(),
appends_in_progress: Default::default(),
nodes_drain_index: Default::default(),
batch_id: Default::default(),
}
}
fn try_append(
&self,
record: &WriteRecord,
dq: &mut VecDeque<WriteBatch>,
) -> Result<Option<RecordAppendResult>> {
let dq_size = dq.len();
if let Some(last_batch) = dq.back_mut() {
return if let Some(result_handle) = last_batch.try_append(record)? {
Ok(Some(RecordAppendResult::new(
result_handle,
dq_size > 1 || last_batch.is_closed(),
false,
false,
)))
} else {
Ok(None)
};
}
Ok(None)
}
fn append_new_batch(
&self,
cluster: &Cluster,
record: &WriteRecord,
dq: &mut VecDeque<WriteBatch>,
) -> Result<RecordAppendResult> {
if let Some(append_result) = self.try_append(record, dq)? {
return Ok(append_result);
}
let physical_table_path = &record.physical_table_path;
let table_path = physical_table_path.get_table_path();
let table_info = cluster.get_table(table_path)?;
let arrow_compression_info = table_info.get_table_config().get_arrow_compression_info()?;
let row_type = &table_info.row_type;
let schema_id = table_info.schema_id;
let mut batch: WriteBatch = match record.record() {
Record::Log(_) => ArrowLog(ArrowLogWriteBatch::new(
self.batch_id.fetch_add(1, Ordering::Relaxed),
Arc::clone(physical_table_path),
schema_id,
arrow_compression_info,
row_type,
current_time_ms(),
matches!(&record.record, Record::Log(LogWriteRecord::RecordBatch(_))),
)?),
Record::Kv(kv_record) => Kv(KvWriteBatch::new(
self.batch_id.fetch_add(1, Ordering::Relaxed),
Arc::clone(physical_table_path),
schema_id,
// TODO: Decide how to derive write limit in the absence of java's equivalent of PreAllocatedPagedOutputView
KvWriteBatch::DEFAULT_WRITE_LIMIT,
record.write_format.to_kv_format()?,
kv_record.target_columns.clone(),
current_time_ms(),
)),
};
let batch_id = batch.batch_id();
let result_handle = batch
.try_append(record)?
.expect("must append to a new batch");
let batch_is_closed = batch.is_closed();
dq.push_back(batch);
self.incomplete_batches
.write()
.insert(batch_id, result_handle.clone());
Ok(RecordAppendResult::new(
result_handle,
dq.len() > 1 || batch_is_closed,
true,
false,
))
}
pub fn append(
&self,
record: &WriteRecord<'_>,
bucket_id: BucketId,
cluster: &Cluster,
abort_if_batch_full: bool,
) -> Result<RecordAppendResult> {
let physical_table_path = &record.physical_table_path;
let table_path = physical_table_path.get_table_path();
let table_info = cluster.get_table(table_path)?;
let is_partitioned_table = table_info.is_partitioned();
let partition_id = if is_partitioned_table {
cluster.get_partition_id(physical_table_path)
} else {
None
};
let dq = {
let mut binding = self
.write_batches
.entry(Arc::clone(physical_table_path))
.or_insert_with(|| BucketAndWriteBatches {
table_id: table_info.table_id,
is_partitioned_table,
partition_id,
batches: Default::default(),
});
let bucket_and_batches = binding.value_mut();
bucket_and_batches
.batches
.entry(bucket_id)
.or_insert_with(|| Arc::new(Mutex::new(VecDeque::new())))
.clone()
};
let mut dq_guard = dq.lock();
if let Some(append_result) = self.try_append(record, &mut dq_guard)? {
return Ok(append_result);
}
if abort_if_batch_full {
return Ok(RecordAppendResult::new_without_result_handle(
true, false, true,
));
}
self.append_new_batch(cluster, record, &mut dq_guard)
}
pub fn ready(&self, cluster: &Arc<Cluster>) -> Result<ReadyCheckResult> {
// Snapshot just the Arcs we need, avoiding cloning the entire BucketAndWriteBatches struct
let entries: Vec<(Arc<PhysicalTablePath>, Option<PartitionId>, BucketBatches)> = self
.write_batches
.iter()
.map(|entry| {
let physical_table_path = Arc::clone(entry.key());
let partition_id = entry.value().partition_id;
let bucket_batches: Vec<_> = entry
.value()
.batches
.iter()
.map(|(bucket_id, batch_arc)| (*bucket_id, batch_arc.clone()))
.collect();
(physical_table_path, partition_id, bucket_batches)
})
.collect();
let mut ready_nodes = HashSet::new();
let mut next_ready_check_delay_ms = self.batch_timeout_ms;
let mut unknown_leader_tables = HashSet::new();
for (physical_table_path, mut partition_id, bucket_batches) in entries {
next_ready_check_delay_ms = self.bucket_ready(
&physical_table_path,
physical_table_path.get_partition_name().is_some(),
&mut partition_id,
bucket_batches,
&mut ready_nodes,
&mut unknown_leader_tables,
cluster,
next_ready_check_delay_ms,
)?
}
Ok(ReadyCheckResult {
ready_nodes,
next_ready_check_delay_ms,
unknown_leader_tables,
})
}
#[allow(clippy::too_many_arguments)]
fn bucket_ready(
&self,
physical_table_path: &Arc<PhysicalTablePath>,
is_partitioned_table: bool,
partition_id: &mut Option<PartitionId>,
bucket_batches: BucketBatches,
ready_nodes: &mut HashSet<ServerNode>,
unknown_leader_tables: &mut HashSet<Arc<PhysicalTablePath>>,
cluster: &Cluster,
next_ready_check_delay_ms: i64,
) -> Result<i64> {
let mut next_delay = next_ready_check_delay_ms;
// First check this table has partitionId.
if is_partitioned_table && partition_id.is_none() {
let partition_id = cluster.get_partition_id(physical_table_path);
if partition_id.is_some() {
// Update the cached partition_id
if let Some(mut entry) = self.write_batches.get_mut(physical_table_path) {
entry.partition_id = partition_id;
}
} else {
log::debug!(
"Partition does not exist for {}, bucket will not be set to ready",
physical_table_path.as_ref()
);
// TODO: we shouldn't add unready partitions to unknownLeaderTables,
// because it cases PartitionNotExistException later
unknown_leader_tables.insert(Arc::clone(physical_table_path));
return Ok(next_delay);
}
}
for (bucket_id, batch) in bucket_batches {
let batch_guard = batch.lock();
if batch_guard.is_empty() {
continue;
}
let batch = batch_guard.front().unwrap();
let waited_time_ms = batch.waited_time_ms(current_time_ms());
let deque_size = batch_guard.len();
let full = deque_size > 1 || batch.is_closed();
let table_bucket = cluster.get_table_bucket(physical_table_path, bucket_id)?;
if let Some(leader) = cluster.leader_for(&table_bucket) {
next_delay =
self.batch_ready(leader, waited_time_ms, full, ready_nodes, next_delay);
} else {
unknown_leader_tables.insert(Arc::clone(physical_table_path));
}
}
Ok(next_delay)
}
fn batch_ready(
&self,
leader: &ServerNode,
waited_time_ms: i64,
full: bool,
ready_nodes: &mut HashSet<ServerNode>,
next_ready_check_delay_ms: i64,
) -> i64 {
if !ready_nodes.contains(leader) {
let expired = waited_time_ms >= self.batch_timeout_ms;
let sendable = full || expired || self.closed || self.flush_in_progress();
if sendable {
ready_nodes.insert(leader.clone());
} else {
let time_left_ms = self.batch_timeout_ms.saturating_sub(waited_time_ms);
return next_ready_check_delay_ms.min(time_left_ms);
}
}
next_ready_check_delay_ms
}
pub fn drain(
&self,
cluster: Arc<Cluster>,
nodes: &HashSet<ServerNode>,
max_size: i32,
) -> Result<HashMap<i32, Vec<ReadyWriteBatch>>> {
if nodes.is_empty() {
return Ok(HashMap::new());
}
let mut batches = HashMap::new();
for node in nodes {
let ready = self.drain_batches_for_one_node(&cluster, node, max_size)?;
if !ready.is_empty() {
batches.insert(node.id(), ready);
}
}
Ok(batches)
}
fn drain_batches_for_one_node(
&self,
cluster: &Cluster,
node: &ServerNode,
max_size: i32,
) -> Result<Vec<ReadyWriteBatch>> {
let mut size: usize = 0;
let buckets = self.get_all_buckets_in_current_node(node, cluster);
let mut ready = Vec::new();
if buckets.is_empty() {
return Ok(ready);
}
let start = {
let mut nodes_drain_index_guard = self.nodes_drain_index.lock();
let drain_index = nodes_drain_index_guard.entry(node.id()).or_insert(0);
*drain_index % buckets.len()
};
let mut current_index = start;
let mut last_processed_index;
loop {
let bucket = &buckets[current_index];
let table_path = bucket.physical_table_path();
let table_bucket = bucket.table_bucket.clone();
last_processed_index = current_index;
current_index = (current_index + 1) % buckets.len();
let deque = self
.write_batches
.get(table_path)
.and_then(|bucket_and_write_batches| {
bucket_and_write_batches
.batches
.get(&table_bucket.bucket_id())
.cloned()
});
if let Some(deque) = deque {
let mut maybe_batch = None;
{
let mut batch_lock = deque.lock();
if !batch_lock.is_empty() {
let first_batch = batch_lock.front().unwrap();
if size + first_batch.estimated_size_in_bytes() > max_size as usize
&& !ready.is_empty()
{
// there is a rare case that a single batch size is larger than the request size
// due to compression; in this case we will still eventually send this batch in
// a single request.
break;
}
maybe_batch = Some(batch_lock.pop_front().unwrap());
}
}
if let Some(mut batch) = maybe_batch {
let current_batch_size = batch.estimated_size_in_bytes();
size += current_batch_size;
// mark the batch as drained.
batch.drained(current_time_ms());
ready.push(ReadyWriteBatch {
table_bucket,
write_batch: batch,
});
}
}
if current_index == start {
break;
}
}
// Store the last processed index to maintain round-robin fairness
{
let mut nodes_drain_index_guard = self.nodes_drain_index.lock();
nodes_drain_index_guard.insert(node.id(), last_processed_index);
}
Ok(ready)
}
pub fn remove_incomplete_batches(&self, batch_id: i64) {
self.incomplete_batches.write().remove(&batch_id);
}
pub fn re_enqueue(&self, ready_write_batch: ReadyWriteBatch) {
ready_write_batch.write_batch.re_enqueued();
let physical_table_path = ready_write_batch.write_batch.physical_table_path();
let bucket_id = ready_write_batch.table_bucket.bucket_id();
let table_id = ready_write_batch.table_bucket.table_id();
let partition_id = ready_write_batch.table_bucket.partition_id();
let is_partitioned_table = partition_id.is_some();
let dq = {
let mut binding = self
.write_batches
.entry(Arc::clone(physical_table_path))
.or_insert_with(|| BucketAndWriteBatches {
table_id,
is_partitioned_table,
partition_id,
batches: Default::default(),
});
let bucket_and_batches = binding.value_mut();
bucket_and_batches
.batches
.entry(bucket_id)
.or_insert_with(|| Arc::new(Mutex::new(VecDeque::new())))
.clone()
};
let mut dq_guard = dq.lock();
dq_guard.push_front(ready_write_batch.write_batch);
}
fn get_all_buckets_in_current_node(
&self,
current: &ServerNode,
cluster: &Cluster,
) -> Vec<BucketLocation> {
let mut buckets = vec![];
for bucket_locations in cluster.get_bucket_locations_by_path().values() {
for bucket_location in bucket_locations {
if let Some(leader) = bucket_location.leader() {
if current.id() == leader.id() {
buckets.push(bucket_location.clone());
}
}
}
}
buckets
}
fn flush_in_progress(&self) -> bool {
self.flushes_in_progress.load(Ordering::SeqCst) > 0
}
pub fn begin_flush(&self) {
self.flushes_in_progress.fetch_add(1, Ordering::SeqCst);
}
#[allow(unused_must_use)]
pub async fn await_flush_completion(&self) -> Result<()> {
// Clone handles before awaiting to avoid holding RwLock read guard across await points
let handles: Vec<_> = self.incomplete_batches.read().values().cloned().collect();
// Await on all handles
let result = async {
for result_handle in handles {
result_handle.wait().await?;
}
Ok(())
}
.await;
// Always decrement flushes_in_progress, even if an error occurred
// This mimics the Java finally block behavior
self.flushes_in_progress.fetch_sub(1, Ordering::SeqCst);
result
}
}
pub struct ReadyWriteBatch {
pub table_bucket: TableBucket,
pub write_batch: WriteBatch,
}
impl ReadyWriteBatch {
pub fn write_batch(&self) -> &WriteBatch {
&self.write_batch
}
}
#[allow(dead_code)]
struct BucketAndWriteBatches {
table_id: TableId,
is_partitioned_table: bool,
partition_id: Option<PartitionId>,
batches: HashMap<BucketId, Arc<Mutex<VecDeque<WriteBatch>>>>,
}
pub struct RecordAppendResult {
pub batch_is_full: bool,
pub new_batch_created: bool,
pub abort_record_for_new_batch: bool,
pub result_handle: Option<ResultHandle>,
}
impl RecordAppendResult {
fn new(
result_handle: ResultHandle,
batch_is_full: bool,
new_batch_created: bool,
abort_record_for_new_batch: bool,
) -> Self {
Self {
batch_is_full,
new_batch_created,
abort_record_for_new_batch,
result_handle: Some(result_handle),
}
}
fn new_without_result_handle(
batch_is_full: bool,
new_batch_created: bool,
abort_record_for_new_batch: bool,
) -> Self {
Self {
batch_is_full,
new_batch_created,
abort_record_for_new_batch,
result_handle: None,
}
}
}
pub struct ReadyCheckResult {
pub ready_nodes: HashSet<ServerNode>,
pub next_ready_check_delay_ms: i64,
pub unknown_leader_tables: HashSet<Arc<PhysicalTablePath>>,
}
impl ReadyCheckResult {
pub fn new(
ready_nodes: HashSet<ServerNode>,
next_ready_check_delay_ms: i64,
unknown_leader_tables: HashSet<Arc<PhysicalTablePath>>,
) -> Self {
ReadyCheckResult {
ready_nodes,
next_ready_check_delay_ms,
unknown_leader_tables,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metadata::TablePath;
use crate::row::{Datum, GenericRow};
use crate::test_utils::{build_cluster, build_table_info};
use std::sync::Arc;
#[tokio::test]
async fn re_enqueue_increments_attempts() -> Result<()> {
let config = Config::default();
let accumulator = RecordAccumulator::new(config);
let table_path = TablePath::new("db".to_string(), "tbl".to_string());
let physical_table_path = Arc::new(PhysicalTablePath::of(Arc::new(table_path.clone())));
let table_info = Arc::new(build_table_info(table_path.clone(), 1, 1));
let cluster = Arc::new(build_cluster(&table_path, 1, 1));
let row = GenericRow {
values: vec![Datum::Int32(1)],
};
let record = WriteRecord::for_append(table_info, physical_table_path, 1, &row);
accumulator.append(&record, 0, &cluster, false)?;
let server = cluster.get_tablet_server(1).expect("server");
let nodes = HashSet::from([server.clone()]);
let mut batches = accumulator.drain(cluster.clone(), &nodes, 1024 * 1024)?;
let mut drained = batches.remove(&1).expect("drained batches");
let batch = drained.pop().expect("batch");
assert_eq!(batch.write_batch.attempts(), 0);
accumulator.re_enqueue(batch);
let mut batches = accumulator.drain(cluster, &nodes, 1024 * 1024)?;
let mut drained = batches.remove(&1).expect("drained batches");
let batch = drained.pop().expect("batch");
assert_eq!(batch.write_batch.attempts(), 1);
Ok(())
}
#[tokio::test]
async fn flush_counter_decremented_on_error() -> Result<()> {
use crate::client::write::broadcast::BroadcastOnce;
use std::sync::atomic::Ordering;
let config = Config::default();
let accumulator = RecordAccumulator::new(config);
accumulator.begin_flush();
assert_eq!(accumulator.flushes_in_progress.load(Ordering::SeqCst), 1);
// Create a failing batch by dropping the BroadcastOnce without broadcasting
{
let broadcast = BroadcastOnce::default();
let receiver = broadcast.receiver();
let handle = ResultHandle::new(receiver);
accumulator.incomplete_batches.write().insert(1, handle);
// broadcast is dropped here, causing an error
}
// Await flush completion should fail but still decrement counter
let result = accumulator.await_flush_completion().await;
assert!(result.is_err());
// Counter should still be decremented (this is the critical fix!)
assert_eq!(accumulator.flushes_in_progress.load(Ordering::SeqCst), 0);
assert!(!accumulator.flush_in_progress());
Ok(())
}
}