mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-08-18 12:08:22 +00:00
f692ac32f3
* test(mito2): isolate sequence publication barrier Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(mito2): trim sequence barrier test noise Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --------- Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
1060 lines
38 KiB
Rust
1060 lines
38 KiB
Rust
// Copyright 2023 Greptime Team
|
|
//
|
|
// Licensed 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.
|
|
|
|
//! Handling write requests.
|
|
|
|
use std::collections::{HashMap, HashSet, hash_map};
|
|
use std::sync::Arc;
|
|
|
|
use api::v1::OpType;
|
|
use common_telemetry::{debug, error};
|
|
use snafu::ensure;
|
|
use store_api::codec::PrimaryKeyEncoding;
|
|
use store_api::logstore::LogStore;
|
|
use store_api::storage::RegionId;
|
|
|
|
use crate::error::{
|
|
InvalidRequestSnafu, PartitionExprVersionMismatchSnafu, RegionNotFoundSnafu, RegionStateSnafu,
|
|
RejectWriteSnafu, Result,
|
|
};
|
|
use crate::metrics;
|
|
use crate::metrics::{
|
|
WRITE_REJECT_TOTAL, WRITE_ROWS_TOTAL, WRITE_STAGE_ELAPSED, WRITE_STALL_TOTAL,
|
|
};
|
|
use crate::region::{RegionLeaderState, RegionRoleState};
|
|
use crate::region_write_ctx::RegionWriteCtx;
|
|
use crate::request::{SenderBulkRequest, SenderWriteRequest, WriteRequest};
|
|
use crate::wal::Wal;
|
|
use crate::worker::RegionWorkerLoop;
|
|
|
|
impl<S: LogStore> RegionWorkerLoop<S> {
|
|
/// Takes and handles all write requests.
|
|
pub(crate) async fn handle_write_requests(
|
|
&mut self,
|
|
write_requests: &mut Vec<SenderWriteRequest>,
|
|
bulk_requests: &mut Vec<SenderBulkRequest>,
|
|
allow_stall: bool,
|
|
) {
|
|
if write_requests.is_empty() && bulk_requests.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let write_region_ids = write_region_ids(write_requests, bulk_requests);
|
|
|
|
// Check region pressure before writes to match the global write buffer behavior.
|
|
self.maybe_flush_worker();
|
|
let pressure = self.maybe_flush_write_regions(write_region_ids);
|
|
|
|
if self.should_reject_write() {
|
|
// The memory pressure is still too high, reject write requests.
|
|
reject_write_requests(write_requests, bulk_requests);
|
|
// Also reject all stalled requests.
|
|
self.reject_stalled_requests();
|
|
return;
|
|
}
|
|
|
|
if !pressure.rejected_region_ids.is_empty() {
|
|
reject_region_write_requests(
|
|
&pressure.rejected_region_ids,
|
|
write_requests,
|
|
bulk_requests,
|
|
);
|
|
for region_id in &pressure.rejected_region_ids {
|
|
self.reject_region_stalled_requests(region_id);
|
|
}
|
|
if write_requests.is_empty() && bulk_requests.is_empty() {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if self.write_buffer_manager.should_stall() && allow_stall {
|
|
let stalled_count = (write_requests.len() + bulk_requests.len()) as i64;
|
|
self.stalling_count.add(stalled_count);
|
|
WRITE_STALL_TOTAL.inc_by(stalled_count as u64);
|
|
self.stalled_requests.append(write_requests, bulk_requests);
|
|
self.listener.on_write_stall();
|
|
return;
|
|
}
|
|
|
|
if allow_stall {
|
|
self.stall_region_write_requests(
|
|
&pressure.stalled_region_ids,
|
|
write_requests,
|
|
bulk_requests,
|
|
);
|
|
if write_requests.is_empty() && bulk_requests.is_empty() {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Prepare write context.
|
|
let mut region_ctxs = {
|
|
let _timer = WRITE_STAGE_ELAPSED
|
|
.with_label_values(&["prepare_ctx"])
|
|
.start_timer();
|
|
self.prepare_region_write_ctx(write_requests, bulk_requests)
|
|
};
|
|
|
|
// Write to WAL.
|
|
{
|
|
let _timer = WRITE_STAGE_ELAPSED
|
|
.with_label_values(&["write_wal"])
|
|
.start_timer();
|
|
if !write_wal(&self.wal, &mut region_ctxs).await {
|
|
// Failed to write to the WAL, all waiters are notified with the error.
|
|
return;
|
|
}
|
|
}
|
|
|
|
let (mut put_rows, mut delete_rows) = (0, 0);
|
|
// Write to memtables.
|
|
{
|
|
let _timer = WRITE_STAGE_ELAPSED
|
|
.with_label_values(&["write_memtable"])
|
|
.start_timer();
|
|
if region_ctxs.len() == 1 {
|
|
// fast path for single region.
|
|
let mut region_ctx = region_ctxs.into_values().next().unwrap();
|
|
region_ctx.write_memtable().await;
|
|
region_ctx.write_bulk().await;
|
|
region_ctx.publish_sequence_and_entry_id();
|
|
put_rows += region_ctx.put_num;
|
|
delete_rows += region_ctx.delete_num;
|
|
} else {
|
|
let region_write_task = region_ctxs
|
|
.into_values()
|
|
.map(|mut region_ctx| {
|
|
// use tokio runtime to schedule tasks.
|
|
common_runtime::spawn_global(async move {
|
|
region_ctx.write_memtable().await;
|
|
region_ctx.write_bulk().await;
|
|
region_ctx.publish_sequence_and_entry_id();
|
|
(region_ctx.put_num, region_ctx.delete_num)
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
for result in futures::future::join_all(region_write_task).await {
|
|
match result {
|
|
Ok((put, delete)) => {
|
|
put_rows += put;
|
|
delete_rows += delete;
|
|
}
|
|
Err(e) => {
|
|
error!(e; "unexpected error when joining region write tasks");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
WRITE_ROWS_TOTAL
|
|
.with_label_values(&["put"])
|
|
.inc_by(put_rows as u64);
|
|
WRITE_ROWS_TOTAL
|
|
.with_label_values(&["delete"])
|
|
.inc_by(delete_rows as u64);
|
|
}
|
|
|
|
/// Handles stalled write requests whose regions no longer need to stall.
|
|
pub(crate) async fn handle_stalled_requests(&mut self) {
|
|
let region_ids = self
|
|
.stalled_requests
|
|
.requests
|
|
.keys()
|
|
.copied()
|
|
.collect::<HashSet<_>>();
|
|
let pressure = self.maybe_flush_write_regions(region_ids);
|
|
for region_id in &pressure.rejected_region_ids {
|
|
self.reject_region_stalled_requests(region_id);
|
|
}
|
|
let ready_region_ids = self
|
|
.stalled_requests
|
|
.requests
|
|
.keys()
|
|
.filter(|region_id| !pressure.stalled_region_ids.contains(region_id))
|
|
.copied()
|
|
.collect::<Vec<_>>();
|
|
|
|
// These requests have already been stalled. Retry ready regions without stalling the
|
|
// same requests again. Regions that still exceed their limit remain in the queue until
|
|
// their own flush releases the pressure.
|
|
for region_id in ready_region_ids {
|
|
self.handle_region_stalled_requests(®ion_id, false).await;
|
|
}
|
|
}
|
|
|
|
/// Rejects all stalled requests.
|
|
pub(crate) fn reject_stalled_requests(&mut self) {
|
|
let stalled = std::mem::take(&mut self.stalled_requests);
|
|
self.stalling_count.sub(stalled.stalled_count() as i64);
|
|
for (_, (_, mut requests, mut bulk)) in stalled.requests {
|
|
reject_write_requests(&mut requests, &mut bulk);
|
|
}
|
|
}
|
|
|
|
/// Rejects a specific region's stalled requests.
|
|
pub(crate) fn reject_region_stalled_requests(&mut self, region_id: &RegionId) {
|
|
debug!("Rejects stalled requests for region {}", region_id);
|
|
let (mut requests, mut bulk) = self.stalled_requests.remove(region_id);
|
|
self.stalling_count
|
|
.sub((requests.len() + bulk.len()) as i64);
|
|
reject_write_requests(&mut requests, &mut bulk);
|
|
}
|
|
|
|
/// Fails a specific region's stalled requests if the region no longer exists.
|
|
pub(crate) fn fail_region_stalled_requests_as_not_found(&mut self, region_id: &RegionId) {
|
|
debug!(
|
|
"Fails stalled requests for region {} as region not found",
|
|
region_id
|
|
);
|
|
let (requests, bulk) = self.stalled_requests.remove(region_id);
|
|
self.stalling_count
|
|
.sub((requests.len() + bulk.len()) as i64);
|
|
|
|
for req in requests {
|
|
req.sender.send(
|
|
RegionNotFoundSnafu {
|
|
region_id: req.request.region_id,
|
|
}
|
|
.fail(),
|
|
);
|
|
}
|
|
for req in bulk {
|
|
req.sender.send(
|
|
RegionNotFoundSnafu {
|
|
region_id: req.region_id,
|
|
}
|
|
.fail(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Handles a specific region's stalled requests.
|
|
///
|
|
/// `allow_stall` should be false for backpressure retry paths to avoid stalling the same
|
|
/// requests again. It should remain true for non-backpressure retries, such as requests stalled
|
|
/// by alter, staging, and region editing. Global reject backpressure still applies before the
|
|
/// stall check.
|
|
pub(crate) async fn handle_region_stalled_requests(
|
|
&mut self,
|
|
region_id: &RegionId,
|
|
allow_stall: bool,
|
|
) {
|
|
debug!("Handles stalled requests for region {}", region_id);
|
|
let (mut requests, mut bulk) = self.stalled_requests.remove(region_id);
|
|
self.stalling_count
|
|
.sub((requests.len() + bulk.len()) as i64);
|
|
self.handle_write_requests(&mut requests, &mut bulk, allow_stall)
|
|
.await;
|
|
}
|
|
|
|
/// Processes same-batch writes for a region before handling its edit-completion notification.
|
|
///
|
|
/// The worker dispatch loop handles background notifications before the current batch's write
|
|
/// buffer. Without this step, writes that arrived during edit N could be classified only after
|
|
/// edit N+1 is started, placing them behind that next edit.
|
|
pub(crate) async fn handle_buffered_region_write_requests(
|
|
&mut self,
|
|
region_id: &RegionId,
|
|
write_requests: &mut Vec<SenderWriteRequest>,
|
|
bulk_requests: &mut Vec<SenderBulkRequest>,
|
|
) {
|
|
let mut current_region_write_requests = write_requests
|
|
.extract_if(.., |r| r.request.region_id == *region_id)
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut current_region_bulk_requests = bulk_requests
|
|
.extract_if(.., |r| r.region_id == *region_id)
|
|
.collect::<Vec<_>>();
|
|
|
|
self.handle_write_requests(
|
|
&mut current_region_write_requests,
|
|
&mut current_region_bulk_requests,
|
|
true,
|
|
)
|
|
.await;
|
|
}
|
|
}
|
|
|
|
impl<S> RegionWorkerLoop<S> {
|
|
/// Validates and groups requests by region.
|
|
fn prepare_region_write_ctx(
|
|
&mut self,
|
|
write_requests: &mut Vec<SenderWriteRequest>,
|
|
bulk_requests: &mut Vec<SenderBulkRequest>,
|
|
) -> HashMap<RegionId, RegionWriteCtx> {
|
|
// Initialize region write context map.
|
|
let mut region_ctxs = HashMap::new();
|
|
self.process_write_requests(&mut region_ctxs, write_requests);
|
|
self.process_bulk_requests(&mut region_ctxs, bulk_requests);
|
|
region_ctxs
|
|
}
|
|
|
|
fn process_write_requests(
|
|
&mut self,
|
|
region_ctxs: &mut HashMap<RegionId, RegionWriteCtx>,
|
|
write_requests: &mut Vec<SenderWriteRequest>,
|
|
) {
|
|
for mut sender_req in write_requests.drain(..) {
|
|
let region_id = sender_req.request.region_id;
|
|
|
|
// If region is waiting for alteration, add requests to pending writes.
|
|
if self.flush_scheduler.has_pending_ddls(region_id) {
|
|
// TODO(yingwen): consider adding some metrics for this.
|
|
// Safety: The region has pending ddls.
|
|
self.flush_scheduler
|
|
.add_write_request_to_pending(sender_req);
|
|
continue;
|
|
}
|
|
|
|
// Checks whether the region exists and is it stalling.
|
|
if let hash_map::Entry::Vacant(e) = region_ctxs.entry(region_id) {
|
|
let Some(region) = self
|
|
.regions
|
|
.get_region_or(region_id, &mut sender_req.sender)
|
|
else {
|
|
// No such region.
|
|
continue;
|
|
};
|
|
#[cfg(test)]
|
|
debug!(
|
|
"Handling write request for region {}, state: {:?}",
|
|
region_id,
|
|
region.state()
|
|
);
|
|
match region.state() {
|
|
RegionRoleState::Leader(RegionLeaderState::Writable)
|
|
| RegionRoleState::Leader(RegionLeaderState::Staging) => {
|
|
if region.reject_all_writes_in_staging() {
|
|
sender_req
|
|
.sender
|
|
.send(RejectWriteSnafu { region_id }.fail());
|
|
continue;
|
|
}
|
|
|
|
let region_ctx = RegionWriteCtx::new(
|
|
region.region_id,
|
|
®ion.version_control,
|
|
region.provider.clone(),
|
|
Some(region.region_stats.written_bytes.clone()),
|
|
);
|
|
|
|
e.insert(region_ctx);
|
|
}
|
|
RegionRoleState::Leader(RegionLeaderState::Altering)
|
|
| RegionRoleState::Leader(RegionLeaderState::Editing) => {
|
|
// Editing is transient: queue the write so edit completion can drain it
|
|
// before starting the next queued edit.
|
|
debug!(
|
|
"Region {} is {:?}, add request to pending writes",
|
|
region.region_id,
|
|
region.state()
|
|
);
|
|
self.stalling_count.add(1);
|
|
WRITE_STALL_TOTAL.inc();
|
|
self.stalled_requests.push(sender_req);
|
|
continue;
|
|
}
|
|
RegionRoleState::Leader(RegionLeaderState::EnteringStaging) => {
|
|
debug!(
|
|
"Region {} is entering staging, add request to pending writes",
|
|
region.region_id
|
|
);
|
|
self.stalling_count.add(1);
|
|
WRITE_STALL_TOTAL.inc();
|
|
self.stalled_requests.push(sender_req);
|
|
continue;
|
|
}
|
|
state => {
|
|
// The region is not writable.
|
|
sender_req.sender.send(
|
|
RegionStateSnafu {
|
|
region_id,
|
|
state,
|
|
expect: RegionRoleState::Leader(RegionLeaderState::Writable),
|
|
}
|
|
.fail(),
|
|
);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Safety: Now we ensure the region exists.
|
|
let region_ctx = region_ctxs.get_mut(®ion_id).unwrap();
|
|
let Some(region) = self
|
|
.regions
|
|
.get_region_or(region_id, &mut sender_req.sender)
|
|
else {
|
|
continue;
|
|
};
|
|
if region.reject_all_writes_in_staging() {
|
|
sender_req
|
|
.sender
|
|
.send(RejectWriteSnafu { region_id }.fail());
|
|
continue;
|
|
}
|
|
let expected_version = region.expected_partition_expr_version();
|
|
if let Err(e) = check_partition_expr_version(
|
|
region_id,
|
|
expected_version,
|
|
sender_req.request.partition_expr_version,
|
|
) {
|
|
sender_req.sender.send(Err(e));
|
|
continue;
|
|
}
|
|
|
|
if let Err(e) = check_op_type(
|
|
region_ctx.version().options.append_mode,
|
|
&sender_req.request,
|
|
) {
|
|
// Do not allow non-put op under append mode.
|
|
sender_req.sender.send(Err(e));
|
|
|
|
continue;
|
|
}
|
|
|
|
// Double check the request schema
|
|
let need_fill_missing_columns =
|
|
if let Some(ref region_metadata) = sender_req.request.region_metadata {
|
|
region_ctx.version().metadata.schema_version != region_metadata.schema_version
|
|
} else {
|
|
true
|
|
};
|
|
// Only fill missing columns if primary key is dense encoded.
|
|
if need_fill_missing_columns
|
|
&& sender_req.request.primary_key_encoding() == PrimaryKeyEncoding::Dense
|
|
&& let Err(e) = sender_req
|
|
.request
|
|
.maybe_fill_missing_columns(®ion_ctx.version().metadata)
|
|
{
|
|
sender_req.sender.send(Err(e));
|
|
|
|
continue;
|
|
}
|
|
|
|
// Collect requests by region.
|
|
region_ctx.push_mutation(
|
|
sender_req.request.op_type as i32,
|
|
Some(sender_req.request.rows),
|
|
sender_req.request.hint,
|
|
sender_req.sender,
|
|
None,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Processes bulk insert requests.
|
|
fn process_bulk_requests(
|
|
&mut self,
|
|
region_ctxs: &mut HashMap<RegionId, RegionWriteCtx>,
|
|
requests: &mut Vec<SenderBulkRequest>,
|
|
) {
|
|
let _timer = metrics::REGION_WORKER_HANDLE_WRITE_ELAPSED
|
|
.with_label_values(&["prepare_bulk_request"])
|
|
.start_timer();
|
|
for mut bulk_req in requests.drain(..) {
|
|
let region_id = bulk_req.region_id;
|
|
// If region is waiting for alteration, add requests to pending writes.
|
|
if self.flush_scheduler.has_pending_ddls(region_id) {
|
|
// Safety: The region has pending ddls.
|
|
self.flush_scheduler.add_bulk_request_to_pending(bulk_req);
|
|
continue;
|
|
}
|
|
|
|
// Checks whether the region exists and is it stalling.
|
|
if let hash_map::Entry::Vacant(e) = region_ctxs.entry(region_id) {
|
|
let Some(region) = self.regions.get_region_or(region_id, &mut bulk_req.sender)
|
|
else {
|
|
continue;
|
|
};
|
|
match region.state() {
|
|
RegionRoleState::Leader(RegionLeaderState::Writable)
|
|
| RegionRoleState::Leader(RegionLeaderState::Staging) => {
|
|
if region.reject_all_writes_in_staging() {
|
|
bulk_req.sender.send(RejectWriteSnafu { region_id }.fail());
|
|
continue;
|
|
}
|
|
let region_ctx = RegionWriteCtx::new(
|
|
region.region_id,
|
|
®ion.version_control,
|
|
region.provider.clone(),
|
|
Some(region.region_stats.written_bytes.clone()),
|
|
);
|
|
|
|
e.insert(region_ctx);
|
|
}
|
|
RegionRoleState::Leader(RegionLeaderState::Altering)
|
|
| RegionRoleState::Leader(RegionLeaderState::Editing) => {
|
|
// Editing is transient: queue the bulk write so edit completion can drain
|
|
// it before starting the next queued edit.
|
|
debug!(
|
|
"Region {} is {:?}, add request to pending writes",
|
|
region.region_id,
|
|
region.state()
|
|
);
|
|
self.stalling_count.add(1);
|
|
WRITE_STALL_TOTAL.inc();
|
|
self.stalled_requests.push_bulk(bulk_req);
|
|
continue;
|
|
}
|
|
state => {
|
|
// The region is not writable.
|
|
bulk_req.sender.send(
|
|
RegionStateSnafu {
|
|
region_id,
|
|
state,
|
|
expect: RegionRoleState::Leader(RegionLeaderState::Writable),
|
|
}
|
|
.fail(),
|
|
);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Safety: Now we ensure the region exists.
|
|
let region_ctx = region_ctxs.get_mut(®ion_id).unwrap();
|
|
let Some(region) = self.regions.get_region_or(region_id, &mut bulk_req.sender) else {
|
|
continue;
|
|
};
|
|
if region.reject_all_writes_in_staging() {
|
|
bulk_req.sender.send(RejectWriteSnafu { region_id }.fail());
|
|
continue;
|
|
}
|
|
let expected_version = region.expected_partition_expr_version();
|
|
if let Err(e) = check_partition_expr_version(
|
|
region_id,
|
|
expected_version,
|
|
bulk_req.partition_expr_version,
|
|
) {
|
|
bulk_req.sender.send(Err(e));
|
|
continue;
|
|
}
|
|
|
|
// Double-check the request schema
|
|
let need_fill_missing_columns =
|
|
!bulk_req.region_metadata.is_some_and(|aligned_schema| {
|
|
aligned_schema.schema_version == region_ctx.version().metadata.schema_version
|
|
});
|
|
|
|
// Fill missing columns if needed
|
|
if need_fill_missing_columns
|
|
&& let Err(e) = bulk_req
|
|
.request
|
|
.fill_missing_columns(®ion_ctx.version().metadata)
|
|
{
|
|
bulk_req.sender.send(Err(e));
|
|
continue;
|
|
}
|
|
|
|
// Collect requests by region.
|
|
if !region_ctx.push_bulk(bulk_req.sender, bulk_req.request, None) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns true if the engine needs to reject some write requests.
|
|
pub(crate) fn should_reject_write(&self) -> bool {
|
|
// If memory usage reaches high threshold (we should also consider stalled requests) returns true.
|
|
self.write_buffer_manager.memory_usage() + self.stalled_requests.estimated_size
|
|
>= self.config.global_write_buffer_reject_size.as_bytes() as usize
|
|
}
|
|
|
|
fn stall_region_write_requests(
|
|
&mut self,
|
|
stalled_region_ids: &HashSet<RegionId>,
|
|
write_requests: &mut Vec<SenderWriteRequest>,
|
|
bulk_requests: &mut Vec<SenderBulkRequest>,
|
|
) {
|
|
let mut stalled_count = 0;
|
|
let mut stalled_write_requests = write_requests
|
|
.extract_if(.., |req| {
|
|
stalled_region_ids.contains(&req.request.region_id)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut stalled_bulk_requests = bulk_requests
|
|
.extract_if(.., |req| stalled_region_ids.contains(&req.region_id))
|
|
.collect::<Vec<_>>();
|
|
|
|
stalled_count += stalled_write_requests.len() + stalled_bulk_requests.len();
|
|
self.stalled_requests
|
|
.append(&mut stalled_write_requests, &mut stalled_bulk_requests);
|
|
|
|
if stalled_count > 0 {
|
|
let stalled_count = stalled_count as i64;
|
|
self.stalling_count.add(stalled_count);
|
|
WRITE_STALL_TOTAL.inc_by(stalled_count as u64);
|
|
self.listener.on_write_stall();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Writes WAL entries of all region contexts to the WAL in one batch and updates
|
|
/// the next entry id of each region on success.
|
|
///
|
|
/// Returns `false` if the batch fails to be written to the WAL. In this case all
|
|
/// contexts are consumed and their waiters are notified with the error, so the
|
|
/// caller should skip the memtable phase.
|
|
async fn write_wal<S: LogStore>(
|
|
wal: &Wal<S>,
|
|
region_ctxs: &mut HashMap<RegionId, RegionWriteCtx>,
|
|
) -> bool {
|
|
let mut wal_writer = wal.writer();
|
|
for region_ctx in region_ctxs.values_mut() {
|
|
if region_ctx.skip_wal() {
|
|
continue;
|
|
}
|
|
if let Err(e) = region_ctx.add_wal_entry(&mut wal_writer).map_err(Arc::new) {
|
|
region_ctx.set_error(e);
|
|
}
|
|
}
|
|
match wal_writer.write_to_wal().await.map_err(Arc::new) {
|
|
Ok(response) => {
|
|
for (region_id, region_ctx) in region_ctxs.iter_mut() {
|
|
if region_ctx.skip_wal() {
|
|
continue;
|
|
}
|
|
// The entry of a failed region (e.g. failed to build its WAL entry) is
|
|
// not in the batch so the response has no last entry id for it. Its
|
|
// waiters are already notified with the error.
|
|
if region_ctx.is_failed() {
|
|
continue;
|
|
}
|
|
|
|
// Safety: the log store implementation ensures that either the `write_to_wal` fails and no
|
|
// response is returned or the last entry ids for each region in the batch do exist.
|
|
let last_entry_id = response.last_entry_ids.get(region_id).unwrap();
|
|
region_ctx.set_next_entry_id(last_entry_id + 1);
|
|
}
|
|
true
|
|
}
|
|
Err(e) => {
|
|
// Failed to write wal.
|
|
for (_, mut region_ctx) in region_ctxs.drain() {
|
|
region_ctx.set_error(e.clone());
|
|
}
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Send rejected error to all `write_requests`.
|
|
fn reject_write_requests(
|
|
write_requests: &mut Vec<SenderWriteRequest>,
|
|
bulk_requests: &mut Vec<SenderBulkRequest>,
|
|
) {
|
|
WRITE_REJECT_TOTAL.inc_by(write_requests.len() as u64);
|
|
|
|
for req in write_requests.drain(..) {
|
|
req.sender.send(
|
|
RejectWriteSnafu {
|
|
region_id: req.request.region_id,
|
|
}
|
|
.fail(),
|
|
);
|
|
}
|
|
for req in bulk_requests.drain(..) {
|
|
let region_id = req.region_id;
|
|
req.sender.send(RejectWriteSnafu { region_id }.fail());
|
|
}
|
|
}
|
|
|
|
fn reject_region_write_requests(
|
|
rejected_region_ids: &HashSet<RegionId>,
|
|
write_requests: &mut Vec<SenderWriteRequest>,
|
|
bulk_requests: &mut Vec<SenderBulkRequest>,
|
|
) {
|
|
let mut rejected_write_requests = write_requests
|
|
.extract_if(.., |req| {
|
|
rejected_region_ids.contains(&req.request.region_id)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut rejected_bulk_requests = bulk_requests
|
|
.extract_if(.., |req| rejected_region_ids.contains(&req.region_id))
|
|
.collect::<Vec<_>>();
|
|
reject_write_requests(&mut rejected_write_requests, &mut rejected_bulk_requests);
|
|
}
|
|
|
|
fn write_region_ids(
|
|
write_requests: &[SenderWriteRequest],
|
|
bulk_requests: &[SenderBulkRequest],
|
|
) -> HashSet<RegionId> {
|
|
write_requests
|
|
.iter()
|
|
.map(|req| req.request.region_id)
|
|
.chain(bulk_requests.iter().map(|req| req.region_id))
|
|
.collect()
|
|
}
|
|
|
|
/// Rejects delete request under append mode.
|
|
fn check_op_type(append_mode: bool, request: &WriteRequest) -> Result<()> {
|
|
if append_mode {
|
|
ensure!(
|
|
request.op_type == OpType::Put,
|
|
InvalidRequestSnafu {
|
|
region_id: request.region_id,
|
|
reason: "DELETE is not allowed under append mode",
|
|
}
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn check_partition_expr_version(
|
|
region_id: RegionId,
|
|
expected_version: u64,
|
|
request_version: Option<u64>,
|
|
) -> Result<()> {
|
|
let request_version = match request_version {
|
|
None => return Ok(()),
|
|
Some(value) => value,
|
|
};
|
|
if request_version != expected_version {
|
|
return PartitionExprVersionMismatchSnafu {
|
|
region_id,
|
|
request_version,
|
|
expected_version,
|
|
}
|
|
.fail();
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use api::v1::helper::{tag_column_schema, time_index_column_schema};
|
|
use api::v1::value::ValueData;
|
|
use api::v1::{ColumnDataType, Row, Rows};
|
|
use common_recordbatch::DfRecordBatch;
|
|
use datatypes::arrow::array::{ArrayRef, StringArray, TimestampMillisecondArray};
|
|
use datatypes::arrow::datatypes::{DataType, Field, Schema};
|
|
use futures::stream;
|
|
use log_store::error::{
|
|
Error as LogStoreError, IllegalStateSnafu, InvalidProviderSnafu, Result as LogStoreResult,
|
|
};
|
|
use store_api::logstore::entry::{Entry, NaiveEntry};
|
|
use store_api::logstore::provider::Provider;
|
|
use store_api::logstore::{AppendBatchResponse, EntryId, SendableEntryStream, WalIndex};
|
|
use store_api::region_request::AffectedRows;
|
|
use tokio::sync::oneshot;
|
|
|
|
use super::*;
|
|
use crate::memtable::bulk::part::BulkPart;
|
|
use crate::request::OptionOutputTx;
|
|
use crate::test_util::ts_ms_value;
|
|
use crate::test_util::version_util::VersionControlBuilder;
|
|
|
|
fn new_bulk_part(num_rows: i64) -> BulkPart {
|
|
let schema = Arc::new(Schema::new(vec![
|
|
Field::new("tag_0", DataType::Utf8, true),
|
|
Field::new(
|
|
"ts",
|
|
DataType::Timestamp(datatypes::arrow::datatypes::TimeUnit::Millisecond, None),
|
|
false,
|
|
),
|
|
]));
|
|
let tag = Arc::new(StringArray::from_iter_values(
|
|
(0..num_rows).map(|value| value.to_string()),
|
|
)) as ArrayRef;
|
|
let ts = Arc::new(TimestampMillisecondArray::from(
|
|
(0..num_rows).collect::<Vec<_>>(),
|
|
)) as ArrayRef;
|
|
let batch = DfRecordBatch::try_new(schema, vec![tag, ts]).unwrap();
|
|
|
|
BulkPart {
|
|
batch,
|
|
max_timestamp: num_rows - 1,
|
|
min_timestamp: 0,
|
|
sequence: 0,
|
|
timestamp_index: 1,
|
|
raw_data: None,
|
|
}
|
|
}
|
|
|
|
/// A log store that fails to build entries for `failing_region` and fails the
|
|
/// whole batch when `fail_append` is true.
|
|
#[derive(Debug, Default)]
|
|
struct MockLogStore {
|
|
failing_region: Option<RegionId>,
|
|
fail_append: bool,
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl LogStore for MockLogStore {
|
|
type Error = LogStoreError;
|
|
|
|
async fn stop(&self) -> LogStoreResult<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn append_batch(&self, entries: Vec<Entry>) -> LogStoreResult<AppendBatchResponse> {
|
|
if self.fail_append {
|
|
return IllegalStateSnafu {}.fail();
|
|
}
|
|
let mut last_entry_ids = HashMap::new();
|
|
for entry in &entries {
|
|
let last_entry_id = last_entry_ids.entry(entry.region_id()).or_insert(0);
|
|
*last_entry_id = entry.entry_id().max(*last_entry_id);
|
|
}
|
|
Ok(AppendBatchResponse { last_entry_ids })
|
|
}
|
|
|
|
async fn read(
|
|
&self,
|
|
_provider: &Provider,
|
|
_entry_id: EntryId,
|
|
_index: Option<WalIndex>,
|
|
) -> LogStoreResult<SendableEntryStream<'static, Entry, Self::Error>> {
|
|
Ok(Box::pin(stream::empty()))
|
|
}
|
|
|
|
async fn create_namespace(&self, _ns: &Provider) -> LogStoreResult<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn delete_namespace(&self, _ns: &Provider) -> LogStoreResult<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn list_namespaces(&self) -> LogStoreResult<Vec<Provider>> {
|
|
Ok(vec![])
|
|
}
|
|
|
|
async fn obsolete(
|
|
&self,
|
|
_provider: &Provider,
|
|
_region_id: RegionId,
|
|
_entry_id: EntryId,
|
|
) -> LogStoreResult<()> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn obsolete_all(
|
|
&self,
|
|
_provider: &Provider,
|
|
_region_id: RegionId,
|
|
) -> LogStoreResult<()> {
|
|
Ok(())
|
|
}
|
|
|
|
fn entry(
|
|
&self,
|
|
data: Vec<u8>,
|
|
entry_id: EntryId,
|
|
region_id: RegionId,
|
|
provider: &Provider,
|
|
) -> LogStoreResult<Entry> {
|
|
if self.failing_region == Some(region_id) {
|
|
return InvalidProviderSnafu {
|
|
expected: "raft_engine",
|
|
actual: "mock",
|
|
}
|
|
.fail();
|
|
}
|
|
Ok(Entry::Naive(NaiveEntry {
|
|
provider: provider.clone(),
|
|
region_id,
|
|
entry_id,
|
|
data,
|
|
}))
|
|
}
|
|
|
|
fn latest_entry_id(&self, _provider: &Provider) -> LogStoreResult<EntryId> {
|
|
Ok(0)
|
|
}
|
|
}
|
|
|
|
fn new_region_ctx(
|
|
region_id: RegionId,
|
|
) -> (RegionWriteCtx, oneshot::Receiver<Result<AffectedRows>>) {
|
|
let version_control = Arc::new(VersionControlBuilder::new().build());
|
|
let mut ctx = RegionWriteCtx::new(
|
|
region_id,
|
|
&version_control,
|
|
Provider::raft_engine_provider(region_id.as_u64()),
|
|
None,
|
|
);
|
|
let (tx, rx) = oneshot::channel();
|
|
ctx.push_mutation(
|
|
OpType::Put as i32,
|
|
Some(Rows {
|
|
schema: vec![
|
|
time_index_column_schema("ts", ColumnDataType::TimestampMillisecond),
|
|
tag_column_schema("tag_0", ColumnDataType::String),
|
|
],
|
|
rows: vec![Row {
|
|
values: vec![
|
|
ts_ms_value(0),
|
|
api::v1::Value {
|
|
value_data: Some(ValueData::StringValue("a".to_string())),
|
|
},
|
|
],
|
|
}],
|
|
}),
|
|
None,
|
|
OptionOutputTx::from(tx),
|
|
None,
|
|
);
|
|
(ctx, rx)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_write_wal_skips_region_failed_to_build_entry() {
|
|
let failing_region = RegionId::new(1, 1);
|
|
let ok_region = RegionId::new(1, 2);
|
|
let wal = Wal::new(Arc::new(MockLogStore {
|
|
failing_region: Some(failing_region),
|
|
..Default::default()
|
|
}));
|
|
|
|
let mut region_ctxs = HashMap::new();
|
|
let (ctx, failing_rx) = new_region_ctx(failing_region);
|
|
let failing_committed_sequence = ctx.version_control().committed_sequence();
|
|
region_ctxs.insert(failing_region, ctx);
|
|
let (ctx, ok_rx) = new_region_ctx(ok_region);
|
|
let ok_committed_sequence = ctx.version_control().committed_sequence();
|
|
region_ctxs.insert(ok_region, ctx);
|
|
let entry_id = region_ctxs[&ok_region].next_entry_id();
|
|
|
|
// The failed region must not fail the batch.
|
|
assert!(write_wal(&wal, &mut region_ctxs).await);
|
|
|
|
assert!(region_ctxs[&failing_region].is_failed());
|
|
assert!(!region_ctxs[&ok_region].is_failed());
|
|
assert_eq!(entry_id + 1, region_ctxs[&ok_region].next_entry_id());
|
|
|
|
for region_ctx in region_ctxs.values_mut() {
|
|
region_ctx.write_memtable().await;
|
|
region_ctx.write_bulk().await;
|
|
region_ctx.publish_sequence_and_entry_id();
|
|
}
|
|
|
|
assert_eq!(
|
|
failing_committed_sequence,
|
|
region_ctxs[&failing_region]
|
|
.version_control()
|
|
.committed_sequence()
|
|
);
|
|
assert_eq!(
|
|
ok_committed_sequence + 1,
|
|
region_ctxs[&ok_region]
|
|
.version_control()
|
|
.committed_sequence()
|
|
);
|
|
|
|
drop(region_ctxs);
|
|
assert!(failing_rx.await.unwrap().is_err());
|
|
assert_eq!(1, ok_rx.await.unwrap().unwrap());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bulk_write_sequence_not_committed_before_install_worker_level() {
|
|
let region_id = RegionId::new(1, 1);
|
|
let version_control = Arc::new(VersionControlBuilder::new().build());
|
|
|
|
let mut region_ctxs = HashMap::new();
|
|
let mut ctx = RegionWriteCtx::new(
|
|
region_id,
|
|
&version_control,
|
|
Provider::raft_engine_provider(region_id.as_u64()),
|
|
None,
|
|
);
|
|
let (tx, rx) = oneshot::channel();
|
|
assert!(ctx.push_bulk(OptionOutputTx::from(tx), new_bulk_part(3), None));
|
|
region_ctxs.insert(region_id, ctx);
|
|
|
|
let wal = Wal::new(Arc::new(MockLogStore::default()));
|
|
assert!(write_wal(&wal, &mut region_ctxs).await);
|
|
assert!(!region_ctxs[®ion_id].is_failed());
|
|
|
|
let mut barrier = crate::region_write_ctx::test_hooks::arm_bulk_install_barrier(
|
|
region_id,
|
|
version_control.clone(),
|
|
);
|
|
|
|
let write_handle = tokio::spawn(async move {
|
|
let mut region_ctx = region_ctxs.remove(®ion_id).unwrap();
|
|
region_ctx.write_memtable().await;
|
|
region_ctx.write_bulk().await;
|
|
region_ctx.publish_sequence_and_entry_id();
|
|
});
|
|
|
|
tokio::time::timeout(
|
|
std::time::Duration::from_secs(10),
|
|
barrier.wait_until_reached(),
|
|
)
|
|
.await
|
|
.expect("bulk write never reached the install barrier");
|
|
|
|
assert_eq!(
|
|
0,
|
|
version_control.committed_sequence(),
|
|
"committed sequence leaked before the bulk part was installed"
|
|
);
|
|
|
|
barrier.release();
|
|
write_handle.await.expect("bulk write should complete");
|
|
assert_eq!(
|
|
3,
|
|
version_control.committed_sequence(),
|
|
"committed sequence must cover the installed bulk rows"
|
|
);
|
|
|
|
assert_eq!(3, rx.await.unwrap().unwrap());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_write_wal_all_regions_failed_to_build_entries() {
|
|
let failing_region = RegionId::new(1, 1);
|
|
let wal = Wal::new(Arc::new(MockLogStore {
|
|
failing_region: Some(failing_region),
|
|
..Default::default()
|
|
}));
|
|
|
|
let mut region_ctxs = HashMap::new();
|
|
let (ctx, rx) = new_region_ctx(failing_region);
|
|
region_ctxs.insert(failing_region, ctx);
|
|
|
|
// Writing an empty batch to the WAL succeeds, the failed region must not panic
|
|
// the worker.
|
|
assert!(write_wal(&wal, &mut region_ctxs).await);
|
|
|
|
assert!(region_ctxs[&failing_region].is_failed());
|
|
drop(region_ctxs);
|
|
assert!(rx.await.unwrap().is_err());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_write_wal_append_batch_failure() {
|
|
let region_id = RegionId::new(1, 1);
|
|
let wal = Wal::new(Arc::new(MockLogStore {
|
|
fail_append: true,
|
|
..Default::default()
|
|
}));
|
|
|
|
let mut region_ctxs = HashMap::new();
|
|
let (ctx, rx) = new_region_ctx(region_id);
|
|
region_ctxs.insert(region_id, ctx);
|
|
|
|
assert!(!write_wal(&wal, &mut region_ctxs).await);
|
|
|
|
// All contexts are consumed and waiters are notified with the error.
|
|
assert!(region_ctxs.is_empty());
|
|
assert!(rx.await.unwrap().is_err());
|
|
}
|
|
}
|