blob: ac3836aa385863a1e57518037d78d3d767640840 [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::broadcast;
use crate::client::metadata::Metadata;
use crate::client::write::batch::WriteBatch;
use crate::client::{ReadyWriteBatch, RecordAccumulator};
use crate::error::Error::UnexpectedError;
use crate::error::{FlussError, Result};
use crate::metadata::{PhysicalTablePath, TableBucket, TablePath};
use crate::proto::{
PbProduceLogRespForBucket, PbPutKvRespForBucket, ProduceLogResponse, PutKvResponse,
};
use crate::rpc::ServerConnection;
use crate::rpc::message::{ProduceLogRequest, PutKvRequest};
use crate::{PartitionId, TableId};
use log::{debug, warn};
use parking_lot::Mutex;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::Duration;
#[allow(dead_code)]
pub struct Sender {
running: bool,
metadata: Arc<Metadata>,
accumulator: Arc<RecordAccumulator>,
in_flight_batches: Mutex<HashMap<TableBucket, Vec<i64>>>,
max_request_size: i32,
ack: i16,
max_request_timeout_ms: i32,
retries: i32,
}
impl Sender {
pub fn new(
metadata: Arc<Metadata>,
accumulator: Arc<RecordAccumulator>,
max_request_size: i32,
max_request_timeout_ms: i32,
ack: i16,
retries: i32,
) -> Self {
Self {
running: true,
metadata,
accumulator,
in_flight_batches: Default::default(),
max_request_size,
ack,
max_request_timeout_ms,
retries,
}
}
pub async fn run(&self) -> Result<()> {
loop {
if !self.running {
return Ok(());
}
self.run_once().await?;
}
}
async fn run_once(&self) -> Result<()> {
let cluster = self.metadata.get_cluster();
let ready_check_result = self.accumulator.ready(&cluster)?;
// Update metadata if needed
if !ready_check_result.unknown_leader_tables.is_empty() {
let mut table_paths: HashSet<&TablePath> = HashSet::new();
let mut physical_table_paths: HashSet<&Arc<PhysicalTablePath>> = HashSet::new();
for unknown_paths in ready_check_result.unknown_leader_tables.iter() {
if unknown_paths.get_partition_name().is_some() {
physical_table_paths.insert(unknown_paths);
} else {
table_paths.insert(unknown_paths.get_table_path());
}
}
if let Err(e) = self
.metadata
.update_tables_metadata(&table_paths, &physical_table_paths, vec![])
.await
{
match e.api_error() {
Some(FlussError::PartitionNotExists) => {
warn!("Partition does not exist during metadata update, continuing: {e}");
}
_ => return Err(e),
}
}
debug!(
"Client update metadata due to unknown leader tables from the batched records: {:?}",
ready_check_result.unknown_leader_tables
);
}
if ready_check_result.ready_nodes.is_empty() {
tokio::time::sleep(Duration::from_millis(
ready_check_result.next_ready_check_delay_ms as u64,
))
.await;
return Ok(());
}
let batches = self.accumulator.drain(
cluster.clone(),
&ready_check_result.ready_nodes,
self.max_request_size,
)?;
if !batches.is_empty() {
self.add_to_inflight_batches(&batches);
self.send_write_requests(batches).await?;
}
Ok(())
}
fn add_to_inflight_batches(&self, batches: &HashMap<i32, Vec<ReadyWriteBatch>>) {
let mut in_flight = self.in_flight_batches.lock();
for batch_list in batches.values() {
for batch in batch_list {
in_flight
.entry(batch.table_bucket.clone())
.or_default()
.push(batch.write_batch.batch_id());
}
}
}
async fn send_write_requests(
&self,
collated: HashMap<i32, Vec<ReadyWriteBatch>>,
) -> Result<()> {
for (leader_id, batches) in collated {
self.send_write_request(leader_id, self.ack, batches)
.await?;
}
Ok(())
}
async fn send_write_request(
&self,
destination: i32,
acks: i16,
batches: Vec<ReadyWriteBatch>,
) -> Result<()> {
if batches.is_empty() {
return Ok(());
}
let mut records_by_bucket = HashMap::new();
let mut write_batch_by_table: HashMap<TableId, Vec<TableBucket>> = HashMap::new();
for batch in batches {
let table_bucket = batch.table_bucket.clone();
write_batch_by_table
.entry(table_bucket.table_id())
.or_default()
.push(table_bucket.clone());
records_by_bucket.insert(table_bucket, batch);
}
let cluster = self.metadata.get_cluster();
let destination_node = match cluster.get_tablet_server(destination) {
Some(node) => node,
None => {
self.handle_batches_with_error(
records_by_bucket.into_values().collect(),
FlussError::LeaderNotAvailableException,
format!("Destination node not found in metadata cache {destination}."),
)
.await?;
return Ok(());
}
};
let connection = match self.metadata.get_connection(destination_node).await {
Ok(connection) => connection,
Err(e) => {
self.handle_batches_with_error(
records_by_bucket.into_values().collect(),
FlussError::NetworkException,
format!("Failed to connect destination node {destination}: {e}"),
)
.await?;
return Ok(());
}
};
for (table_id, table_buckets) in write_batch_by_table {
let mut request_batches: Vec<ReadyWriteBatch> = table_buckets
.iter()
.filter_map(|bucket| records_by_bucket.remove(bucket))
.collect();
if request_batches.is_empty() {
continue;
}
let write_request = match Self::build_write_request(
table_id,
acks,
self.max_request_timeout_ms,
&mut request_batches,
) {
Ok(req) => req,
Err(e) => {
self.handle_batches_with_local_error(
request_batches,
format!("Failed to build write request: {e}"),
)?;
continue;
}
};
// let's put in back into records_by_bucket
// since response handle will use it.
for request_batch in request_batches {
records_by_bucket.insert(request_batch.table_bucket.clone(), request_batch);
}
self.send_and_handle_response(
&connection,
write_request,
table_id,
&table_buckets,
&mut records_by_bucket,
)
.await?;
}
Ok(())
}
fn build_write_request(
table_id: i64,
acks: i16,
timeout_ms: i32,
request_batches: &mut [ReadyWriteBatch],
) -> Result<WriteRequest> {
let first_batch = &request_batches.first().unwrap().write_batch;
let request = match first_batch {
WriteBatch::ArrowLog(_) => {
let req = ProduceLogRequest::new(table_id, acks, timeout_ms, request_batches)?;
WriteRequest::ProduceLog(req)
}
WriteBatch::Kv(kv_write_batch) => {
let target_columns = kv_write_batch.target_columns();
for batch in request_batches.iter().skip(1) {
match &batch.write_batch {
WriteBatch::ArrowLog(_) => {
return Err(UnexpectedError {
message: "Expecting KvWriteBatch but found ArrowLogWriteBatch"
.to_string(),
source: None,
});
}
WriteBatch::Kv(kvb) => {
if target_columns != kvb.target_columns() {
return Err(UnexpectedError {
message: format!(
"All the write batches to make put kv request should have the same target columns, but got {:?} and {:?}.",
target_columns,
kvb.target_columns()
),
source: None,
});
}
}
}
}
let cols = target_columns
.map(|arc| arc.iter().map(|&c| c as i32).collect())
.unwrap_or_default();
let req = PutKvRequest::new(table_id, acks, timeout_ms, cols, request_batches)?;
WriteRequest::PutKv(req)
}
};
Ok(request)
}
async fn send_and_handle_response(
&self,
connection: &ServerConnection,
write_request: WriteRequest,
table_id: i64,
table_buckets: &[TableBucket],
records_by_bucket: &mut HashMap<TableBucket, ReadyWriteBatch>,
) -> Result<()> {
macro_rules! send {
($request:expr) => {
match connection.request($request).await {
Ok(response) => {
self.handle_write_response(
table_id,
table_buckets,
records_by_bucket,
response,
)
.await
}
Err(e) => {
self.handle_batches_with_error(
table_buckets
.iter()
.filter_map(|b| records_by_bucket.remove(b))
.collect(),
FlussError::NetworkException,
format!("Failed to send write request: {e}"),
)
.await
}
}
};
}
match write_request {
WriteRequest::ProduceLog(req) => send!(req),
WriteRequest::PutKv(req) => send!(req),
}
}
async fn handle_write_response<R: WriteResponse>(
&self,
table_id: i64,
request_buckets: &[TableBucket],
records_by_bucket: &mut HashMap<TableBucket, ReadyWriteBatch>,
response: R,
) -> Result<()> {
let mut invalid_metadata_tables: HashSet<TablePath> = HashSet::new();
let mut invalid_physical_table_paths: HashSet<Arc<PhysicalTablePath>> = HashSet::new();
let mut pending_buckets: HashSet<TableBucket> = request_buckets.iter().cloned().collect();
for bucket_resp in response.buckets_resp() {
let tb = TableBucket::new_with_partition(
table_id,
bucket_resp.partition_id(),
bucket_resp.bucket_id(),
);
let Some(ready_batch) = records_by_bucket.remove(&tb) else {
panic!("Missing ready batch for table bucket {tb}");
};
pending_buckets.remove(&tb);
match bucket_resp.error_code() {
Some(code) if code != FlussError::None.code() => {
let error = FlussError::for_code(code);
let message = bucket_resp
.error_message()
.cloned()
.unwrap_or_else(|| error.message().to_string());
if let Some(physical_table_path) =
self.handle_write_batch_error(ready_batch, error, message)?
{
invalid_metadata_tables
.insert(physical_table_path.get_table_path().clone());
invalid_physical_table_paths.insert(physical_table_path);
}
}
_ => self.complete_batch(ready_batch),
}
}
for bucket in pending_buckets {
if let Some(ready_batch) = records_by_bucket.remove(&bucket) {
if let Some(physical_table_path) = self.handle_write_batch_error(
ready_batch,
FlussError::UnknownServerError,
format!("Missing response for table bucket {bucket}"),
)? {
invalid_metadata_tables.insert(physical_table_path.get_table_path().clone());
invalid_physical_table_paths.insert(physical_table_path);
}
}
}
self.update_metadata_if_needed(invalid_metadata_tables, invalid_physical_table_paths)
.await;
Ok(())
}
fn complete_batch(&self, ready_write_batch: ReadyWriteBatch) {
self.finish_batch(ready_write_batch, Ok(()));
}
fn fail_batch(&self, ready_write_batch: ReadyWriteBatch, error: broadcast::Error) {
self.finish_batch(ready_write_batch, Err(error));
}
fn finish_batch(&self, ready_write_batch: ReadyWriteBatch, result: broadcast::Result<()>) {
if ready_write_batch.write_batch.complete(result) {
self.remove_from_inflight_batches(&ready_write_batch);
// remove from incomplete batches
self.accumulator
.remove_incomplete_batches(ready_write_batch.write_batch.batch_id())
}
}
async fn handle_batches_with_error(
&self,
batches: Vec<ReadyWriteBatch>,
error: FlussError,
message: String,
) -> Result<()> {
let mut invalid_metadata_tables: HashSet<TablePath> = HashSet::new();
let mut invalid_physical_table_paths: HashSet<Arc<PhysicalTablePath>> = HashSet::new();
for batch in batches {
if let Some(physical_table_path) =
self.handle_write_batch_error(batch, error, message.clone())?
{
invalid_metadata_tables.insert(physical_table_path.get_table_path().clone());
invalid_physical_table_paths.insert(physical_table_path);
}
}
self.update_metadata_if_needed(invalid_metadata_tables, invalid_physical_table_paths)
.await;
Ok(())
}
fn handle_batches_with_local_error(
&self,
batches: Vec<ReadyWriteBatch>,
message: String,
) -> Result<()> {
for batch in batches {
self.fail_batch(
batch,
broadcast::Error::Client {
message: message.clone(),
},
);
}
Ok(())
}
fn handle_write_batch_error(
&self,
ready_write_batch: ReadyWriteBatch,
error: FlussError,
message: String,
) -> Result<Option<Arc<PhysicalTablePath>>> {
let physical_table_path = Arc::clone(ready_write_batch.write_batch.physical_table_path());
if self.can_retry(&ready_write_batch, error) {
warn!(
"Retrying write batch for {} on bucket {} after error {error:?}: {message}",
physical_table_path.as_ref(),
ready_write_batch.table_bucket.bucket_id()
);
self.re_enqueue_batch(ready_write_batch);
return Ok(Self::is_invalid_metadata_error(error).then_some(physical_table_path));
}
if error == FlussError::DuplicateSequenceException {
warn!(
"Duplicate sequence for {} on bucket {}: {message}",
physical_table_path.as_ref(),
ready_write_batch.table_bucket.bucket_id()
);
self.complete_batch(ready_write_batch);
return Ok(None);
}
self.fail_batch(
ready_write_batch,
broadcast::Error::WriteFailed {
code: error.code(),
message,
},
);
Ok(Self::is_invalid_metadata_error(error).then_some(physical_table_path))
}
fn re_enqueue_batch(&self, ready_write_batch: ReadyWriteBatch) {
self.remove_from_inflight_batches(&ready_write_batch);
self.accumulator.re_enqueue(ready_write_batch);
}
fn remove_from_inflight_batches(&self, ready_write_batch: &ReadyWriteBatch) {
let batch_id = ready_write_batch.write_batch.batch_id();
let mut in_flight_guard = self.in_flight_batches.lock();
if let Some(in_flight) = in_flight_guard.get_mut(&ready_write_batch.table_bucket) {
in_flight.retain(|id| *id != batch_id);
if in_flight.is_empty() {
in_flight_guard.remove(&ready_write_batch.table_bucket);
}
}
}
fn can_retry(&self, ready_write_batch: &ReadyWriteBatch, error: FlussError) -> bool {
ready_write_batch.write_batch.attempts() < self.retries
&& !ready_write_batch.write_batch.is_done()
&& Self::is_retriable_error(error)
}
async fn update_metadata_if_needed(
&self,
table_paths: HashSet<TablePath>,
physical_table_path: HashSet<Arc<PhysicalTablePath>>,
) {
if table_paths.is_empty() {
return;
}
let table_path_refs: HashSet<&TablePath> = table_paths.iter().collect();
let physical_table_path_refs: HashSet<&Arc<PhysicalTablePath>> =
physical_table_path.iter().collect();
if let Err(e) = self
.metadata
.update_tables_metadata(&table_path_refs, &physical_table_path_refs, vec![])
.await
{
warn!("Failed to update metadata after write error: {e:?}");
}
}
fn is_invalid_metadata_error(error: FlussError) -> bool {
matches!(
error,
FlussError::NotLeaderOrFollower
| FlussError::UnknownTableOrBucketException
| FlussError::LeaderNotAvailableException
| FlussError::NetworkException
)
}
fn is_retriable_error(error: FlussError) -> bool {
matches!(
error,
FlussError::NetworkException
| FlussError::NotLeaderOrFollower
| FlussError::UnknownTableOrBucketException
| FlussError::LeaderNotAvailableException
| FlussError::LogStorageException
| FlussError::KvStorageException
| FlussError::StorageException
| FlussError::RequestTimeOut
| FlussError::NotEnoughReplicasAfterAppendException
| FlussError::NotEnoughReplicasException
| FlussError::CorruptMessage
| FlussError::CorruptRecordException
)
}
pub async fn close(&mut self) {
self.running = false;
}
}
enum WriteRequest {
ProduceLog(ProduceLogRequest),
PutKv(PutKvRequest),
}
trait BucketResponse {
fn bucket_id(&self) -> i32;
fn error_code(&self) -> Option<i32>;
fn error_message(&self) -> Option<&String>;
fn partition_id(&self) -> Option<PartitionId>;
}
impl BucketResponse for PbProduceLogRespForBucket {
fn bucket_id(&self) -> i32 {
self.bucket_id
}
fn error_code(&self) -> Option<i32> {
self.error_code
}
fn error_message(&self) -> Option<&String> {
self.error_message.as_ref()
}
fn partition_id(&self) -> Option<PartitionId> {
self.partition_id
}
}
impl BucketResponse for PbPutKvRespForBucket {
fn bucket_id(&self) -> i32 {
self.bucket_id
}
fn error_code(&self) -> Option<i32> {
self.error_code
}
fn error_message(&self) -> Option<&String> {
self.error_message.as_ref()
}
fn partition_id(&self) -> Option<PartitionId> {
self.partition_id
}
}
trait WriteResponse {
type BucketResp: BucketResponse;
fn buckets_resp(&self) -> &[Self::BucketResp];
}
impl WriteResponse for ProduceLogResponse {
type BucketResp = PbProduceLogRespForBucket;
fn buckets_resp(&self) -> &[Self::BucketResp] {
&self.buckets_resp
}
}
impl WriteResponse for PutKvResponse {
type BucketResp = PbPutKvRespForBucket;
fn buckets_resp(&self) -> &[Self::BucketResp] {
&self.buckets_resp
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::WriteRecord;
use crate::cluster::Cluster;
use crate::config::Config;
use crate::metadata::{PhysicalTablePath, TablePath};
use crate::proto::{PbProduceLogRespForBucket, ProduceLogResponse};
use crate::row::{Datum, GenericRow};
use crate::rpc::FlussError;
use crate::test_utils::{build_cluster_arc, build_table_info};
use std::collections::{HashMap, HashSet};
fn build_ready_batch(
accumulator: &RecordAccumulator,
cluster: Arc<Cluster>,
table_path: Arc<TablePath>,
) -> Result<(ReadyWriteBatch, crate::client::ResultHandle)> {
let table_info = Arc::new(build_table_info(table_path.as_ref().clone(), 1, 1));
let physical_table_path = Arc::new(PhysicalTablePath::of(table_path));
let row = GenericRow {
values: vec![Datum::Int32(1)],
};
let record = WriteRecord::for_append(table_info, physical_table_path, 1, &row);
let result = accumulator.append(&record, 0, &cluster, false)?;
let result_handle = result.result_handle.expect("result handle");
let server = cluster.get_tablet_server(1).expect("server");
let nodes = HashSet::from([server.clone()]);
let mut batches = accumulator.drain(cluster, &nodes, 1024 * 1024)?;
let mut drained = batches.remove(&1).expect("drained batches");
let batch = drained.pop().expect("batch");
Ok((batch, result_handle))
}
#[tokio::test]
async fn handle_write_batch_error_retries() -> Result<()> {
let table_path = Arc::new(TablePath::new("db".to_string(), "tbl".to_string()));
let cluster = build_cluster_arc(table_path.as_ref(), 1, 1);
let metadata = Arc::new(Metadata::new_for_test(cluster.clone()));
let accumulator = Arc::new(RecordAccumulator::new(Config::default()));
let sender = Sender::new(metadata, accumulator.clone(), 1024 * 1024, 1000, 1, 1);
let (batch, _handle) =
build_ready_batch(accumulator.as_ref(), cluster.clone(), table_path.clone())?;
let mut inflight = HashMap::new();
inflight.insert(1, vec![batch]);
sender.add_to_inflight_batches(&inflight);
let batch = inflight.remove(&1).unwrap().pop().unwrap();
sender.handle_write_batch_error(
batch,
FlussError::RequestTimeOut,
"timeout".to_string(),
)?;
let server = cluster.get_tablet_server(1).expect("server");
let nodes = HashSet::from([server.clone()]);
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 handle_write_batch_error_fails() -> Result<()> {
let table_path = Arc::new(TablePath::new("db".to_string(), "tbl".to_string()));
let cluster = build_cluster_arc(table_path.as_ref(), 1, 1);
let metadata = Arc::new(Metadata::new_for_test(cluster.clone()));
let accumulator = Arc::new(RecordAccumulator::new(Config::default()));
let sender = Sender::new(metadata, accumulator.clone(), 1024 * 1024, 1000, 1, 0);
let (batch, handle) = build_ready_batch(accumulator.as_ref(), cluster.clone(), table_path)?;
sender.handle_write_batch_error(
batch,
FlussError::InvalidTableException,
"invalid".to_string(),
)?;
let batch_result = handle.wait().await?;
assert!(matches!(
batch_result,
Err(broadcast::Error::WriteFailed { code, .. })
if code == FlussError::InvalidTableException.code()
));
Ok(())
}
#[tokio::test]
async fn handle_produce_response_duplicate_sequence_completes() -> Result<()> {
let table_path = Arc::new(TablePath::new("db".to_string(), "tbl".to_string()));
let cluster = build_cluster_arc(table_path.as_ref(), 1, 1);
let metadata = Arc::new(Metadata::new_for_test(cluster.clone()));
let accumulator = Arc::new(RecordAccumulator::new(Config::default()));
let sender = Sender::new(metadata, accumulator.clone(), 1024 * 1024, 1000, 1, 0);
let (batch, handle) = build_ready_batch(accumulator.as_ref(), cluster, table_path)?;
let request_buckets = vec![batch.table_bucket.clone()];
let mut records_by_bucket = HashMap::new();
records_by_bucket.insert(batch.table_bucket.clone(), batch);
let response = ProduceLogResponse {
buckets_resp: vec![PbProduceLogRespForBucket {
bucket_id: 0,
error_code: Some(FlussError::DuplicateSequenceException.code()),
error_message: Some("dup".to_string()),
..Default::default()
}],
};
sender
.handle_write_response(1, &request_buckets, &mut records_by_bucket, response)
.await?;
let batch_result = handle.wait().await?;
assert!(matches!(batch_result, Ok(())));
Ok(())
}
}