This is an automated email from the ASF dual-hosted git repository.
gavinchou pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 6f3af1a9d56 [enhance](load) Support lock-free load cancellation
(#67759)
6f3af1a9d56 is described below
commit 6f3af1a9d56a6ff2c70f4dabfd703f5e71f56451
Author: hui lai <[email protected]>
AuthorDate: Wed Oct 14 17:27:40 2026 +0800
[enhance](load) Support lock-free load cancellation (#67759)
### What problem does this PR solve?
Support lock-free load cancellation by publishing the load cancellation
status without acquiring channel or writer locks or synchronously
traversing writers. Previously, close could hold those locks while
waiting for flush or delete bitmap work, delaying cancellation.
A shared per-load `AtomicStatus` separates cancellation publication from
resource cleanup. Local/cloud write and close paths observe the status
at phase boundaries, and pending asynchronous EOS RPCs are completed
during final-owner cleanup. Flush and delete bitmap tasks cooperate with
that status; final owners drain running work before reclaiming
resources.
The same status is propagated through ordinary and incremental opens,
local/cloud writers, group children, rowset-writer contexts, and both
delete bitmap executors. Queued memtable flush tasks check it before
flushing. Bitmap tokens reject submissions when cancellation is
observable and check again when a queued callback is dispatched,
skipping the calculation after cancellation. Each callback owns its
submitter's resource context.
Final-owner cleanup first completes pending EOS RPC callbacks before
destroying writers. The final sender checks the shared cancellation
status before publishing EOS arrival and again before flushing;
successful arrival is not revoked by later cancellation. Cleanup then
stops and drains memtable flush producers, then cancels builder and
rowset-writer bitmap tokens. Token cancellation discards remaining
queued callbacks and waits for running callbacks before builder
rollback, cloud cache cleanup, or local file cleanup. This also covers
group children and initialization that created bitmap work before
memtable initialization failed. The local rowset writer drains bitmap
callbacks before its derived state is destroyed.
Earlier token calculation failures remain local to that token. A
callback failure observed after load cancellation preserves the
cancellation reason. Failed submissions recheck the effective status
instead of replacing cancellation with a generic thread-pool shutdown
error.
The existing manager changes retain cancellation reasons for failed
requests and explicit cancellation, protect cached failures from stale
successful EOS transitions, and compare channel instances before
removing mappings. Cached reasons preserve the original message,
including empty failure messages.
### Cancellation contract and limits
- **Cancel:** publish the shared status; no bitmap registry, token
traversal, or shutdown is added to this publication path.
- **Submit bitmap:** reject work when cancellation is observed.
- **Dispatch queued bitmap:** check the shared status and skip the
calculation.
- **Final-owner cleanup:** cancel tokens, discard pending callbacks, and
wait for running callbacks before reclaiming resources.
This does not proactively remove queued tasks when cancellation is
published, wake bitmap waiters immediately, or interrupt
calculations/I/O that have already started. A retained close can still
wait for queued callbacks to be dispatched. Final-owner destruction can
still extend cancel RPC completion. `tablet_writer_cancel` uses master's
dedicated load-light work pool. Batched queue removal is deferred.
Related: #67778. This PR integrates bitmap status checks and cleanup
with shared-status publication; it does not introduce the load-wide
synchronous token-shutdown coordinator.
### Release note
Load cancellation publishes a shared status without waiting for channel
or writer locks held by close. Queued flush and delete bitmap work
observes cancellation, new bitmap submissions are rejected, and final
writer cleanup discards pending bitmap callbacks while safely draining
running work.
---
be/src/cloud/cloud_delta_writer.cpp | 6 +-
be/src/cloud/cloud_rowset_builder.cpp | 13 +-
be/src/cloud/cloud_rowset_builder.h | 2 +
be/src/cloud/cloud_rowset_writer.cpp | 3 +-
be/src/cloud/cloud_tablets_channel.cpp | 1 +
be/src/load/channel/load_channel.cpp | 45 +-
be/src/load/channel/load_channel.h | 10 +-
be/src/load/channel/load_channel_mgr.cpp | 107 ++--
be/src/load/channel/load_channel_mgr.h | 7 +-
be/src/load/channel/tablets_channel.cpp | 25 +-
be/src/load/channel/tablets_channel.h | 9 +
be/src/load/delta_writer/delta_writer.cpp | 29 +-
be/src/load/delta_writer/delta_writer.h | 5 +-
be/src/load/delta_writer/delta_writer_context.h | 3 +
be/src/load/memtable/memtable_flush_executor.cpp | 15 +-
be/src/load/memtable/memtable_flush_executor.h | 17 +-
be/src/load/memtable/memtable_memory_limiter.cpp | 17 +-
be/src/load/memtable/memtable_writer.cpp | 26 +-
.../storage/delete/calc_delete_bitmap_executor.cpp | 71 ++-
.../storage/delete/calc_delete_bitmap_executor.h | 46 +-
be/src/storage/rowset/beta_rowset_writer.cpp | 25 +-
be/src/storage/rowset/beta_rowset_writer.h | 2 +
be/src/storage/rowset/rowset_writer.h | 3 +
be/src/storage/rowset/rowset_writer_context.h | 1 +
be/src/storage/rowset_builder.cpp | 18 +-
be/src/storage/rowset_builder.h | 4 +-
be/test/load/channel/load_channel_eos_test.cpp | 19 +-
be/test/load/channel/load_channel_mgr_test.cpp | 274 ++++++++++
.../delta_writer_cancel_integration_test.cpp | 591 +++++++++++++++++++++
.../load/delta_writer/delta_writer_cancel_test.cpp | 295 ++++++++++
.../load/memtable/memtable_flush_executor_test.cpp | 119 +++++
.../load/memtable/memtable_memory_limiter_test.cpp | 91 ++++
.../delete/calc_delete_bitmap_executor_test.cpp | 152 ++++++
33 files changed, 1905 insertions(+), 146 deletions(-)
diff --git a/be/src/cloud/cloud_delta_writer.cpp
b/be/src/cloud/cloud_delta_writer.cpp
index 19eaef0f00a..83553e176cd 100644
--- a/be/src/cloud/cloud_delta_writer.cpp
+++ b/be/src/cloud/cloud_delta_writer.cpp
@@ -93,7 +93,7 @@ Status CloudDeltaWriter::write(const Block* block, const
TabletAddRowsPayload& r
ExecEnv::GetInstance()->memtable_memory_limiter()->handle_table_memtable_backpressure(
[this]() {
std::lock_guard lock(_mtx);
- return _is_cancelled;
+ return _is_cancelled || !_get_load_cancel_status().ok();
},
table_id());
}
@@ -115,6 +115,7 @@ Status CloudDeltaWriter::write(const Block* block, const
TabletAddRowsPayload& r
return _memtable_writer->flush_running_count() >=
effective_flush_running_count_limit;
};
while (need_backpressure()) {
+ RETURN_IF_ERROR(_get_load_cancel_status());
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
@@ -154,6 +155,7 @@ void CloudDeltaWriter::update_tablet_stats() {
Status CloudDeltaWriter::commit_rowset() {
g_cloud_commit_rowset_count << 1;
std::lock_guard<bthread::Mutex> lock(_mtx);
+ RETURN_IF_ERROR(_get_load_cancel_status());
// Handle empty rowset (no data written)
if (!_is_init) {
@@ -173,6 +175,7 @@ Status CloudDeltaWriter::_commit_empty_rowset() {
RETURN_IF_ERROR(_rowset_builder->init());
RETURN_IF_ERROR(_rowset_builder->build_rowset());
+ RETURN_IF_ERROR(_get_load_cancel_status());
// If skip writing empty rowset metadata is enabled, we do not commit
rowset to meta service.
if (config::skip_writing_empty_rowset_metadata) {
@@ -183,6 +186,7 @@ Status CloudDeltaWriter::_commit_empty_rowset() {
}
Status CloudDeltaWriter::set_txn_related_info() {
+ RETURN_IF_ERROR(_get_load_cancel_status());
return rowset_builder()->set_txn_related_info();
}
diff --git a/be/src/cloud/cloud_rowset_builder.cpp
b/be/src/cloud/cloud_rowset_builder.cpp
index d9065c340ec..5660fb6bbb3 100644
--- a/be/src/cloud/cloud_rowset_builder.cpp
+++ b/be/src/cloud/cloud_rowset_builder.cpp
@@ -23,6 +23,7 @@
#include "cloud/cloud_storage_engine.h"
#include "cloud/cloud_tablet.h"
#include "cloud/cloud_tablet_mgr.h"
+#include "cpp/sync_point.h"
#include "io/fs/file_system.h"
#include "storage/rowset/group_rowset_writer.h"
#include "storage/rowset/rowset_factory.h"
@@ -53,6 +54,8 @@
CloudGroupRowsetBuilder::CloudGroupRowsetBuilder(CloudStorageEngine& engine,
CloudRowsetBuilder::~CloudRowsetBuilder() {
// Clear file cache immediately when load fails
if (_is_init && _rowset != nullptr &&
_rowset->rowset_meta()->rowset_state() == PREPARED) {
+
TEST_SYNC_POINT_CALLBACK("CloudRowsetBuilder::~CloudRowsetBuilder:before_clear_cache",
+ this);
_rowset->clear_cache();
}
}
@@ -89,6 +92,7 @@ Status CloudRowsetBuilder::init() {
context.txn_id = _req.txn_id;
context.txn_expiration = _req.txn_expiration;
context.load_id = _req.load_id;
+ context.load_cancel_status = _req.load_cancel_status;
context.db_id = _req.table_schema_param->db_id();
context.table_id = _req.table_schema_param->table_id();
context.rowset_state = PREPARED;
@@ -117,7 +121,8 @@ Status CloudRowsetBuilder::init() {
_rowset_writer = DORIS_TRY(_tablet->create_rowset_writer(context, false));
_rowset_id = context.rowset_id;
- _calc_delete_bitmap_token =
_engine.calc_delete_bitmap_executor()->create_token();
+ _calc_delete_bitmap_token =
+
_engine.calc_delete_bitmap_executor()->create_token(_req.load_cancel_status);
if (!_skip_writing_rowset_metadata) {
RETURN_IF_ERROR(_engine.meta_mgr().prepare_rowset(*_rowset_writer->rowset_meta(),
"",
@@ -172,6 +177,12 @@ Status CloudGroupRowsetBuilder::wait_calc_delete_bitmap() {
return _data_builder->wait_calc_delete_bitmap();
}
+Status CloudGroupRowsetBuilder::cancel(const Status& st) {
+ RETURN_IF_ERROR(_data_builder->cancel(st));
+ RETURN_IF_ERROR(_row_binlog_builder->cancel(st));
+ return BaseRowsetBuilder::cancel(st);
+}
+
void CloudGroupRowsetBuilder::update_tablet_stats() {
_data_builder->update_tablet_stats();
_row_binlog_builder->update_tablet_stats();
diff --git a/be/src/cloud/cloud_rowset_builder.h
b/be/src/cloud/cloud_rowset_builder.h
index c65e1924448..8d70a36fd64 100644
--- a/be/src/cloud/cloud_rowset_builder.h
+++ b/be/src/cloud/cloud_rowset_builder.h
@@ -74,6 +74,8 @@ public:
Status wait_calc_delete_bitmap() override;
+ Status cancel(const Status& st) override;
+
void update_tablet_stats() override;
Status commit_rowset(const std::string& job_id, int64_t table_id) override;
diff --git a/be/src/cloud/cloud_rowset_writer.cpp
b/be/src/cloud/cloud_rowset_writer.cpp
index 9753ca3dd99..20aa151a341 100644
--- a/be/src/cloud/cloud_rowset_writer.cpp
+++ b/be/src/cloud/cloud_rowset_writer.cpp
@@ -89,7 +89,8 @@ Status CloudRowsetWriter::init(const RowsetWriterContext&
rowset_writer_context)
_context.segment_collector =
std::make_shared<SegmentCollectorT<BaseBetaRowsetWriter>>(this);
_context.file_writer_creator =
std::make_shared<FileWriterCreatorT<BaseBetaRowsetWriter>>(this);
if (_context.mow_context != nullptr) {
- _calc_delete_bitmap_token =
_engine.calc_delete_bitmap_executor_for_load()->create_token();
+ _calc_delete_bitmap_token =
_engine.calc_delete_bitmap_executor_for_load()->create_token(
+ _context.load_cancel_status);
}
return Status::OK();
}
diff --git a/be/src/cloud/cloud_tablets_channel.cpp
b/be/src/cloud/cloud_tablets_channel.cpp
index 28caf74f79d..4d9f76a145d 100644
--- a/be/src/cloud/cloud_tablets_channel.cpp
+++ b/be/src/cloud/cloud_tablets_channel.cpp
@@ -205,6 +205,7 @@ Status
CloudTabletsChannel::_close_writers(PTabletWriterAddBlockResult* res) {
bool success = true;
for (auto&& [tablet_id, base_writer] : _tablet_writers) {
+ RETURN_IF_ERROR(_check_cancelled());
auto* writer = static_cast<CloudDeltaWriter*>(base_writer.get());
// ATTN: the strict mode means strict filtering of column type
conversions during import.
// Sometimes all inputs are filtered, but the partition ID is still
set, and the writer is
diff --git a/be/src/load/channel/load_channel.cpp
b/be/src/load/channel/load_channel.cpp
index e1066347c87..7cf31092087 100644
--- a/be/src/load/channel/load_channel.cpp
+++ b/be/src/load/channel/load_channel.cpp
@@ -20,8 +20,6 @@
#include <gen_cpp/internal_service.pb.h>
#include <glog/logging.h>
-#include <vector>
-
#include "cloud/cloud_tablets_channel.h"
#include "cloud/config.h"
#include "common/logging.h"
@@ -44,6 +42,7 @@ LoadChannel::LoadChannel(const UniqueId& load_id, int64_t
timeout_s, bool is_hig
std::string sender_ip, int64_t backend_id, bool
enable_profile,
int64_t wg_id)
: _load_id(load_id),
+ _cancel_status(std::make_shared<AtomicStatus>()),
_timeout_s(timeout_s),
_is_high_priority(is_high_priority),
_sender_ip(std::move(sender_ip)),
@@ -82,6 +81,15 @@ LoadChannel::LoadChannel(const UniqueId& load_id, int64_t
timeout_s, bool is_hig
}
LoadChannel::~LoadChannel() {
+ // Pending EOS RPCs do not own the channel. Complete them before destroying
+ // writers, which can wait for running flush and delete-bitmap tasks.
+ const auto reason =
+ is_cancelled()
+ ? cancel_status()
+ : Status::Cancelled("Load channel destroyed before all EOS
senders arrived");
+ for (const auto& [id, channel] : _tablets_channels) {
+ channel->eos_completion()->complete(reason);
+ }
g_loadchannel_cnt << -1;
std::stringstream rows_str;
for (const auto& entry : _tablets_channels_rows) {
@@ -142,6 +150,7 @@ Status LoadChannel::open(const PTabletWriterOpenRequest&
params) {
channel = std::make_shared<TabletsChannel>(engine.to_local(),
key, _load_id,
_is_high_priority,
_self_profile);
}
+ channel->set_load_cancel_status(_cancel_status);
{
std::lock_guard<std::mutex> lt(_tablets_channels_lock);
_tablets_channels.insert({index_id, channel});
@@ -184,6 +193,7 @@ Status
LoadChannel::_get_tablets_channel(std::shared_ptr<BaseTabletsChannel>& ch
Status LoadChannel::add_batch(const PTabletWriterAddBlockRequest& request,
PTabletWriterAddBlockResult* response,
std::shared_ptr<EosCompletion>* eos_completion) {
+ RETURN_IF_ERROR(cancel_status());
DBUG_EXECUTE_IF("LoadChannel.add_batch.failed",
{ return Status::InternalError("fault injection"); });
SCOPED_TIMER(_add_batch_timer);
@@ -240,6 +250,7 @@ Status LoadChannel::_handle_eos(BaseTabletsChannel* channel,
// errors belong to this RPC and cannot revoke earlier EOS responses.
RETURN_IF_ERROR(channel->close(this, request, response, &finished));
+ RETURN_IF_ERROR(cancel_status());
if (finished) {
std::lock_guard<std::mutex> l(_lock);
{
@@ -297,26 +308,18 @@ bool LoadChannel::is_finished() {
return _tablets_channels.empty();
}
-Status LoadChannel::cancel() {
- _cancelled.store(true);
- std::vector<std::shared_ptr<BaseTabletsChannel>> channels;
- {
- std::lock_guard<std::mutex> l(_lock);
- channels.reserve(_tablets_channels.size());
- for (auto& [id, channel] : _tablets_channels) {
- channels.push_back(channel);
- }
- }
- // Complete pending RPCs before waiting for writer cancellation. No
callback
- // may run under _lock, including when the timeout cleaner calls cancel().
- for (auto& channel : channels) {
- channel->eos_completion()->complete(
- Status::Cancelled("Load channel {} cancelled",
_load_id.to_string()));
- }
- for (auto& channel : channels) {
- static_cast<void>(channel->cancel());
- }
+Status LoadChannel::cancel(const Status& reason) {
+ DCHECK(!reason.ok());
+ _cancel_status->update(reason);
return Status::OK();
}
+Status LoadChannel::cancel_status() const {
+ return _cancel_status->ok() ? Status::OK() : _cancel_status->status();
+}
+
+bool LoadChannel::is_cancelled() const {
+ return !_cancel_status->ok();
+}
+
} // namespace doris
diff --git a/be/src/load/channel/load_channel.h
b/be/src/load/channel/load_channel.h
index 108abeedac4..be3ece6d570 100644
--- a/be/src/load/channel/load_channel.h
+++ b/be/src/load/channel/load_channel.h
@@ -41,6 +41,7 @@ class PTabletWriterAddBlockRequest;
class PTabletWriterAddBlockResult;
class OpenPartitionRequest;
class BaseTabletsChannel;
+class AtomicStatus;
// A LoadChannel manages tablets channels for all indexes
// corresponding to a certain load job
@@ -64,7 +65,10 @@ public:
// return true if this load channel has been opened and all tablets
channels are closed then.
bool is_finished();
- Status cancel();
+ // Publish cancellation without taking channel/writer locks. In-flight
+ // requests retain ownership and release writers after their work finishes.
+ Status cancel(const Status& reason = Status::Cancelled("load channel
cancelled"));
+ Status cancel_status() const;
time_t last_updated_time() const { return _last_updated_time.load(); }
@@ -74,7 +78,7 @@ public:
bool is_high_priority() const { return _is_high_priority; }
- bool is_cancelled() const { return _cancelled.load(); }
+ bool is_cancelled() const;
WorkloadGroupPtr workload_group() const { return
_resource_ctx->workload_group(); }
@@ -117,7 +121,7 @@ private:
std::unordered_set<int64_t> _finished_channel_ids;
// set to true if at least one tablets channel has been opened
bool _opened = false;
- std::atomic<bool> _cancelled {false};
+ const std::shared_ptr<AtomicStatus> _cancel_status;
std::shared_ptr<ResourceContext> _resource_ctx;
diff --git a/be/src/load/channel/load_channel_mgr.cpp
b/be/src/load/channel/load_channel_mgr.cpp
index 5682edeb99e..35ac1754606 100644
--- a/be/src/load/channel/load_channel_mgr.cpp
+++ b/be/src/load/channel/load_channel_mgr.cpp
@@ -88,6 +88,14 @@ Status LoadChannelMgr::open(const PTabletWriterOpenRequest&
params) {
if (it != _load_channels.end()) {
channel = it->second;
} else {
+ auto* handle = _load_state_channels->lookup(load_id.to_string());
+ if (handle != nullptr) {
+ auto* value =
static_cast<CacheValue*>(_load_state_channels->value(handle));
+ auto status =
+ value != nullptr ?
Status::Cancelled(value->_cancel_reason) : Status::OK();
+ _load_state_channels->release(handle);
+ RETURN_IF_ERROR(status);
+ }
// create a new load channel
int64_t timeout_in_req_s =
params.has_load_channel_timeout_s() ?
params.load_channel_timeout_s() : -1;
@@ -121,14 +129,11 @@ Status
LoadChannelMgr::_get_load_channel(std::shared_ptr<LoadChannel>& channel,
if (handle != nullptr) {
// load is cancelled
if (auto* value = _load_state_channels->value(handle); value !=
nullptr) {
- const auto& cancel_reason =
reinterpret_cast<CacheValue*>(value)->_cancel_reason;
+ const auto cancel_reason =
reinterpret_cast<CacheValue*>(value)->_cancel_reason;
_load_state_channels->release(handle);
- if (!cancel_reason.empty()) {
- LOG(INFO) << fmt::format(
- "The channel has been cancelled, load_id = {},
error = {}",
- print_id(load_id), cancel_reason);
- return Status::Cancelled(cancel_reason);
- }
+ LOG(INFO) << fmt::format("The channel has been cancelled,
load_id = {}, error = {}",
+ print_id(load_id), cancel_reason);
+ return Status::Cancelled(cancel_reason);
} else {
// load is success, success only when eos be true
_load_state_channels->release(handle);
@@ -178,24 +183,45 @@ Status LoadChannelMgr::add_batch(const
PTabletWriterAddBlockRequest& request,
// this case will be handled in load channel's add batch method.
Status st = channel->add_batch(request, response, eos_completion);
if (UNLIKELY(!st.ok())) {
- RETURN_IF_ERROR(channel->cancel());
+ RETURN_IF_ERROR(_cancel_load_channel(channel, st));
return st;
}
// 4. handle finish
if (channel->is_finished()) {
- _finish_load_channel(load_id);
+ _finish_load_channel(channel);
+ }
+ return Status::OK();
+}
+
+Status LoadChannelMgr::_cancel_load_channel(const
std::shared_ptr<LoadChannel>& channel,
+ const Status& reason) {
+ // Publish to the retained instance even if timeout cleanup or EOS removed
it.
+ RETURN_IF_ERROR(channel->cancel(reason));
+ std::lock_guard<std::mutex> l(_lock);
+ const auto& load_id = channel->load_id();
+ auto it = _load_channels.find(load_id);
+ if (it == _load_channels.end() || it->second != channel) {
+ // A late RPC must not cancel a replacement opened with the same load
ID.
+ return Status::OK();
}
+ _load_channels.erase(it);
+ _record_cancelled_load_channel(load_id,
std::string(channel->cancel_status().msg()));
return Status::OK();
}
-void LoadChannelMgr::_finish_load_channel(const UniqueId load_id) {
+void LoadChannelMgr::_finish_load_channel(const std::shared_ptr<LoadChannel>&
channel) {
+ const auto& load_id = channel->load_id();
VLOG_NOTICE << "removing load channel " << load_id << " because it's
finished";
{
std::lock_guard<std::mutex> l(_lock);
- if (_load_channels.contains(load_id)) {
- _load_channels.erase(load_id);
+ auto it = _load_channels.find(load_id);
+ if (it == _load_channels.end() || it->second != channel ||
channel->is_cancelled()) {
+ // A concurrent cancel may already have removed the channel and
recorded
+ // its failure. Do not replace that tombstone with a successful
EOS.
+ return;
}
+ _load_channels.erase(it);
auto* handle = _load_state_channels->insert(load_id.to_string(),
nullptr, 1, 1);
_load_state_channels->release(handle);
}
@@ -204,41 +230,53 @@ void LoadChannelMgr::_finish_load_channel(const UniqueId
load_id) {
Status LoadChannelMgr::cancel(const PTabletWriterCancelRequest& params) {
UniqueId load_id(params.id());
+ const auto reason = params.has_cancel_reason() &&
!params.cancel_reason().empty()
+ ? params.cancel_reason()
+ : "load channel cancelled";
std::shared_ptr<LoadChannel> cancelled_channel;
{
std::lock_guard<std::mutex> l(_lock);
if (_load_channels.contains(load_id)) {
cancelled_channel = _load_channels[load_id];
+ // Publish while this exact instance is still mapped, then cache
its
+ // effective first failure instead of a later cancel request's
reason.
+ // cancel() only updates the shared status; it takes no writer
locks.
+
RETURN_IF_ERROR(cancelled_channel->cancel(Status::Cancelled(reason)));
_load_channels.erase(load_id);
- }
- // We just need to record the first cancel msg
- auto* existing_handle =
_load_state_channels->lookup(load_id.to_string());
- if (existing_handle == nullptr) {
- if (params.has_cancel_reason() && !params.cancel_reason().empty())
{
- std::unique_ptr<CacheValue> cancel_reason_ptr =
std::make_unique<CacheValue>();
- cancel_reason_ptr->_cancel_reason = params.cancel_reason();
- size_t cache_capacity =
- cancel_reason_ptr->_cancel_reason.capacity() +
sizeof(CacheValue);
- auto* handle = _load_state_channels->insert(
- load_id.to_string(), cancel_reason_ptr.get(), 1,
cache_capacity);
- cancel_reason_ptr.release();
- _load_state_channels->release(handle);
- LOG(INFO) << fmt::format("load_id = {}, record_error reason =
{}",
- print_id(load_id),
params.cancel_reason());
- }
+ _record_cancelled_load_channel(load_id,
+
std::string(cancelled_channel->cancel_status().msg()));
} else {
- _load_state_channels->release(existing_handle);
+ _record_cancelled_load_channel(load_id, reason);
}
}
if (cancelled_channel != nullptr) {
- RETURN_IF_ERROR(cancelled_channel->cancel());
+ // Keep the final owner's destruction outside the manager lock, since
it
+ // may still wait for in-flight work.
LOG(INFO) << "load channel has been cancelled: " << load_id;
}
return Status::OK();
}
+void LoadChannelMgr::_record_cancelled_load_channel(const UniqueId& load_id,
+ const std::string& reason)
{
+ // Keep the first terminal state recorded for this load ID.
+ auto* existing_handle = _load_state_channels->lookup(load_id.to_string());
+ if (existing_handle != nullptr) {
+ _load_state_channels->release(existing_handle);
+ return;
+ }
+ auto value = std::make_unique<CacheValue>();
+ value->_cancel_reason = reason;
+ size_t cache_capacity = value->_cancel_reason.capacity() +
sizeof(CacheValue);
+ auto* handle =
+ _load_state_channels->insert(load_id.to_string(), value.get(), 1,
cache_capacity);
+ value.release();
+ _load_state_channels->release(handle);
+ LOG(INFO) << fmt::format("load_id = {}, record_error reason = {}",
print_id(load_id), reason);
+}
+
Status LoadChannelMgr::_start_bg_worker() {
RETURN_IF_ERROR(Thread::create(
"LoadChannelMgr", "cancel_timeout_load_channels",
@@ -276,12 +314,13 @@ Status LoadChannelMgr::_start_load_channels_clean() {
}
}
- // we must cancel these load channels before destroying them.
- // otherwise some object may be invalid before trying to visit it.
- // eg: MemTracker in load channel
+ // Publish cancellation to retained RPCs before releasing the cleanup
references.
+ // The last owner performs writer cleanup; memtables and writers retain
their
+ // resource contexts until their work finishes. Destruction may still wait
here
+ // when cleanup owns the last reference, so keep it outside the manager
lock.
for (auto& channel : need_delete_channels) {
RETURN_IF_ERROR(channel->cancel());
- LOG(INFO) << "load channel has been safely deleted: " <<
channel->load_id()
+ LOG(INFO) << "cancellation published for timed out load channel: " <<
channel->load_id()
<< ", timeout(s): " << channel->timeout();
}
diff --git a/be/src/load/channel/load_channel_mgr.h
b/be/src/load/channel/load_channel_mgr.h
index 255f8b1c223..1c06fded153 100644
--- a/be/src/load/channel/load_channel_mgr.h
+++ b/be/src/load/channel/load_channel_mgr.h
@@ -82,7 +82,10 @@ private:
Status _get_load_channel(std::shared_ptr<LoadChannel>& channel, bool&
is_eof,
const UniqueId& load_id, const
PTabletWriterAddBlockRequest& request);
- void _finish_load_channel(UniqueId load_id);
+ Status _cancel_load_channel(const std::shared_ptr<LoadChannel>& channel,
const Status& reason);
+ void _finish_load_channel(const std::shared_ptr<LoadChannel>& channel);
+ // Caller holds _lock.
+ void _record_cancelled_load_channel(const UniqueId& load_id, const
std::string& reason);
Status _start_bg_worker();
@@ -90,6 +93,8 @@ private:
public:
class CacheValue : public LRUCacheValueBase {
public:
+ // Unformatted message, without a Status code prefix or stack
trace.
+ // A non-null cache value marks cancellation even when the message
is empty.
std::string _cancel_reason;
};
diff --git a/be/src/load/channel/tablets_channel.cpp
b/be/src/load/channel/tablets_channel.cpp
index ab540459727..01e19042245 100644
--- a/be/src/load/channel/tablets_channel.cpp
+++ b/be/src/load/channel/tablets_channel.cpp
@@ -90,6 +90,14 @@ BaseTabletsChannel::~BaseTabletsChannel() {
TabletsChannel::~TabletsChannel() = default;
+Status BaseTabletsChannel::_check_cancelled() {
+ if (_load_cancel_status && !_load_cancel_status->ok()) {
+ _close_status = _load_cancel_status->status();
+ return _close_status;
+ }
+ return Status::OK();
+}
+
Status BaseTabletsChannel::_get_current_seq(int64_t& cur_seq,
const
PTabletWriterAddBlockRequest& request,
bool& should_write) {
@@ -138,8 +146,9 @@ void BaseTabletsChannel::_init_profile(RuntimeProfile*
profile) {
Status BaseTabletsChannel::open(const PTabletWriterOpenRequest& request) {
std::lock_guard<std::mutex> l(_lock);
- // if _state is kOpened, it's a normal case, already open by other sender
- // if _state is kFinished, already cancelled by other sender
+ RETURN_IF_ERROR(_check_cancelled());
+ // Another sender may have already opened or closed this channel.
+ // Shared load cancellation is checked above and does not change _state.
if (_state == kOpened) {
RETURN_IF_ERROR(_init_adaptive_random_bucket_state(request));
return Status::OK();
@@ -198,6 +207,7 @@ Status BaseTabletsChannel::incremental_open(const
PTabletWriterOpenRequest& para
}
std::lock_guard<std::mutex> l(_lock);
+ RETURN_IF_ERROR(_check_cancelled());
// one sender may incremental_open many times. but only close one time. so
dont count duplicately.
if (_open_by_incremental) {
@@ -238,6 +248,7 @@ Status BaseTabletsChannel::incremental_open(const
PTabletWriterOpenRequest& para
incremental_tablet_num++;
WriteRequest wrequest;
+ wrequest.load_cancel_status = _load_cancel_status;
wrequest.index_id = params.index_id();
wrequest.tablet_id = tablet.tablet_id();
wrequest.schema_hash = schema_hash;
@@ -357,11 +368,15 @@ Status
BaseTabletsChannel::_close_and_notify(std::unique_lock<std::mutex>& lock,
_state = kFinished;
_final_sender_id = sender_id;
_final_close_in_progress = true;
+ const auto arrival_status = _check_cancelled();
lock.unlock();
- _eos_completion->complete(Status::OK());
+ _eos_completion->complete(arrival_status);
lock.lock();
- _close_status = _close_writers(response);
+ _close_status = _check_cancelled();
+ if (_close_status.ok()) {
+ _close_status = _close_writers(response);
+ }
_final_close_result =
std::make_unique<PTabletWriterAddBlockResult>(*response);
_final_close_in_progress = false;
_final_close_cv.notify_all();
@@ -424,6 +439,7 @@ Status
TabletsChannel::_close_writers(PTabletWriterAddBlockResult* res) {
std::set<DeltaWriter*> need_wait_writers;
// under _lock. no need _tablet_writers_lock again.
for (auto&& [tablet_id, writer] : _tablet_writers) {
+ RETURN_IF_ERROR(_check_cancelled());
if (_partition_ids.contains(writer->partition_id())) {
auto st = writer->close();
if (!st.ok()) {
@@ -620,6 +636,7 @@ Status BaseTabletsChannel::_open_all_writers(const
PTabletWriterOpenRequest& req
.write_file_cache = request.write_file_cache(),
.storage_vault_id = request.storage_vault_id(),
.enable_table_memtable_backpressure =
request.is_adaptive_random_bucket(),
+ .load_cancel_status = _load_cancel_status,
};
if (tablet.has_binlog_tablet_id()) {
wrequest.binlog_tablet_id = tablet.binlog_tablet_id();
diff --git a/be/src/load/channel/tablets_channel.h
b/be/src/load/channel/tablets_channel.h
index 51a1de5c7db..0bb734cf3fd 100644
--- a/be/src/load/channel/tablets_channel.h
+++ b/be/src/load/channel/tablets_channel.h
@@ -113,6 +113,11 @@ public:
// no-op when this channel has been closed or cancelled
virtual Status cancel();
+ // Set once, before publishing the channel or opening any writers.
+ void set_load_cancel_status(std::shared_ptr<AtomicStatus> status) {
+ _load_cancel_status = std::move(status);
+ }
+
const std::shared_ptr<EosCompletion>& eos_completion() const { return
_eos_completion; }
void refresh_profile();
@@ -127,6 +132,9 @@ public:
bool is_finished() const { return _state == kFinished; }
protected:
+ // Caller holds _lock. The cancellation publisher never needs this lock.
+ Status _check_cancelled();
+
// Called by the final sender under _lock. Publish arrival before flushing,
// releasing the channel lock while invoking RPC callbacks.
Status _close_and_notify(std::unique_lock<std::mutex>& lock, int sender_id,
@@ -181,6 +189,7 @@ protected:
State _state;
UniqueId _load_id;
+ std::shared_ptr<AtomicStatus> _load_cancel_status;
// initialized in open function
int64_t _txn_id = -1;
diff --git a/be/src/load/delta_writer/delta_writer.cpp
b/be/src/load/delta_writer/delta_writer.cpp
index abcda75afa4..84504805e1e 100644
--- a/be/src/load/delta_writer/delta_writer.cpp
+++ b/be/src/load/delta_writer/delta_writer.cpp
@@ -31,6 +31,7 @@
#include "common/logging.h"
#include "common/status.h"
#include "core/block/block.h"
+#include "cpp/sync_point.h"
#include "io/fs/file_writer.h" // IWYU pragma: keep
#include "load/memtable/memtable_flush_executor.h"
#include "load/memtable/memtable_memory_limiter.h"
@@ -93,13 +94,19 @@ void DeltaWriter::_init_profile(RuntimeProfile* profile) {
}
BaseDeltaWriter::~BaseDeltaWriter() {
+ // Initialization may have created bitmap tokens before memtable init
failed.
+ // Stop producers and drain both bitmap phases before any builder cleanup.
+ // A derived constructor can fail before it installs its builder.
+ if (_rowset_builder != nullptr) {
+ auto st = _get_load_cancel_status();
+ WARN_IF_ERROR(BaseDeltaWriter::cancel_with_status(
+ st.ok() ? Status::Cancelled("delta writer
destroyed") : st),
+ "failed to cancel work while destroying delta writer");
+ }
if (!_is_init) {
return;
}
- // cancel and wait all memtables in flush queue to be finished
- static_cast<void>(_memtable_writer->cancel());
-
if (_rowset_builder->tablet() != nullptr) {
const FlushStatistic& stat = _memtable_writer->get_flush_token_stats();
_rowset_builder->tablet()->flush_bytes->increment(stat.flush_size_bytes);
@@ -136,15 +143,24 @@ int64_t BaseDeltaWriter::table_id() const {
DeltaWriter::~DeltaWriter() = default;
+Status BaseDeltaWriter::_get_load_cancel_status() const {
+ return _req.load_cancel_status && !_req.load_cancel_status->ok()
+ ? _req.load_cancel_status->status()
+ : Status::OK();
+}
+
Status BaseDeltaWriter::init() {
if (_is_init) {
return Status::OK();
}
+ RETURN_IF_ERROR(_get_load_cancel_status());
std::shared_ptr<WorkloadGroup> wg_sptr = nullptr;
if (doris::thread_context()->is_attach_task()) {
wg_sptr = doris::thread_context()->resource_ctx()->workload_group();
}
RETURN_IF_ERROR(_rowset_builder->init());
+
TEST_SYNC_POINT_RETURN_WITH_VALUE("BaseDeltaWriter::init:before_memtable_init",
Status::OK(),
+ this);
RETURN_IF_ERROR(_memtable_writer->init(
_rowset_builder->rowset_writer(), _rowset_builder->tablet_schema(),
_rowset_builder->get_partial_update_info(), wg_sptr,
@@ -166,7 +182,7 @@ Status DeltaWriter::write(const Block* block, const
TabletAddRowsPayload& rows,
ExecEnv::GetInstance()->memtable_memory_limiter()->handle_table_memtable_backpressure(
[this]() {
std::lock_guard<std::mutex> l(_lock);
- return _is_cancelled;
+ return _is_cancelled || !_get_load_cancel_status().ok();
},
table_id());
}
@@ -182,6 +198,7 @@ Status DeltaWriter::write(const Block* block, const
TabletAddRowsPayload& rows,
config::memtable_flush_running_count_limit *
(_req.write_req_type == WriteRequestType::GROUP ? 2 : 1);
while (_memtable_writer->flush_running_count() >=
effective_flush_running_count_limit) {
+ RETURN_IF_ERROR(_get_load_cancel_status());
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
@@ -221,6 +238,7 @@ Status DeltaWriter::close() {
Status BaseDeltaWriter::build_rowset() {
SCOPED_TIMER(_close_wait_timer);
RETURN_IF_ERROR(_memtable_writer->close_wait(_profile));
+ RETURN_IF_ERROR(_get_load_cancel_status());
return _rowset_builder->build_rowset();
}
@@ -232,6 +250,7 @@ Status DeltaWriter::build_rowset() {
}
Status BaseDeltaWriter::submit_calc_delete_bitmap_task() {
+ RETURN_IF_ERROR(_get_load_cancel_status());
return _rowset_builder->submit_calc_delete_bitmap_task();
}
@@ -242,6 +261,7 @@ Status BaseDeltaWriter::wait_calc_delete_bitmap() {
Status DeltaWriter::commit_txn() {
std::lock_guard<std::mutex> l(_lock);
SCOPED_TIMER(_commit_txn_timer);
+ RETURN_IF_ERROR(_get_load_cancel_status());
return _rowset_builder->commit_txn();
}
@@ -254,6 +274,7 @@ Status BaseDeltaWriter::cancel_with_status(const Status&
st) {
return Status::OK();
}
RETURN_IF_ERROR(_memtable_writer->cancel_with_status(st));
+ RETURN_IF_ERROR(_rowset_builder->cancel(st));
_is_cancelled = true;
return Status::OK();
}
diff --git a/be/src/load/delta_writer/delta_writer.h
b/be/src/load/delta_writer/delta_writer.h
index 4bed76d80cd..a405349161a 100644
--- a/be/src/load/delta_writer/delta_writer.h
+++ b/be/src/load/delta_writer/delta_writer.h
@@ -70,7 +70,8 @@ public:
Status submit_calc_delete_bitmap_task();
Status wait_calc_delete_bitmap();
- // abandon current memtable and wait for all pending-flushing memtables to
be destructed.
+ // Abandon the current memtable and cancel queued flush tasks.
+ // Wait for running tasks before releasing their resources.
// mem_consumption() should be 0 after this function returns.
Status cancel();
virtual Status cancel_with_status(const Status& st);
@@ -104,6 +105,8 @@ public:
google::protobuf::RepeatedPtrField<PTabletLoadRowsetInfo>*
tablet_info);
protected:
+ Status _get_load_cancel_status() const;
+
virtual void _init_profile(RuntimeProfile* profile);
Status init();
diff --git a/be/src/load/delta_writer/delta_writer_context.h
b/be/src/load/delta_writer/delta_writer_context.h
index 06b4659c79b..88de1a171bf 100644
--- a/be/src/load/delta_writer/delta_writer_context.h
+++ b/be/src/load/delta_writer/delta_writer_context.h
@@ -30,6 +30,7 @@ namespace doris {
class TupleDescriptor;
class SlotDescriptor;
class OlapTableSchemaParam;
+class AtomicStatus;
enum class WriteRequestType {
DATA = 0, // data write
@@ -56,6 +57,8 @@ struct WriteRequest {
WriteRequestType write_req_type = WriteRequestType::DATA;
std::string storage_vault_id;
bool enable_table_memtable_backpressure = false;
+ // Shared by all writers of a load; published before writers can submit
work.
+ std::shared_ptr<AtomicStatus> load_cancel_status = nullptr;
};
struct TabletAddRowsPayload {
diff --git a/be/src/load/memtable/memtable_flush_executor.cpp
b/be/src/load/memtable/memtable_flush_executor.cpp
index 2ad61661d4f..6345e5121e1 100644
--- a/be/src/load/memtable/memtable_flush_executor.cpp
+++ b/be/src/load/memtable/memtable_flush_executor.cpp
@@ -161,6 +161,11 @@ Status FlushToken::_submit_sub_tasks(ThreadPool* pool,
return Status::OK();
}
+Status FlushToken::_get_load_cancel_status() const {
+ return _load_cancel_status && !_load_cancel_status->ok() ?
_load_cancel_status->status()
+ : Status::OK();
+}
+
Status FlushToken::submit(std::shared_ptr<MemTable> mem_table) {
{
std::shared_lock rdlk(_flush_status_lock);
@@ -259,7 +264,7 @@ Status FlushToken::wait() {
return _flush_status;
}
}
- return Status::OK();
+ return _get_load_cancel_status();
}
Status FlushToken::_try_reserve_memory(const std::shared_ptr<ResourceContext>&
resource_context,
@@ -356,7 +361,7 @@ void FlushToken::_flush_memtable_impl(RowsetWriter*
flush_writer, MemTable* memt
}};
DBUG_EXECUTE_IF("FlushToken.flush_memtable.wait_before_first_shutdown",
{ std::this_thread::sleep_for(std::chrono::milliseconds(10
* 1000)); });
- if (_is_shutdown()) {
+ if (_is_shutdown() || !_get_load_cancel_status().ok()) {
return;
}
DBUG_EXECUTE_IF("FlushToken.flush_memtable.wait_after_first_shutdown",
@@ -406,6 +411,7 @@ void FlushToken::_flush_memtable_impl(RowsetWriter*
flush_writer, MemTable* memt
// }};
std::shared_ptr<Block> flush_block;
RETURN_IF_ERROR(_memtable2block(memtable, shared_memtable,
flush_block));
+ RETURN_IF_ERROR(_get_load_cancel_status());
RETURN_IF_ERROR(
flush_writer->flush_memtable(flush_block.get(),
segment_id, &flush_size));
memtable->set_flush_success();
@@ -518,7 +524,8 @@ void MemTableFlushExecutor::update_memtable_flush_threads()
{
Status MemTableFlushExecutor::create_flush_token(
std::shared_ptr<FlushToken>& flush_token,
std::shared_ptr<RowsetWriter> rowset_writer,
bool is_high_priority, std::shared_ptr<WorkloadGroup> wg_sptr,
- std::shared_ptr<OlapTableSchemaParam> table_schema_param) {
+ std::shared_ptr<OlapTableSchemaParam> table_schema_param,
+ std::shared_ptr<AtomicStatus> load_cancel_status) {
switch (rowset_writer->type()) {
case ALPHA_ROWSET:
// alpha rowset do not support flush in CONCURRENT. and not support
alpha rowset now.
@@ -526,7 +533,7 @@ Status MemTableFlushExecutor::create_flush_token(
case BETA_ROWSET: {
// beta rowset can be flush in CONCURRENT, because each memtable using
a new segment writer.
ThreadPool* pool = is_high_priority ? _high_prio_flush_pool.get() :
_flush_pool.get();
- flush_token = FlushToken::create_shared(pool, wg_sptr);
+ flush_token = FlushToken::create_shared(pool, wg_sptr,
std::move(load_cancel_status));
flush_token->set_rowset_writer(rowset_writer);
flush_token->set_table_schema_param(std::move(table_schema_param));
return Status::OK();
diff --git a/be/src/load/memtable/memtable_flush_executor.h
b/be/src/load/memtable/memtable_flush_executor.h
index c3053d483e5..3c707439b00 100644
--- a/be/src/load/memtable/memtable_flush_executor.h
+++ b/be/src/load/memtable/memtable_flush_executor.h
@@ -99,13 +99,16 @@ class FlushToken : public
std::enable_shared_from_this<FlushToken> {
ENABLE_FACTORY_CREATOR(FlushToken);
public:
- FlushToken(ThreadPool* thread_pool, std::shared_ptr<WorkloadGroup> wg_sptr)
- : _flush_status(Status::OK()), _thread_pool(thread_pool),
_wg_wptr(wg_sptr) {}
+ FlushToken(ThreadPool* thread_pool, std::shared_ptr<WorkloadGroup> wg_sptr,
+ std::shared_ptr<AtomicStatus> load_cancel_status = nullptr)
+ : _flush_status(Status::OK()),
+ _load_cancel_status(std::move(load_cancel_status)),
+ _thread_pool(thread_pool),
+ _wg_wptr(wg_sptr) {}
Status submit(std::shared_ptr<MemTable> mem_table);
- // error has happens, so we cancel this token
- // And remove all tasks in the queue.
+ // Stop flush work and wait for running tasks. Queued tasks skip flushing
when dispatched.
void cancel();
// wait all tasks in token to be completed.
@@ -125,6 +128,8 @@ public:
const MemTableStat& memtable_stat() { return _memtable_stat; }
private:
+ Status _get_load_cancel_status() const;
+
void _shutdown_flush_token() { _shutdown.store(true); }
bool _is_shutdown() { return _shutdown.load(); }
void _wait_submit_task_finish();
@@ -155,6 +160,7 @@ private:
// Note: Once its value is set to Failed, it cannot return to SUCCESS.
std::shared_mutex _flush_status_lock;
Status _flush_status;
+ const std::shared_ptr<AtomicStatus> _load_cancel_status;
FlushStatistic _stats;
@@ -198,7 +204,8 @@ public:
Status create_flush_token(std::shared_ptr<FlushToken>& flush_token,
std::shared_ptr<RowsetWriter> rowset_writer,
bool is_high_priority,
std::shared_ptr<WorkloadGroup> wg_sptr,
- std::shared_ptr<OlapTableSchemaParam>
table_schema_param = nullptr);
+ std::shared_ptr<OlapTableSchemaParam>
table_schema_param = nullptr,
+ std::shared_ptr<AtomicStatus> load_cancel_status
= nullptr);
// return true if it already has any flushing task
bool check_and_inc_has_any_flushing_task() {
diff --git a/be/src/load/memtable/memtable_memory_limiter.cpp
b/be/src/load/memtable/memtable_memory_limiter.cpp
index f98aea05f71..d32ed37d18d 100644
--- a/be/src/load/memtable/memtable_memory_limiter.cpp
+++ b/be/src/load/memtable/memtable_memory_limiter.cpp
@@ -282,9 +282,10 @@ int64_t
MemTableMemoryLimiter::_flush_active_memtables(int64_t need_flush) {
}
int64_t mem_flushed = 0;
+ int64_t mem_reclaimed = 0;
int64_t num_flushed = 0;
- while (mem_flushed < need_flush && !heap.empty()) {
+ while (mem_flushed + mem_reclaimed < need_flush && !heap.empty()) {
auto [writer, sort_mem] = heap.top();
heap.pop();
auto w = writer.lock();
@@ -298,6 +299,12 @@ int64_t
MemTableMemoryLimiter::_flush_active_memtables(int64_t need_flush) {
continue;
}
Status st = w->flush_async();
+ if (st.is<ErrorCode::CANCELLED>()) {
+ // The active memtable was discarded, not flushed. Running flush
+ // tasks are still owned by the writer and must not be waited here.
+ mem_reclaimed += mem;
+ continue;
+ }
if (!st.ok()) {
auto err_msg = fmt::format(
"tablet writer failed to reduce mem consumption by
flushing memtable, "
@@ -312,7 +319,13 @@ int64_t
MemTableMemoryLimiter::_flush_active_memtables(int64_t need_flush) {
g_memtable_memory_limit_flush_size_bytes << mem;
}
LOG(INFO) << "flushed " << num_flushed << " out of " <<
_active_writers.size()
- << " active writers, flushed size: " <<
PrettyPrinter::print_bytes(mem_flushed);
+ << " active writers, flushed size: " <<
PrettyPrinter::print_bytes(mem_flushed)
+ << ", reclaimed cancelled memtables: " <<
PrettyPrinter::print_bytes(mem_reclaimed);
+ if (mem_reclaimed > 0) {
+ // Recheck the hard limit before the caller waits, and wake other
waiters
+ // if reclaiming cancelled memtables has relieved memory pressure.
+ _refresh_mem_tracker();
+ }
return mem_flushed;
}
diff --git a/be/src/load/memtable/memtable_writer.cpp
b/be/src/load/memtable/memtable_writer.cpp
index 09ab384b847..f05e5a5cde4 100644
--- a/be/src/load/memtable/memtable_writer.cpp
+++ b/be/src/load/memtable/memtable_writer.cpp
@@ -114,7 +114,7 @@ Status MemTableWriter::init(std::shared_ptr<RowsetWriter>
rowset_writer,
RETURN_IF_ERROR(
ExecEnv::GetInstance()->storage_engine().memtable_flush_executor()->create_flush_token(
_flush_token, _rowset_writer, _req.is_high_priority,
wg_sptr,
- _req.table_schema_param));
+ _req.table_schema_param, _req.load_cancel_status));
_is_init = true;
return Status::OK();
@@ -132,6 +132,9 @@ Status MemTableWriter::write(const Block* block, const
TabletAddRowsPayload& row
_lock_watch.start();
std::lock_guard<std::mutex> l(_lock);
_lock_watch.stop();
+ if (_req.load_cancel_status && !_req.load_cancel_status->ok()) {
+ return _req.load_cancel_status->status();
+ }
if (_is_cancelled) {
return _cancel_status;
}
@@ -228,10 +231,20 @@ Status MemTableWriter::_flush_memtable_async() {
Status MemTableWriter::flush_async() {
std::lock_guard<std::mutex> l(_lock);
- // Three calling paths:
- // 1. call by local, from `VTabletWriterV2::_write_memtable`.
- // 2. call by remote, from `LoadChannelMgr::_get_load_channel`.
- // 3. call by daemon thread, from `handle_paused_queries` ->
`flush_workload_group_memtables`.
+ if (_req.load_cancel_status && !_req.load_cancel_status->ok()) {
+ // Reclaim the unsubmitted memtable even while an RPC retains this
writer.
+ // Do not cancel the flush token here: pressure flushing holds the
limiter
+ // lock and must not wait for running flush tasks. Leave _is_cancelled
+ // unset so the final owner's cancel still drains the token.
+ {
+ std::lock_guard<std::mutex> lm(_mem_table_ptr_lock);
+ _mem_table.reset();
+ }
+ return Status::Cancelled("Load has been cancelled: {}",
+
_req.load_cancel_status->status().to_string());
+ }
+ // Memory-pressure flushing can race with writer initialization or close.
+ // DeltaWriter also exposes this operation through flush_memtable_async().
if (!_is_init || _is_closed) {
// This writer is uninitialized or closed before flushing, do nothing.
// We return OK instead of NOT_INITIALIZED or ALREADY_CLOSED.
@@ -287,6 +300,9 @@ Status MemTableWriter::close() {
_lock_watch.start();
std::lock_guard<std::mutex> l(_lock);
_lock_watch.stop();
+ if (_req.load_cancel_status && !_req.load_cancel_status->ok()) {
+ return _req.load_cancel_status->status();
+ }
if (_is_cancelled) {
return _cancel_status;
}
diff --git a/be/src/storage/delete/calc_delete_bitmap_executor.cpp
b/be/src/storage/delete/calc_delete_bitmap_executor.cpp
index af1b4bfb23a..d5fb2f87b39 100644
--- a/be/src/storage/delete/calc_delete_bitmap_executor.cpp
+++ b/be/src/storage/delete/calc_delete_bitmap_executor.cpp
@@ -22,6 +22,7 @@
#include <ostream>
#include "common/logging.h"
+#include "cpp/sync_point.h"
#include "load/memtable/memtable.h"
#include "storage/tablet/base_tablet.h"
#include "util/time.h"
@@ -35,16 +36,9 @@ Status CalcDeleteBitmapToken::submit(BaseTabletSPtr tablet,
RowsetSharedPtr cur_
int64_t end_version, DeleteBitmapPtr
delete_bitmap,
RowsetWriter* rowset_writer,
DeleteBitmapPtr tablet_delete_bitmap) {
- {
- std::shared_lock rlock(_lock);
- RETURN_IF_ERROR(_status);
- _resource_ctx = thread_context()->resource_ctx();
- }
-
const auto submit_time_us = MonotonicMicros();
- return _thread_token->submit_func([=, this]() {
+ return submit_func([=]() {
const auto queue_time_us = MonotonicMicros() - submit_time_us;
- SCOPED_ATTACH_TASK(_resource_ctx);
auto st = tablet->calc_segment_delete_bitmap(cur_rowset, cur_segment,
target_rowsets,
delete_bitmap,
end_version, rowset_writer,
tablet_delete_bitmap,
queue_time_us);
@@ -53,11 +47,8 @@ Status CalcDeleteBitmapToken::submit(BaseTabletSPtr tablet,
RowsetSharedPtr cur_
<< tablet->tablet_id() << " rowset: " <<
cur_rowset->rowset_id()
<< " seg_id: " << cur_segment->id() << " version: "
<< end_version
<< " error: " << st;
- std::lock_guard wlock(_lock);
- if (_status.ok()) {
- _status = st;
- }
}
+ return st;
});
}
@@ -65,33 +56,57 @@ Status CalcDeleteBitmapToken::submit(BaseTabletSPtr tablet,
TabletSchemaSPtr sch
RowsetId rowset_id,
const
std::vector<segment_v2::SegmentSharedPtr>& segments,
DeleteBitmapPtr delete_bitmap) {
- {
- std::shared_lock rlock(_lock);
- RETURN_IF_ERROR(_status);
- _resource_ctx = thread_context()->resource_ctx();
- }
const auto submit_time_us = MonotonicMicros();
- return _thread_token->submit_func([=, this]() {
+ return submit_func([=]() {
const auto queue_time_us = MonotonicMicros() - submit_time_us;
- SCOPED_ATTACH_TASK(_resource_ctx);
auto st = tablet->calc_delete_bitmap_between_segments(schema,
rowset_id, segments,
delete_bitmap,
queue_time_us);
if (!st.ok()) {
LOG(WARNING) << "failed to calc delete bitmap between segments,
tablet_id: "
<< tablet->tablet_id() << " rowset: " << rowset_id
<< " segments num: " << segments.size() << " error: "
<< st;
- std::lock_guard wlock(_lock);
- if (_status.ok()) {
- _status = st;
- }
}
+ return st;
});
}
+Status CalcDeleteBitmapToken::_submit_func(std::function<void()> func) {
+
TEST_SYNC_POINT_CALLBACK("CalcDeleteBitmapToken::submit_func:before_submit",
this);
+ auto st = _thread_token->submit_func(std::move(func));
+ if (!st.ok()) {
+ // Teardown can shut down the token after the entry check.
+ RETURN_IF_ERROR(_get_status());
+ }
+ return st;
+}
+
Status CalcDeleteBitmapToken::wait() {
_thread_token->wait();
- // all tasks complete here, don't need lock;
- return _status;
+ return _get_status();
+}
+
+Status CalcDeleteBitmapToken::_get_status() {
+ std::shared_lock rlock(_lock);
+ RETURN_IF_ERROR(_status);
+ return _load_cancel_status && !_load_cancel_status->ok() ?
_load_cancel_status->status()
+ : Status::OK();
+}
+
+void CalcDeleteBitmapToken::_set_status(const Status& st) {
+ DCHECK(!st.ok());
+ std::lock_guard wlock(_lock);
+ if (_status.ok()) {
+ // Keep an earlier token failure local. If load cancellation is already
+ // observable, a later callback failure must not replace its reason.
+ _status = _load_cancel_status && !_load_cancel_status->ok() ?
_load_cancel_status->status()
+ : st;
+ }
+}
+
+void CalcDeleteBitmapToken::cancel(const Status& st) {
+ _set_status(st);
+ // Running callbacks may still need _lock to publish their result.
+ _thread_token->shutdown();
}
void CalcDeleteBitmapExecutor::init(const std::string& name, int max_threads) {
@@ -101,9 +116,11 @@ void CalcDeleteBitmapExecutor::init(const std::string&
name, int max_threads) {
.build(&_thread_pool));
}
-std::unique_ptr<CalcDeleteBitmapToken>
CalcDeleteBitmapExecutor::create_token() {
+std::unique_ptr<CalcDeleteBitmapToken> CalcDeleteBitmapExecutor::create_token(
+ std::shared_ptr<AtomicStatus> load_cancel_status) {
return std::make_unique<CalcDeleteBitmapToken>(
- _thread_pool->new_token(ThreadPool::ExecutionMode::CONCURRENT));
+ _thread_pool->new_token(ThreadPool::ExecutionMode::CONCURRENT),
+ std::move(load_cancel_status));
}
} // namespace doris
diff --git a/be/src/storage/delete/calc_delete_bitmap_executor.h
b/be/src/storage/delete/calc_delete_bitmap_executor.h
index 4f0f0fd2c7f..22a126a1c26 100644
--- a/be/src/storage/delete/calc_delete_bitmap_executor.h
+++ b/be/src/storage/delete/calc_delete_bitmap_executor.h
@@ -20,6 +20,7 @@
#include <stdint.h>
#include <atomic>
+#include <functional>
#include <iosfwd>
#include <memory>
#include <shared_mutex>
@@ -48,8 +49,14 @@ enum RowsetTypePB : int;
// 4. call `get_delete_bitmap()` to get the result of all tasks
class CalcDeleteBitmapToken {
public:
- explicit CalcDeleteBitmapToken(std::unique_ptr<ThreadPoolToken>
thread_token)
- : _thread_token(std::move(thread_token)), _status(Status::OK()) {}
+ explicit CalcDeleteBitmapToken(std::unique_ptr<ThreadPoolToken>
thread_token,
+ std::shared_ptr<AtomicStatus>
load_cancel_status = nullptr)
+ : _thread_token(std::move(thread_token)),
+ _load_cancel_status(std::move(load_cancel_status)),
+ _status(Status::OK()) {}
+
+ // Drain callbacks before destroying the status and lock they access.
+ ~CalcDeleteBitmapToken() { _thread_token->shutdown(); }
// calculate delete bitmap of `cur_segment` to historical `target_rowsets`
Status submit(BaseTabletSPtr tablet, RowsetSharedPtr cur_rowset,
@@ -66,19 +73,18 @@ public:
// submit a generic function to the thread pool
template <typename Func>
Status submit_func(Func&& func) {
- {
- std::shared_lock rlock(_lock);
- RETURN_IF_ERROR(_status);
- _resource_ctx = thread_context()->resource_ctx();
- }
- return _thread_token->submit_func([this, func =
std::forward<Func>(func)]() {
- SCOPED_ATTACH_TASK(_resource_ctx);
- auto st = func();
+ RETURN_IF_ERROR(_get_status());
+ // Each callback owns the context of its submitter, even when
submissions overlap.
+ auto resource_ctx = thread_context()->resource_ctx();
+ return _submit_func([this, resource_ctx, func =
std::forward<Func>(func)]() {
+ SCOPED_ATTACH_TASK(resource_ctx);
+ // Publication does not remove queued tasks. Skip their
calculation when dispatched.
+ auto st = _get_status();
+ if (st.ok()) {
+ st = func();
+ }
if (!st.ok()) {
- std::lock_guard wlock(_lock);
- if (_status.ok()) {
- _status = st;
- }
+ _set_status(st);
}
});
}
@@ -86,16 +92,21 @@ public:
// wait all tasks in token to be completed.
Status wait();
- void cancel() { _thread_token->shutdown(); }
+ // Teardown only: discard pending callbacks and wait for running callbacks.
+ void cancel(const Status& st = Status::Cancelled("delete bitmap
calculation cancelled"));
private:
+ Status _submit_func(std::function<void()> func);
+ Status _get_status();
+ void _set_status(const Status& st);
+
std::unique_ptr<ThreadPoolToken> _thread_token;
+ const std::shared_ptr<AtomicStatus> _load_cancel_status;
std::shared_mutex _lock;
// Records the current status of the calc delete bitmap job.
// Note: Once its value is set to Failed, it cannot return to SUCCESS.
Status _status;
- std::shared_ptr<ResourceContext> _resource_ctx;
};
// CalcDeleteBitmapExecutor is responsible for calc delete bitmap concurrently.
@@ -108,7 +119,8 @@ public:
// init should be called after storage engine is opened,
void init(const std::string& name, int max_threads);
- std::unique_ptr<CalcDeleteBitmapToken> create_token();
+ std::unique_ptr<CalcDeleteBitmapToken> create_token(
+ std::shared_ptr<AtomicStatus> load_cancel_status = nullptr);
private:
std::unique_ptr<ThreadPool> _thread_pool;
diff --git a/be/src/storage/rowset/beta_rowset_writer.cpp
b/be/src/storage/rowset/beta_rowset_writer.cpp
index 265448a8314..70c855901ad 100644
--- a/be/src/storage/rowset/beta_rowset_writer.cpp
+++ b/be/src/storage/rowset/beta_rowset_writer.cpp
@@ -339,7 +339,13 @@ BetaRowsetWriter::BetaRowsetWriter(StorageEngine& engine)
RowBinlogRowsetWriter::RowBinlogRowsetWriter(StorageEngine& engine) :
BetaRowsetWriter(engine) {}
BaseBetaRowsetWriter::~BaseBetaRowsetWriter() {
+ // Finish callbacks before removing files they can still read on
cancellation.
+ if (_calc_delete_bitmap_token) {
+ _calc_delete_bitmap_token->cancel();
+ }
if (!_already_built && _rowset_meta->is_local()) {
+
TEST_SYNC_POINT_CALLBACK("BaseBetaRowsetWriter::~BaseBetaRowsetWriter:before_file_cleanup",
+ this);
// abnormal exit, remove all files generated
auto& fs = io::global_local_filesystem();
for (int i = _segment_start_id; i <
_segment_creator.next_segment_id(); ++i) {
@@ -352,12 +358,13 @@ BaseBetaRowsetWriter::~BaseBetaRowsetWriter() {
fmt::format("Failed to delete file={}", seg_path));
}
}
- if (_calc_delete_bitmap_token) {
- _calc_delete_bitmap_token->cancel();
- }
}
BetaRowsetWriter::~BetaRowsetWriter() {
+ // Bitmap callbacks capture this and can call virtual methods using
derived state.
+ if (_calc_delete_bitmap_token != nullptr) {
+ _calc_delete_bitmap_token->cancel();
+ }
/* Note that segcompaction is async and in parallel with load job. So we
should handle carefully
* when the job is cancelled. Although it is meaningless to continue
segcompaction when the job
* is cancelled, the objects involved in the job should be preserved
during segcompaction to
@@ -365,6 +372,12 @@ BetaRowsetWriter::~BetaRowsetWriter() {
WARN_IF_ERROR(_wait_flying_segcompaction(), "segment compaction failed");
}
+void BaseBetaRowsetWriter::cancel_calc_delete_bitmap(const Status& st) {
+ if (_calc_delete_bitmap_token != nullptr) {
+ _calc_delete_bitmap_token->cancel(st);
+ }
+}
+
Status BaseBetaRowsetWriter::init(const RowsetWriterContext&
rowset_writer_context) {
_context = rowset_writer_context;
DCHECK(_context.tablet_schema != nullptr);
@@ -457,6 +470,8 @@ Status
BaseBetaRowsetWriter::_generate_delete_bitmap(int32_t segment_id) {
OlapStopWatch watch;
// Step 2: Build tmp rowset (needs file_writer to be closed)
+
TEST_SYNC_POINT_CALLBACK("BaseBetaRowsetWriter::_generate_delete_bitmap:before_build_tmp",
+ this);
RowsetSharedPtr rowset_ptr;
st = _build_tmp(rowset_ptr);
if (!st.ok()) {
@@ -497,6 +512,7 @@ Status
BaseBetaRowsetWriter::_generate_delete_bitmap(int32_t segment_id) {
<< _context.mow_context->delete_bitmap->cardinality()
<< ", queue_time_us: " << queue_time_us
<< ", cost: " << watch.get_elapse_time_us() << "(us), total
rows: " << total_rows;
+
TEST_SYNC_POINT_CALLBACK("BaseBetaRowsetWriter::_generate_delete_bitmap:finished",
this);
return Status::OK();
});
}
@@ -507,7 +523,8 @@ Status BetaRowsetWriter::init(const RowsetWriterContext&
rowset_writer_context)
_segcompaction_worker->init_mem_tracker(rowset_writer_context);
}
if (_context.mow_context != nullptr) {
- _calc_delete_bitmap_token =
_engine.calc_delete_bitmap_executor_for_load()->create_token();
+ _calc_delete_bitmap_token =
_engine.calc_delete_bitmap_executor_for_load()->create_token(
+ _context.load_cancel_status);
}
return Status::OK();
}
diff --git a/be/src/storage/rowset/beta_rowset_writer.h
b/be/src/storage/rowset/beta_rowset_writer.h
index 49811ac7714..3887919f0da 100644
--- a/be/src/storage/rowset/beta_rowset_writer.h
+++ b/be/src/storage/rowset/beta_rowset_writer.h
@@ -118,6 +118,8 @@ public:
~BaseBetaRowsetWriter() override;
+ void cancel_calc_delete_bitmap(const Status& st) override;
+
Status init(const RowsetWriterContext& rowset_writer_context) override;
Status add_block(const Block* block) override;
diff --git a/be/src/storage/rowset/rowset_writer.h
b/be/src/storage/rowset/rowset_writer.h
index 8bb6cb057a1..a1f38ee4768 100644
--- a/be/src/storage/rowset/rowset_writer.h
+++ b/be/src/storage/rowset/rowset_writer.h
@@ -75,6 +75,9 @@ public:
RowsetWriter() = default;
virtual ~RowsetWriter() = default;
+ // Called after flush producers stop and before owner cleanup.
+ virtual void cancel_calc_delete_bitmap(const Status& st) {}
+
virtual Status init(const RowsetWriterContext& rowset_writer_context) = 0;
virtual Status add_block(const Block* block) {
diff --git a/be/src/storage/rowset/rowset_writer_context.h
b/be/src/storage/rowset/rowset_writer_context.h
index 7c9d4add5ce..3a29a8a4b66 100644
--- a/be/src/storage/rowset/rowset_writer_context.h
+++ b/be/src/storage/rowset/rowset_writer_context.h
@@ -91,6 +91,7 @@ struct RowsetWriterContext {
int64_t txn_id {0};
int64_t txn_expiration {0}; // For cloud mode
PUniqueId load_id;
+ std::shared_ptr<AtomicStatus> load_cancel_status;
TabletUid tablet_uid {0, 0};
// indicate whether the data among segments is overlapping.
// default is OVERLAP_UNKNOWN.
diff --git a/be/src/storage/rowset_builder.cpp
b/be/src/storage/rowset_builder.cpp
index 2620eedf321..c4b7530ce97 100644
--- a/be/src/storage/rowset_builder.cpp
+++ b/be/src/storage/rowset_builder.cpp
@@ -256,7 +256,8 @@ Status RowsetBuilder::init() {
tmp_pending_rowset_ids.begin() + 1);
_pending_rs_guard =
_engine.pending_local_rowsets().add(tmp_pending_rowset_ids);
- _calc_delete_bitmap_token =
_engine.calc_delete_bitmap_executor()->create_token();
+ _calc_delete_bitmap_token =
+
_engine.calc_delete_bitmap_executor()->create_token(_req.load_cancel_status);
_is_init = true;
return Status::OK();
@@ -267,6 +268,7 @@ Status
BaseRowsetBuilder::_init_context_common_fields(RowsetWriterContext& conte
context.txn_id = _req.txn_id;
context.load_id = _req.load_id;
+ context.load_cancel_status = _req.load_cancel_status;
context.db_id = _req.table_schema_param->db_id();
context.table_id = _req.table_schema_param->table_id();
context.rowset_state = PREPARED;
@@ -413,13 +415,17 @@ Status RowsetBuilder::commit_txn() {
return Status::OK();
}
-Status BaseRowsetBuilder::cancel() {
+Status BaseRowsetBuilder::cancel(const Status& st) {
std::lock_guard<std::mutex> l(_lock);
if (_is_cancelled) {
return Status::OK();
}
if (_calc_delete_bitmap_token != nullptr) {
- _calc_delete_bitmap_token->cancel();
+ _calc_delete_bitmap_token->cancel(st);
+ }
+ // Flush-time bitmap work belongs to the rowset writer, not the builder
token.
+ if (_rowset_writer != nullptr) {
+ _rowset_writer->cancel_calc_delete_bitmap(st);
}
_is_cancelled = true;
return Status::OK();
@@ -598,6 +604,12 @@ Status GroupRowsetBuilder::wait_calc_delete_bitmap() {
return _txn_rs_builder->wait_calc_delete_bitmap();
}
+Status GroupRowsetBuilder::cancel(const Status& st) {
+ RETURN_IF_ERROR(_txn_rs_builder->cancel(st));
+ RETURN_IF_ERROR(_row_binlog_rowset_builder->cancel(st));
+ return BaseRowsetBuilder::cancel(st);
+}
+
Status GroupRowsetBuilder::commit_txn() {
// Attach binlog rowset (and its independent binlog tablet) to txn rowset,
so that
// commit/rollback and clean-up are all handled by txn rowset builder.
diff --git a/be/src/storage/rowset_builder.h b/be/src/storage/rowset_builder.h
index 0ff74f5121d..a0eb9f40d54 100644
--- a/be/src/storage/rowset_builder.h
+++ b/be/src/storage/rowset_builder.h
@@ -67,7 +67,7 @@ public:
return Status::NotSupported("BaseRowsetBuilder::commit_txn not
implemented");
}
- Status cancel();
+ virtual Status cancel(const Status& st = Status::Cancelled("rowset builder
cancelled"));
const std::shared_ptr<RowsetWriter>& rowset_writer() const { return
_rowset_writer; }
@@ -206,6 +206,8 @@ public:
Status wait_calc_delete_bitmap() override;
+ Status cancel(const Status& st) override;
+
Status commit_txn() override;
const BaseTabletSPtr& tablet_sptr() const override { return
_txn_rs_builder->tablet_sptr(); }
diff --git a/be/test/load/channel/load_channel_eos_test.cpp
b/be/test/load/channel/load_channel_eos_test.cpp
index ca76dac5c83..624c97ab4b3 100644
--- a/be/test/load/channel/load_channel_eos_test.cpp
+++ b/be/test/load/channel/load_channel_eos_test.cpp
@@ -75,6 +75,7 @@ protected:
channel = std::make_shared<TabletsChannel>(*_engine,
TabletsChannelKey(id, 10),
UniqueId(1, 2), true,
nullptr);
}
+ channel->set_load_cancel_status(_load->_cancel_status);
// No writers are needed to exercise real sender accounting and close.
channel->_state = BaseTabletsChannel::kOpened;
channel->_num_remaining_senders = senders;
@@ -287,7 +288,7 @@ TEST_P(LoadChannelEosTest,
NonWaitingLastSenderReleasesEarlierWaiter) {
EXPECT_EQ(replies, 1);
}
-TEST_P(LoadChannelEosTest, CancelReleasesRegisteredAndNotYetRegisteredRpcs) {
+TEST_P(LoadChannelEosTest,
FinalOwnerReleasesRegisteredAndNotYetRegisteredRpcsAfterCancel) {
auto channel = make_channel(3);
PTabletWriterAddBlockResult response;
std::shared_ptr<EosCompletion> first;
@@ -297,14 +298,17 @@ TEST_P(LoadChannelEosTest,
CancelReleasesRegisteredAndNotYetRegisteredRpcs) {
int replies = 0;
auto callback = [&](const Status& status) {
EXPECT_TRUE(status.is<ErrorCode::CANCELLED>());
- // Re-enter the load lock; cancellation must invoke callbacks outside
it.
- EXPECT_FALSE(_load->is_finished());
+ // Final-owner cleanup invokes RPC callbacks outside the
tablets-channel lock.
+ std::lock_guard lock(channel->_lock);
++replies;
};
first->add_waiter(callback);
EXPECT_TRUE(_load->cancel().ok());
- second->add_waiter(callback);
EXPECT_TRUE(_load->cancel().ok());
+ EXPECT_EQ(replies, 0);
+ _load.reset();
+ EXPECT_EQ(replies, 1);
+ second->add_waiter(callback);
EXPECT_EQ(replies, 2);
}
@@ -322,6 +326,7 @@ TEST_P(LoadChannelEosTest,
TimeoutCleanerReleasesPendingRpc) {
EXPECT_TRUE(manager.get_all_load_channel_ids().empty());
++replies;
});
+ _load.reset(); // The manager is the final owner, as with pending
asynchronous RPCs.
EXPECT_TRUE(manager._start_load_channels_clean().ok());
EXPECT_EQ(replies, 1);
manager.stop();
@@ -481,15 +486,15 @@ TEST_P(LoadChannelEosTest,
ArrivalCallbacksAllowConcurrentRetryAndCancel) {
Defer disable_sync_points {[&] { sp->disable_processing(); }};
std::shared_ptr<EosCompletion> last;
- EXPECT_TRUE(_load->add_batch(eos(1), &response, &last).ok());
+ EXPECT_TRUE(_load->add_batch(eos(1), &response,
&last).is<ErrorCode::CANCELLED>());
last->add_waiter([&](const Status& status) {
EXPECT_TRUE(status.ok());
++replies;
});
EXPECT_EQ(replies, 3);
- EXPECT_EQ(final_closes, 1);
+ EXPECT_EQ(final_closes, 0);
EXPECT_TRUE(_load->is_cancelled());
- EXPECT_TRUE(_load->is_finished());
+ EXPECT_FALSE(_load->is_finished());
}
TEST_P(LoadChannelEosTest, FinalSenderRetryWaitsForCommitFailure) {
diff --git a/be/test/load/channel/load_channel_mgr_test.cpp
b/be/test/load/channel/load_channel_mgr_test.cpp
new file mode 100644
index 00000000000..007592d9641
--- /dev/null
+++ b/be/test/load/channel/load_channel_mgr_test.cpp
@@ -0,0 +1,274 @@
+// 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.
+
+#include "load/channel/load_channel_mgr.h"
+
+#include <gtest/gtest.h>
+
+#include <thread>
+
+#include "runtime/exec_env.h"
+#include "runtime/fragment_mgr.h"
+#include "util/countdown_latch.h"
+
+namespace doris {
+
+class LoadChannelMgrTest : public testing::Test {
+protected:
+ void SetUp() override {
+ auto* exec_env = ExecEnv::GetInstance();
+ if (exec_env->fragment_mgr() == nullptr) {
+ _fragment_mgr = std::make_unique<FragmentMgr>(exec_env);
+ exec_env->_fragment_mgr = _fragment_mgr.get();
+ }
+ _mgr = std::make_unique<LoadChannelMgr>();
+ // Drive timeout cleanup explicitly so the interleavings are
deterministic.
+ _mgr->_load_state_channels =
std::make_unique<LoadChannelMgr::LoadStateChannelCache>(1024);
+ }
+
+ void TearDown() override {
+ _mgr->stop();
+ _mgr.reset();
+ if (_fragment_mgr != nullptr) {
+ _fragment_mgr->stop();
+ ExecEnv::GetInstance()->_fragment_mgr = nullptr;
+ _fragment_mgr.reset();
+ }
+ }
+
+ std::shared_ptr<LoadChannel> create_channel() {
+ auto channel = std::make_shared<LoadChannel>(_load_id, 1, true,
"127.0.0.1", 0, false, -1);
+ std::lock_guard<std::mutex> lock(_mgr->_lock);
+ _mgr->_load_channels[_load_id] = channel;
+ return channel;
+ }
+
+ void expect_no_cached_state() {
+ auto* handle =
_mgr->_load_state_channels->lookup(_load_id.to_string());
+ EXPECT_EQ(handle, nullptr);
+ if (handle != nullptr) {
+ _mgr->_load_state_channels->release(handle);
+ }
+ }
+
+ void expect_cached_failure(const Status& reason) {
+ auto* handle =
_mgr->_load_state_channels->lookup(_load_id.to_string());
+ ASSERT_NE(handle, nullptr);
+ auto* value =
+
static_cast<LoadChannelMgr::CacheValue*>(_mgr->_load_state_channels->value(handle));
+ EXPECT_NE(value, nullptr);
+ if (value != nullptr) {
+ EXPECT_EQ(value->_cancel_reason, reason.msg());
+ }
+ _mgr->_load_state_channels->release(handle);
+ }
+
+ UniqueId _load_id {1, 2};
+ std::unique_ptr<FragmentMgr> _fragment_mgr;
+ std::unique_ptr<LoadChannelMgr> _mgr;
+};
+
+TEST_F(LoadChannelMgrTest, FailedBatchCancelsRetainedChannelAndCachesReason) {
+ auto channel = create_channel();
+ PTabletWriterAddBlockRequest request;
+ *request.mutable_id() = _load_id.to_proto();
+ request.set_index_id(100);
+ PTabletWriterAddBlockResult response;
+ // No index was opened, so this exercises the actual add_batch failure
path.
+ auto st = _mgr->add_batch(request, &response, nullptr);
+ ASSERT_FALSE(st.ok());
+ EXPECT_EQ(channel->cancel_status().to_string(), st.to_string());
+ EXPECT_TRUE(_mgr->_load_channels.empty());
+
+ request.set_eos(true);
+ auto late_status = _mgr->add_batch(request, &response, nullptr);
+ EXPECT_TRUE(late_status.is<ErrorCode::CANCELLED>()) << late_status;
+ EXPECT_EQ(late_status.msg(), st.msg());
+
+ PTabletWriterOpenRequest open_request;
+ *open_request.mutable_id() = _load_id.to_proto();
+ EXPECT_TRUE(_mgr->open(open_request).is<ErrorCode::CANCELLED>());
+}
+
+TEST_F(LoadChannelMgrTest, PublicCancelCachesAlreadyPublishedFailure) {
+ auto channel = create_channel();
+ const auto first_failure = Status::InternalError("first writer failure");
+ CountDownLatch published(1);
+ CountDownLatch resume_failure(1);
+ Status publish_status;
+ Status failure_status;
+ std::thread failed_request([&] {
+ SCOPED_INIT_THREAD_CONTEXT();
+ // Stage the publication separately to model _cancel_load_channel
paused
+ // before taking the manager lock. Its repeated publication is
idempotent.
+ publish_status = channel->cancel(first_failure);
+ published.count_down();
+ resume_failure.wait();
+ failure_status = _mgr->_cancel_load_channel(channel, first_failure);
+ });
+ published.wait();
+
+ PTabletWriterCancelRequest cancel_request;
+ *cancel_request.mutable_id() = _load_id.to_proto();
+ cancel_request.set_cancel_reason("later upstream cancellation");
+ const auto cancel_status = _mgr->cancel(cancel_request);
+ resume_failure.count_down();
+ failed_request.join();
+
+ ASSERT_TRUE(publish_status.ok()) << publish_status;
+ ASSERT_TRUE(cancel_status.ok()) << cancel_status;
+ ASSERT_TRUE(failure_status.ok()) << failure_status;
+ EXPECT_EQ(channel->cancel_status().to_string(), first_failure.to_string());
+ EXPECT_TRUE(_mgr->_load_channels.empty());
+
+ expect_cached_failure(first_failure);
+
+ PTabletWriterOpenRequest open_request;
+ *open_request.mutable_id() = _load_id.to_proto();
+ const auto open_status = _mgr->open(open_request);
+ EXPECT_TRUE(open_status.is<ErrorCode::CANCELLED>()) << open_status;
+ EXPECT_EQ(open_status.msg(), first_failure.msg());
+
+ PTabletWriterAddBlockRequest add_request;
+ *add_request.mutable_id() = _load_id.to_proto();
+ add_request.set_eos(true);
+ PTabletWriterAddBlockResult response;
+ const auto add_status = _mgr->add_batch(add_request, &response, nullptr);
+ EXPECT_TRUE(add_status.is<ErrorCode::CANCELLED>()) << add_status;
+ EXPECT_EQ(add_status.msg(), first_failure.msg());
+}
+
+TEST_F(LoadChannelMgrTest,
PublicCancelKeepsTheSameMessageWithOrWithoutChannel) {
+ PTabletWriterCancelRequest request;
+ *request.mutable_id() = _load_id.to_proto();
+ request.set_cancel_reason("upstream load failed");
+ const auto reason = Status::Cancelled(request.cancel_reason());
+
+ // The no-channel path also covers cancellation arriving before open.
+ for (bool has_channel : {false, true}) {
+ if (has_channel) {
+ create_channel();
+ }
+ ASSERT_TRUE(_mgr->cancel(request).ok());
+ expect_cached_failure(reason);
+
+ PTabletWriterOpenRequest open_request;
+ *open_request.mutable_id() = _load_id.to_proto();
+ auto open_status = _mgr->open(open_request);
+ EXPECT_TRUE(open_status.is<ErrorCode::CANCELLED>());
+ EXPECT_EQ(open_status.msg(), reason.msg());
+
+ PTabletWriterAddBlockRequest add_request;
+ *add_request.mutable_id() = _load_id.to_proto();
+ add_request.set_eos(true);
+ PTabletWriterAddBlockResult response;
+ auto add_status = _mgr->add_batch(add_request, &response, nullptr);
+ EXPECT_TRUE(add_status.is<ErrorCode::CANCELLED>());
+ EXPECT_EQ(add_status.msg(), reason.msg());
+
+ PTabletWriterCancelRequest later_request = request;
+ later_request.set_cancel_reason("later cancellation");
+ ASSERT_TRUE(_mgr->cancel(later_request).ok());
+ expect_cached_failure(reason);
+ _mgr->_load_state_channels->erase(_load_id.to_string());
+ }
+}
+
+TEST_F(LoadChannelMgrTest, EmptyFailureMessageStillRejectsLateRequests) {
+ auto channel = create_channel();
+ const auto reason = Status::InternalError("");
+ ASSERT_TRUE(_mgr->_cancel_load_channel(channel, reason).ok());
+ expect_cached_failure(reason);
+
+ PTabletWriterOpenRequest open_request;
+ *open_request.mutable_id() = _load_id.to_proto();
+ auto open_status = _mgr->open(open_request);
+ EXPECT_TRUE(open_status.is<ErrorCode::CANCELLED>());
+ EXPECT_TRUE(open_status.msg().empty());
+
+ PTabletWriterAddBlockRequest add_request;
+ *add_request.mutable_id() = _load_id.to_proto();
+ add_request.set_eos(true);
+ PTabletWriterAddBlockResult response;
+ auto add_status = _mgr->add_batch(add_request, &response, nullptr);
+ EXPECT_TRUE(add_status.is<ErrorCode::CANCELLED>());
+ EXPECT_TRUE(add_status.msg().empty());
+}
+
+TEST_F(LoadChannelMgrTest, LateFailureDoesNotCancelReplacementAfterTimeout) {
+ auto original = create_channel();
+ CountDownLatch captured(1);
+ CountDownLatch resume_failure(1);
+ Status failure_status;
+ std::thread failed_request([&, channel = original] {
+ SCOPED_INIT_THREAD_CONTEXT();
+ captured.count_down();
+ resume_failure.wait();
+ failure_status = _mgr->_cancel_load_channel(channel,
Status::InternalError("old request"));
+ });
+ captured.wait();
+ original->_last_updated_time.store(0);
+ EXPECT_TRUE(_mgr->_start_load_channels_clean().ok());
+ auto replacement = create_channel();
+ resume_failure.count_down();
+ failed_request.join();
+
+ EXPECT_TRUE(failure_status.ok()) << failure_status;
+ EXPECT_TRUE(original->is_cancelled());
+ EXPECT_FALSE(replacement->is_cancelled());
+ EXPECT_EQ(_mgr->_load_channels.at(_load_id), replacement);
+ expect_no_cached_state();
+}
+
+TEST_F(LoadChannelMgrTest, LateFinishDoesNotRemoveReplacement) {
+ auto original = create_channel();
+ auto replacement = create_channel();
+ _mgr->_finish_load_channel(original);
+ EXPECT_EQ(_mgr->_load_channels.at(_load_id), replacement);
+ EXPECT_FALSE(replacement->is_cancelled());
+ expect_no_cached_state();
+}
+
+TEST_F(LoadChannelMgrTest, FinishDoesNotOverwritePublishedFailure) {
+ auto channel = create_channel();
+ auto reason = Status::InternalError("first failure");
+ ASSERT_TRUE(channel->cancel(reason).ok());
+ // EOS arrives between cancellation publication and removal from the
manager.
+ _mgr->_finish_load_channel(channel);
+ EXPECT_EQ(_mgr->_load_channels.at(_load_id), channel);
+ expect_no_cached_state();
+ ASSERT_TRUE(_mgr->_cancel_load_channel(channel,
Status::InternalError("later failure")).ok());
+ _mgr->_finish_load_channel(channel);
+
+ expect_cached_failure(reason);
+}
+
+TEST_F(LoadChannelMgrTest, FailureStillCancelsOriginalAfterFinish) {
+ auto channel = create_channel();
+ _mgr->_finish_load_channel(channel);
+ ASSERT_TRUE(_mgr->_load_channels.empty());
+ auto reason = Status::InternalError("late failure");
+ ASSERT_TRUE(_mgr->_cancel_load_channel(channel, reason).ok());
+ EXPECT_EQ(channel->cancel_status().to_string(), reason.to_string());
+ // A previously completed manager transition keeps its terminal state.
+ auto* handle = _mgr->_load_state_channels->lookup(_load_id.to_string());
+ ASSERT_NE(handle, nullptr);
+ EXPECT_EQ(_mgr->_load_state_channels->value(handle), nullptr);
+ _mgr->_load_state_channels->release(handle);
+}
+
+} // namespace doris
diff --git a/be/test/load/delta_writer/delta_writer_cancel_integration_test.cpp
b/be/test/load/delta_writer/delta_writer_cancel_integration_test.cpp
new file mode 100644
index 00000000000..56c12708956
--- /dev/null
+++ b/be/test/load/delta_writer/delta_writer_cancel_integration_test.cpp
@@ -0,0 +1,591 @@
+// 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.
+
+#include <gen_cpp/AgentService_types.h>
+#include <gen_cpp/Descriptors_types.h>
+#include <gen_cpp/Types_types.h>
+#include <gtest/gtest.h>
+
+#include <atomic>
+#include <chrono>
+#include <filesystem>
+#include <future>
+#include <memory>
+#include <string>
+#include <thread>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "cloud/cloud_delta_writer.h"
+#include "cloud/cloud_rowset_builder.h"
+#include "cloud/cloud_storage_engine.h"
+#include "cloud/cloud_tablet.h"
+#include "cloud/cloud_tablet_mgr.h"
+#include "cloud/config.h"
+#include "common/config.h"
+#include "core/block/block.h"
+#include "core/field.h"
+#include "cpp/sync_point.h"
+#include "io/fs/local_file_system.h"
+#include "io/fs/remote_file_system.h"
+#include "load/channel/eos_completion.h"
+#include "load/channel/load_channel.h"
+#include "load/channel/tablets_channel.h"
+#include "load/delta_writer/delta_writer.h"
+#include "load/memtable/memtable_flush_executor.h"
+#include "load/memtable/memtable_memory_limiter.h"
+#include "runtime/exec_env.h"
+#include "runtime/fragment_mgr.h"
+#include "runtime/memory/mem_tracker_limiter.h"
+#include "runtime/thread_context.h"
+#include "storage/delete/calc_delete_bitmap_executor.h"
+#include "storage/rowset/beta_rowset_writer.h"
+#include "storage/rowset/rowset_factory.h"
+#include "storage/rowset_builder.h"
+#include "storage/storage_engine.h"
+#include "storage/storage_policy.h"
+#include "storage/tablet/tablet.h"
+#include "storage/tablet/tablet_manager.h"
+#include "storage/tablet/tablet_meta.h"
+#include "storage/tablet_info.h"
+#include "testutil/creators.h"
+#include "util/countdown_latch.h"
+
+namespace doris {
+namespace {
+
+constexpr int64_t kStorageResourceId = 6775971001;
+
+// Exercise cloud writer I/O against real temporary files without an
object-store service.
+class LocalRemoteFileSystem final : public io::RemoteFileSystem {
+public:
+ explicit LocalRemoteFileSystem(std::string root_path)
+ : RemoteFileSystem(std::move(root_path),
std::to_string(kStorageResourceId),
+ io::FileSystemType::BROKER) {}
+
+private:
+ Status create_file_impl(const io::Path& file, io::FileWriterPtr* writer,
+ const io::FileWriterOptions* opts) override {
+ return io::global_local_filesystem()->create_file(file, writer, opts);
+ }
+
+ Status create_directory_impl(const io::Path& dir, bool failed_if_exists)
override {
+ return io::global_local_filesystem()->create_directory(dir,
failed_if_exists);
+ }
+
+ Status delete_file_impl(const io::Path& file) override {
+ return io::global_local_filesystem()->delete_file(file);
+ }
+
+ Status batch_delete_impl(const std::vector<io::Path>& files) override {
+ return io::global_local_filesystem()->batch_delete(files);
+ }
+
+ Status delete_directory_impl(const io::Path& dir) override {
+ return io::global_local_filesystem()->delete_directory(dir);
+ }
+
+ Status exists_impl(const io::Path& path, bool* res) const override {
+ return io::global_local_filesystem()->exists(path, res);
+ }
+
+ Status file_size_impl(const io::Path& file, int64_t* file_size) const
override {
+ return io::global_local_filesystem()->file_size(file, file_size);
+ }
+
+ Status list_impl(const io::Path& dir, bool only_file,
std::vector<io::FileInfo>* files,
+ bool* exists) override {
+ return io::global_local_filesystem()->list(dir, only_file, files,
exists);
+ }
+
+ Status rename_impl(const io::Path& orig_name, const io::Path& new_name)
override {
+ return io::global_local_filesystem()->rename(orig_name, new_name);
+ }
+
+ Status upload_impl(const io::Path& local_file, const io::Path&
remote_file) override {
+ return io::global_local_filesystem()->link_file(local_file,
remote_file);
+ }
+
+ Status batch_upload_impl(const std::vector<io::Path>& local_files,
+ const std::vector<io::Path>& remote_files)
override {
+ DCHECK_EQ(local_files.size(), remote_files.size());
+ for (size_t i = 0; i < local_files.size(); ++i) {
+ RETURN_IF_ERROR(upload_impl(local_files[i], remote_files[i]));
+ }
+ return Status::OK();
+ }
+
+ Status download_impl(const io::Path& remote_file, const io::Path&
local_file) override {
+ return io::global_local_filesystem()->link_file(remote_file,
local_file);
+ }
+
+ Status open_file_internal(const io::Path& file, io::FileReaderSPtr* reader,
+ const io::FileReaderOptions& opts) override {
+ return io::global_local_filesystem()->open_file(file, reader, &opts);
+ }
+};
+
+} // namespace
+
+class DeltaWriterCancelIntegrationTest : public testing::TestWithParam<bool> {
+protected:
+ static constexpr int64_t kTabletId = 71001;
+ static constexpr int64_t kIndexId = 72001;
+
+ void SetUp() override {
+ auto* env = ExecEnv::GetInstance();
+ _saved_engine = std::move(env->_storage_engine);
+ _saved_limiter = std::move(env->_memtable_memory_limiter);
+ _saved_fragment_mgr = env->_fragment_mgr;
+ _saved_cloud_id = config::cloud_unique_id;
+ _saved_deploy_mode = config::deploy_mode;
+ _saved_storage_root = config::storage_root_path;
+ _saved_sync_rowsets =
config::cloud_mow_sync_rowsets_when_load_txn_begin;
+ _saved_file_cache = config::enable_file_cache;
+ _saved_between_segments =
config::enable_calc_delete_bitmap_between_segments_concurrently;
+ config::cloud_unique_id = GetParam() ? "bitmap-cancel-ut" : "";
+ config::deploy_mode = GetParam() ? "cloud" : "local";
+ config::cloud_mow_sync_rowsets_when_load_txn_begin = false;
+ config::enable_file_cache = false;
+ config::enable_calc_delete_bitmap_between_segments_concurrently = true;
+ _root = (std::filesystem::current_path() / "ut_dir" /
+ (GetParam() ? "bitmap_cancel_cloud" : "bitmap_cancel_local"))
+ .string();
+ config::storage_root_path = _root;
+
ASSERT_TRUE(io::global_local_filesystem()->delete_directory(_root).ok());
+
ASSERT_TRUE(io::global_local_filesystem()->create_directory(_root).ok());
+ _attach =
std::make_unique<AttachTask>(MemTrackerLimiter::create_shared(
+ MemTrackerLimiter::Type::OTHER,
"DeltaWriterCancelIntegrationTest"));
+
+ _fragment_mgr = std::make_unique<FragmentMgr>(env);
+ env->_fragment_mgr = _fragment_mgr.get();
+ env->set_memtable_memory_limiter(new MemTableMemoryLimiter());
+ if (GetParam()) {
+ auto engine = std::make_unique<CloudStorageEngine>(EngineOptions
{});
+ engine->init_calc_delete_bitmap_executor_for_UT();
+ engine->_memtable_flush_executor =
std::make_unique<MemTableFlushExecutor>();
+ engine->_memtable_flush_executor->init(1);
+ auto fs = std::make_shared<LocalRemoteFileSystem>(_root);
+ engine->set_latest_fs(fs);
+ // Temporary rowsets copy the resource ID through protobuf
metadata and
+ // resolve it from the registry when bitmap callbacks reopen their
segments.
+ put_storage_resource(kStorageResourceId, StorageResource {fs}, 0);
+ env->set_storage_engine(std::move(engine));
+ } else {
+ EngineOptions options;
+ options.store_paths.emplace_back(_root, -1);
+ auto engine = std::make_unique<StorageEngine>(options);
+ ASSERT_TRUE(engine->open().ok());
+ env->set_storage_engine(std::move(engine));
+ }
+ _engine = &env->storage_engine();
+ _load_channel = std::make_shared<LoadChannel>(UniqueId {71, 1}, 60,
false, "", 0, true, -1);
+ create_tablet(kTabletId);
+ }
+
+ void TearDown() override {
+ // Unblock callbacks on assertion failure, but do not pre-drain the
executor:
+ // writer destruction itself must enforce the lifetime guarantee.
+ _release_bitmap.count_down();
+ if (_cleanup.valid()) {
+ _cleanup.get();
+ }
+ _load_channel.reset();
+ _tablet.reset();
+ auto* sp = SyncPoint::get_instance();
+ sp->disable_processing();
+ _guards.clear();
+ sp->clear_trace();
+ auto* env = ExecEnv::GetInstance();
+ env->set_memtable_memory_limiter(nullptr);
+ env->set_storage_engine(nullptr);
+ // All cloud writers and callbacks must finish before their resource
is removed.
+ if (GetParam()) {
+ delete_storage_resource(kStorageResourceId);
+ }
+ env->_storage_engine = std::move(_saved_engine);
+ env->_memtable_memory_limiter = std::move(_saved_limiter);
+ if (_fragment_mgr) {
+ _fragment_mgr->stop();
+ env->_fragment_mgr = _saved_fragment_mgr;
+ _fragment_mgr.reset();
+ }
+ _attach.reset();
+
EXPECT_TRUE(io::global_local_filesystem()->delete_directory(_root).ok());
+ config::cloud_unique_id = _saved_cloud_id;
+ config::deploy_mode = _saved_deploy_mode;
+ config::storage_root_path = _saved_storage_root;
+ config::cloud_mow_sync_rowsets_when_load_txn_begin =
_saved_sync_rowsets;
+ config::enable_file_cache = _saved_file_cache;
+ config::enable_calc_delete_bitmap_between_segments_concurrently =
_saved_between_segments;
+ }
+
+ void create_tablet(int64_t id) {
+ _request = testutil::create_tablet_request(
+ id, 73001, 74001, 1, TKeysType::UNIQUE_KEYS,
+ {{"k1", TPrimitiveType::INT, true}, {"v1",
TPrimitiveType::INT, false}});
+ _request.__set_enable_unique_key_merge_on_write(true);
+ if (GetParam()) {
+ std::unordered_map<uint32_t, uint32_t> ids {{0, 0}, {1, 1}};
+ auto meta = TabletMeta::create(_request, TabletUid::gen_uid(), 0,
2, ids);
+ auto tablet = std::make_shared<CloudTablet>(_engine->to_cloud(),
meta);
+ auto rs_meta = std::make_shared<RowsetMeta>();
+ rs_meta->set_rowset_type(BETA_ROWSET);
+ rs_meta->set_rowset_state(VISIBLE);
+ rs_meta->set_tablet_id(id);
+
rs_meta->set_tablet_schema_hash(_request.tablet_schema.schema_hash);
+ rs_meta->set_tablet_schema(tablet->tablet_schema());
+ rs_meta->set_version(Version(0, 1));
+ rs_meta->set_segments_overlap(OVERLAP_UNKNOWN);
+ RowsetSharedPtr rowset;
+ ASSERT_TRUE(RowsetFactory::create_rowset(tablet->tablet_schema(),
"", rs_meta, &rowset)
+ .ok());
+ {
+ std::unique_lock meta_lock(tablet->get_header_lock());
+ tablet->add_rowsets({rowset}, false, meta_lock);
+ tablet->set_cumulative_layer_point(2);
+ }
+ _engine->to_cloud().tablet_mgr().put_tablet_for_UT(tablet);
+ ASSERT_TRUE(io::global_local_filesystem()
+ ->create_directory(fmt::format("{}/data/{}",
_root, id))
+ .ok());
+ _tablet = tablet;
+ } else {
+ ASSERT_TRUE(
+ _engine->to_local().create_tablet(_request,
_load_channel->_self_profile).ok());
+ _tablet = _engine->to_local().tablet_manager()->get_tablet(id);
+ }
+ ASSERT_NE(_tablet, nullptr);
+ }
+
+ PTabletWriterOpenRequest open_request(int64_t id, bool incremental =
false) {
+ auto desc = testutil::create_descriptor_table(
+ {{TYPE_INT, "k1", false}, {TYPE_INT, "v1", false}});
+ auto schema = testutil::create_table_schema_param(
+ desc, kIndexId, _request.tablet_schema.schema_hash,
_request.tablet_schema.columns);
+ PTabletWriterOpenRequest request;
+ request.mutable_id()->set_hi(71);
+ request.mutable_id()->set_lo(1);
+ request.set_index_id(kIndexId);
+ request.set_txn_id(75001);
+ request.set_txn_expiration(4102444800);
+ request.set_num_senders(1);
+ request.set_sender_id(0);
+ request.set_is_incremental(incremental);
+ request.set_is_high_priority(_load_channel->is_high_priority());
+ request.set_enable_profile(true);
+ schema->to_protobuf(request.mutable_schema());
+ auto* tablet = request.add_tablets();
+ tablet->set_tablet_id(id);
+ tablet->set_partition_id(_request.partition_id);
+ return request;
+ }
+
+ Status open_writer(int64_t id, bool incremental = false) {
+ auto request = open_request(id, incremental);
+ Status status;
+ // open() attaches the load task itself. Model its RPC worker
separately
+ // from the fixture's attached task to avoid nesting AttachTask.
+ std::thread rpc([&] { status = _load_channel->open(request); });
+ rpc.join();
+ RETURN_IF_ERROR(status);
+ _writer =
_load_channel->_tablets_channels.at(kIndexId)->_tablet_writers.at(id).get();
+ _builder = _writer->_rowset_builder.get();
+ if (GetParam()) {
+ // Only metadata RPCs are skipped; builders and both executors
initialize normally.
+
static_cast<CloudRowsetBuilder*>(_builder)->set_skip_writing_rowset_metadata(true);
+ }
+ return Status::OK();
+ }
+
+ Block block() {
+ auto result = _tablet->tablet_schema()->create_storage_block();
+ {
+ auto guard = result.mutate_columns_scoped();
+ auto& columns = guard.mutable_columns();
+
columns[0]->insert(Field::create_field<PrimitiveType::TYPE_INT>(1));
+
columns[1]->insert(Field::create_field<PrimitiveType::TYPE_INT>(10));
+ }
+ return result;
+ }
+
+ void check_wiring() {
+ _rowset_writer =
static_cast<BaseBetaRowsetWriter*>(_builder->rowset_writer().get());
+ const auto& cancellation = _load_channel->_cancel_status;
+ ASSERT_EQ(_writer->_req.load_cancel_status, cancellation);
+ ASSERT_EQ(_builder->_req.load_cancel_status, cancellation);
+ ASSERT_EQ(_rowset_writer->context().load_cancel_status, cancellation);
+ ASSERT_NE(_builder->_calc_delete_bitmap_token, nullptr);
+ ASSERT_NE(_rowset_writer->_calc_delete_bitmap_token, nullptr);
+ EXPECT_EQ(_builder->_calc_delete_bitmap_token->_load_cancel_status,
cancellation);
+
EXPECT_EQ(_rowset_writer->_calc_delete_bitmap_token->_load_cancel_status,
cancellation);
+ EXPECT_EQ(_builder->_calc_delete_bitmap_token->_thread_token->_pool,
+ _engine->calc_delete_bitmap_executor()->_thread_pool.get());
+
EXPECT_EQ(_rowset_writer->_calc_delete_bitmap_token->_thread_token->_pool,
+
_engine->calc_delete_bitmap_executor_for_load()->_thread_pool.get());
+ EXPECT_NE(_engine->calc_delete_bitmap_executor()->_thread_pool.get(),
+
_engine->calc_delete_bitmap_executor_for_load()->_thread_pool.get());
+ }
+
+ template <typename Callback>
+ void on(const std::string& point, Callback&& callback) {
+ _guards.emplace_back();
+ SyncPoint::get_instance()->set_call_back(point,
std::forward<Callback>(callback),
+ &_guards.back());
+ SyncPoint::get_instance()->enable_processing();
+ }
+
+ void block_bitmap_and_observe_cleanup() {
+ _segment_path = _rowset_writer->context().segment_path(0);
+ if (GetParam()) {
+ _segment_path = (_engine->to_cloud().latest_fs()->root_path() /
_segment_path).string();
+ }
+ on("BaseBetaRowsetWriter::_generate_delete_bitmap:before_build_tmp",
[&](auto&& args) {
+ if (try_any_cast<BaseBetaRowsetWriter*>(args[0]) ==
_rowset_writer) {
+ _bitmap_started.count_down();
+ _release_bitmap.wait();
+ bool exists = false;
+
EXPECT_TRUE(io::global_local_filesystem()->exists(_segment_path, &exists).ok());
+ EXPECT_TRUE(exists);
+ EXPECT_FALSE(_cleanup_started.load());
+ }
+ });
+ on("BaseBetaRowsetWriter::_generate_delete_bitmap:finished",
[&](auto&& args) {
+ if (try_any_cast<BaseBetaRowsetWriter*>(args[0]) ==
_rowset_writer) {
+ _bitmap_finished = true;
+ }
+ });
+ on("BaseBetaRowsetWriter::~BaseBetaRowsetWriter:before_file_cleanup",
[&](auto&& args) {
+ if (try_any_cast<BaseBetaRowsetWriter*>(args[0]) ==
_rowset_writer) {
+ EXPECT_TRUE(_bitmap_finished.load());
+ _cleanup_started = true;
+ }
+ });
+ on("CloudRowsetBuilder::~CloudRowsetBuilder:before_clear_cache",
[&](auto&& args) {
+ if (try_any_cast<CloudRowsetBuilder*>(args[0]) == _builder) {
+ EXPECT_TRUE(_bitmap_finished.load());
+ _cleanup_started = true;
+ }
+ });
+ }
+
+ void start_cleanup() {
+ auto resource_ctx = _load_channel->_resource_ctx;
+ auto& owner =
_load_channel->_tablets_channels.at(kIndexId)->_tablet_writers.at(kTabletId);
+ _cleanup =
+ std::async(std::launch::async, [resource_ctx, writer =
std::move(owner)]() mutable {
+ SCOPED_ATTACH_TASK(resource_ctx);
+ writer.reset();
+ });
+ EXPECT_EQ(_cleanup.wait_for(std::chrono::milliseconds(100)),
std::future_status::timeout);
+ EXPECT_FALSE(_cleanup_started.load());
+ EXPECT_FALSE(_bitmap_finished.load());
+ _release_bitmap.count_down();
+ _cleanup.get();
+ EXPECT_TRUE(_bitmap_finished.load());
+ }
+
+ Status flush_two_memtables() {
+ auto data = block();
+ for (int i = 0; i < 2; ++i) {
+ RETURN_IF_ERROR(_writer->write(&data, TabletAddRowsPayload
{.row_idxs = {0}}));
+ RETURN_IF_ERROR(_writer->flush_memtable_async());
+ RETURN_IF_ERROR(_writer->wait_flush());
+ }
+ return _rowset_writer->_calc_delete_bitmap_token->wait();
+ }
+
+ void exercise_both_phases() {
+ ASSERT_TRUE(_writer->init().ok());
+ check_wiring();
+ auto st = flush_two_memtables();
+ ASSERT_TRUE(st.ok()) << st;
+ ASSERT_TRUE(_writer->close().ok());
+ ASSERT_TRUE(_writer->build_rowset().ok());
+ std::atomic<int> builder_submissions {0};
+ on("CalcDeleteBitmapToken::submit_func:before_submit", [&](auto&&
args) {
+ if (try_any_cast<CalcDeleteBitmapToken*>(args[0]) ==
+ _builder->_calc_delete_bitmap_token.get()) {
+ ++builder_submissions;
+ }
+ });
+ ASSERT_TRUE(_writer->submit_calc_delete_bitmap_task().ok());
+ ASSERT_TRUE(_writer->wait_calc_delete_bitmap().ok());
+ EXPECT_GT(builder_submissions.load(), 0);
+ SyncPoint::get_instance()->clear_call_back(
+ "CalcDeleteBitmapToken::submit_func:before_submit");
+ }
+
+ Status prepare_partial_init_callback() {
+ check_wiring();
+ auto data = block();
+ if (GetParam()) {
+ // Keep a real temporary PREPARED rowset in the builder to
exercise its
+ // cache-cleanup branch. build() would instead mark the rowset
COMMITTED.
+ RETURN_IF_ERROR(_rowset_writer->flush_single_block(&data));
+ RETURN_IF_ERROR(_rowset_writer->_calc_delete_bitmap_token->wait());
+ RETURN_IF_ERROR(_rowset_writer->_build_tmp(_builder->_rowset));
+ EXPECT_EQ(_builder->rowset()->rowset_meta()->rowset_state(),
PREPARED);
+ block_bitmap_and_observe_cleanup();
+ // Resubmit the real callback against the already-created segment.
+ return _rowset_writer->_generate_delete_bitmap(0);
+ }
+ block_bitmap_and_observe_cleanup();
+ return _rowset_writer->flush_single_block(&data);
+ }
+
+ std::string _root;
+ std::string _saved_cloud_id;
+ std::string _saved_deploy_mode;
+ std::string _saved_storage_root;
+ bool _saved_sync_rowsets = false;
+ bool _saved_file_cache = false;
+ bool _saved_between_segments = false;
+ std::unique_ptr<BaseStorageEngine> _saved_engine;
+ std::unique_ptr<MemTableMemoryLimiter> _saved_limiter;
+ FragmentMgr* _saved_fragment_mgr = nullptr;
+ std::unique_ptr<FragmentMgr> _fragment_mgr;
+ std::unique_ptr<AttachTask> _attach;
+ BaseStorageEngine* _engine = nullptr;
+ BaseTabletSPtr _tablet;
+ TCreateTabletReq _request;
+ std::shared_ptr<LoadChannel> _load_channel;
+ BaseDeltaWriter* _writer = nullptr;
+ BaseRowsetBuilder* _builder = nullptr;
+ BaseBetaRowsetWriter* _rowset_writer = nullptr;
+ std::string _segment_path;
+ std::vector<SyncPoint::CallbackGuard> _guards;
+ CountDownLatch _bitmap_started {1};
+ CountDownLatch _release_bitmap {1};
+ std::atomic<bool> _bitmap_finished {false};
+ std::atomic<bool> _cleanup_started {false};
+ std::future<void> _cleanup;
+};
+
+TEST_P(DeltaWriterCancelIntegrationTest,
OrdinaryAndIncrementalOpenWireBothPhases) {
+ for (int i = 0; i < 2; ++i) {
+ if (i != 0) {
+ create_tablet(kTabletId + i);
+ }
+ ASSERT_TRUE(open_writer(kTabletId + i, i != 0).ok());
+ ASSERT_NO_FATAL_FAILURE(exercise_both_phases());
+ }
+ ASSERT_TRUE(_load_channel->cancel().ok());
+ for (const auto& [_, writer] :
_load_channel->_tablets_channels.at(kIndexId)->_tablet_writers) {
+ auto* builder = writer->_rowset_builder.get();
+ auto* rowset_writer =
static_cast<BaseBetaRowsetWriter*>(builder->rowset_writer().get());
+
EXPECT_TRUE(builder->_calc_delete_bitmap_token->wait().is<ErrorCode::CANCELLED>());
+
EXPECT_TRUE(rowset_writer->_calc_delete_bitmap_token->wait().is<ErrorCode::CANCELLED>());
+ }
+}
+
+TEST_P(DeltaWriterCancelIntegrationTest,
PublicationReturnsBeforeFinalOwnerDrainsRealCallback) {
+ ASSERT_TRUE(open_writer(kTabletId).ok());
+ ASSERT_TRUE(_writer->init().ok());
+ check_wiring();
+ auto data = block();
+ block_bitmap_and_observe_cleanup();
+ ASSERT_TRUE(_rowset_writer->flush_single_block(&data).ok());
+ ASSERT_TRUE(_bitmap_started.wait_for(std::chrono::seconds(10)));
+ ASSERT_TRUE(_load_channel->cancel().ok());
+ EXPECT_FALSE(_bitmap_finished.load());
+ EXPECT_FALSE(_cleanup_started.load());
+ start_cleanup();
+ _load_channel.reset();
+ if (!GetParam()) {
+ EXPECT_TRUE(_cleanup_started.load());
+ bool exists = true;
+ ASSERT_TRUE(io::global_local_filesystem()->exists(_segment_path,
&exists).ok());
+ EXPECT_FALSE(exists);
+ }
+}
+
+TEST_P(DeltaWriterCancelIntegrationTest,
PartialInitDestructionDrainsBeforeFileOrCacheCleanup) {
+ ASSERT_TRUE(open_writer(kTabletId).ok());
+ const auto failure = Status::InternalError("memtable initialization
failed");
+ on("BaseDeltaWriter::init:before_memtable_init", [&](auto&& args) {
+ if (try_any_cast<BaseDeltaWriter*>(args[0]) != _writer) {
+ return;
+ }
+ auto st = prepare_partial_init_callback();
+ EXPECT_TRUE(st.ok()) << st;
+ auto* ret = try_any_cast_ret<Status>(args);
+ ret->first = st.ok() ? failure : st;
+ ret->second = true;
+ });
+ EXPECT_EQ(_writer->init(), failure);
+ ASSERT_FALSE(_writer->_is_init);
+ ASSERT_TRUE(_builder->_is_init);
+ ASSERT_TRUE(_bitmap_started.wait_for(std::chrono::seconds(10)));
+ start_cleanup();
+ EXPECT_TRUE(_cleanup_started.load());
+ if (!GetParam()) {
+ bool exists = true;
+ ASSERT_TRUE(io::global_local_filesystem()->exists(_segment_path,
&exists).ok());
+ EXPECT_FALSE(exists);
+ }
+}
+
+TEST_P(DeltaWriterCancelIntegrationTest,
FinalOwnerCompletesPendingEosBeforeDestroyingWriters) {
+ ASSERT_TRUE(open_writer(kTabletId).ok());
+ auto* tablets = _load_channel->_tablets_channels.at(kIndexId).get();
+ auto completion = tablets->eos_completion();
+ const auto reason = Status::InternalError("cancel before all EOS senders
arrive");
+ int replies = 0;
+ completion->add_waiter([&](const Status& status) {
+ EXPECT_EQ(status, reason);
+ EXPECT_EQ(tablets->_tablet_writers.at(kTabletId).get(), _writer);
+ ++replies;
+ });
+ {
+ // Publishing cancellation must not take either channel lock or run
RPC callbacks.
+ std::lock_guard load_lock(_load_channel->_lock);
+ std::lock_guard tablets_lock(tablets->_lock);
+ ASSERT_TRUE(_load_channel->cancel(reason).ok());
+ EXPECT_EQ(replies, 0);
+ }
+ _load_channel.reset();
+ EXPECT_EQ(replies, 1);
+ // The service may register after add_batch released the last channel
owner.
+ completion->add_waiter([&](const Status& status) {
+ EXPECT_EQ(status, reason);
+ ++replies;
+ });
+ EXPECT_EQ(replies, 2);
+}
+
+TEST_P(DeltaWriterCancelIntegrationTest,
PublishedCancellationWinsBeforeEosArrival) {
+ ASSERT_TRUE(open_writer(kTabletId).ok());
+ auto* tablets = _load_channel->_tablets_channels.at(kIndexId).get();
+ const auto reason = Status::Cancelled("cancel before final EOS sender");
+ ASSERT_TRUE(_load_channel->cancel(reason).ok());
+ int replies = 0;
+ tablets->eos_completion()->add_waiter([&](const Status& status) {
+ EXPECT_EQ(status, reason);
+ ++replies;
+ });
+ PTabletWriterAddBlockResult response;
+ std::unique_lock lock(tablets->_lock);
+ tablets->_num_remaining_senders = 0;
+ EXPECT_EQ(tablets->_close_and_notify(lock, 0, &response), reason);
+ EXPECT_EQ(replies, 1);
+}
+
+INSTANTIATE_TEST_SUITE_P(LocalCloud, DeltaWriterCancelIntegrationTest,
testing::Bool());
+
+} // namespace doris
diff --git a/be/test/load/delta_writer/delta_writer_cancel_test.cpp
b/be/test/load/delta_writer/delta_writer_cancel_test.cpp
new file mode 100644
index 00000000000..a1170c48c3f
--- /dev/null
+++ b/be/test/load/delta_writer/delta_writer_cancel_test.cpp
@@ -0,0 +1,295 @@
+// 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.
+
+#include <gtest/gtest.h>
+
+#include <atomic>
+#include <chrono>
+#include <future>
+#include <memory>
+#include <mutex>
+#include <thread>
+#include <vector>
+
+#include "cloud/cloud_delta_writer.h"
+#include "cloud/cloud_rowset_builder.h"
+#include "cloud/cloud_rowset_writer.h"
+#include "cloud/cloud_storage_engine.h"
+#include "load/channel/load_channel.h"
+#include "load/channel/tablets_channel.h"
+#include "load/delta_writer/delta_writer.h"
+#include "runtime/exec_env.h"
+#include "runtime/fragment_mgr.h"
+#include "runtime/memory/mem_tracker_limiter.h"
+#include "runtime/thread_context.h"
+#include "storage/delete/calc_delete_bitmap_executor.h"
+#include "storage/options.h"
+#include "storage/rowset/beta_rowset_writer.h"
+#include "storage/rowset/group_rowset_writer.h"
+#include "storage/rowset/rowset_writer_context.h"
+#include "storage/rowset_builder.h"
+#include "storage/storage_engine.h"
+#include "util/countdown_latch.h"
+#include "util/defer_op.h"
+
+namespace doris {
+
+// Exercise both local/cloud writers, with and without row binlog, without
tablet I/O.
+class DeltaWriterCancelTest : public testing::TestWithParam<int> {
+protected:
+ void SetUp() override {
+ auto tracker =
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER,
+
"DeltaWriterCancelTest");
+ _attach_task = std::make_unique<AttachTask>(tracker);
+ ASSERT_TRUE(ThreadPoolBuilder("DeltaWriterCancelTest")
+ .set_min_threads(1)
+ .set_max_threads(1)
+ .build(&_pool)
+ .ok());
+ _previous_fragment_mgr = ExecEnv::GetInstance()->_fragment_mgr;
+ _fragment_mgr = std::make_unique<FragmentMgr>(ExecEnv::GetInstance());
+ ExecEnv::GetInstance()->_fragment_mgr = _fragment_mgr.get();
+ _load_channel = std::make_shared<LoadChannel>(UniqueId {}, 60, false,
"", 0, false, -1);
+ WriteRequest data_req;
+ data_req.load_cancel_status = _load_channel->_cancel_status;
+ WriteRequest group_req = data_req;
+ group_req.write_req_type = WriteRequestType::GROUP;
+ WriteRequest binlog_req = data_req;
+ binlog_req.write_req_type = WriteRequestType::ROW_BINLOG;
+ if (is_cloud()) {
+ _cloud_engine = std::make_unique<CloudStorageEngine>(EngineOptions
{});
+ if (is_group()) {
+ _writer = std::make_unique<CloudDeltaWriter>(*_cloud_engine,
group_req, data_req,
+ binlog_req,
nullptr, UniqueId {});
+ auto* group =
static_cast<CloudGroupRowsetBuilder*>(_writer->_rowset_builder.get());
+ _builders = {group->data_builder(),
group->row_binlog_builder()};
+ } else {
+ _writer = std::make_unique<CloudDeltaWriter>(*_cloud_engine,
data_req, nullptr,
+ UniqueId {});
+ }
+ } else {
+ _local_engine = std::make_unique<StorageEngine>(EngineOptions {});
+ if (is_group()) {
+ _writer = std::make_unique<DeltaWriter>(*_local_engine,
group_req, data_req,
+ binlog_req, nullptr,
UniqueId {});
+ auto* group =
static_cast<GroupRowsetBuilder*>(_writer->_rowset_builder.get());
+ _builders = {group->txn_rowset_builder(),
group->row_binlog_builder()};
+ } else {
+ _writer = std::make_unique<DeltaWriter>(*_local_engine,
data_req, nullptr,
+ UniqueId {});
+ }
+ }
+ if (!is_group()) {
+ _builders = {_writer->_rowset_builder.get()};
+ }
+ }
+
+ void TearDown() override {
+ // Release the unrelated task before destroying writers, even after a
failed assertion.
+ _release_worker.count_down();
+ if (_pool) {
+ _pool->wait();
+ }
+ _writer.reset();
+ _tokens.clear();
+ _pool.reset();
+ _cloud_engine.reset();
+ _local_engine.reset();
+ _load_channel.reset();
+ if (_fragment_mgr) {
+ _fragment_mgr->stop();
+ ExecEnv::GetInstance()->_fragment_mgr = _previous_fragment_mgr;
+ _fragment_mgr.reset();
+ }
+ _attach_task.reset();
+ }
+
+ bool is_cloud() const { return GetParam() & 1; }
+ bool is_group() const { return GetParam() & 2; }
+
+ void install_tokens() {
+ for (auto* builder : _builders) {
+ std::shared_ptr<BaseBetaRowsetWriter> rowset_writer;
+ if (is_cloud()) {
+ rowset_writer =
std::make_shared<CloudRowsetWriter>(*_cloud_engine);
+ rowset_writer->_rowset_meta = std::make_shared<RowsetMeta>();
+ } else {
+ rowset_writer =
std::make_shared<BetaRowsetWriter>(*_local_engine);
+ // Local writer destruction requires the segcompaction tracker
that init()
+ // creates, even when these cancellation tests never create
segment files.
+ RowsetWriterContext context;
+ context.tablet_schema = std::make_shared<TabletSchema>();
+ ASSERT_TRUE(rowset_writer->init(context).ok());
+ }
+ // Set up only the state needed by cancellation and destruction.
No files are created.
+ rowset_writer->_calc_delete_bitmap_token = make_token();
+ builder->_calc_delete_bitmap_token = make_token();
+ builder->_rowset_writer = rowset_writer;
+ _tokens.push_back(rowset_writer->_calc_delete_bitmap_token.get());
+ _tokens.push_back(builder->_calc_delete_bitmap_token.get());
+ }
+ if (is_group()) {
+ auto group_writer = std::make_shared<GroupRowsetWriter>();
+ group_writer->set_data_writer(_builders[0]->rowset_writer());
+ group_writer->set_row_binlog_writer(_builders[1]->rowset_writer());
+ _writer->_rowset_builder->_rowset_writer = std::move(group_writer);
+ }
+ }
+
+ std::unique_ptr<CalcDeleteBitmapToken> make_token() {
+ return std::make_unique<CalcDeleteBitmapToken>(
+ _pool->new_token(ThreadPool::ExecutionMode::CONCURRENT),
+ _load_channel->_cancel_status);
+ }
+
+ FragmentMgr* _previous_fragment_mgr = nullptr;
+ std::unique_ptr<FragmentMgr> _fragment_mgr;
+ std::shared_ptr<LoadChannel> _load_channel;
+ std::unique_ptr<AttachTask> _attach_task;
+ std::unique_ptr<StorageEngine> _local_engine;
+ std::unique_ptr<CloudStorageEngine> _cloud_engine;
+ std::unique_ptr<ThreadPool> _pool;
+ std::unique_ptr<BaseDeltaWriter> _writer;
+ std::vector<BaseRowsetBuilder*> _builders;
+ std::vector<CalcDeleteBitmapToken*> _tokens;
+ CountDownLatch _worker_started {1};
+ CountDownLatch _release_worker {1};
+ std::atomic<int> _executed {0};
+};
+
+TEST_P(DeltaWriterCancelTest, PublicationSkipsQueuedWorkWithoutDraining) {
+ install_tokens();
+ ASSERT_TRUE(_pool->submit_func([this] {
+ _worker_started.count_down();
+ _release_worker.wait();
+ }).ok());
+ ASSERT_TRUE(_worker_started.wait_for(std::chrono::seconds(10)));
+ for (auto* token : _tokens) {
+ ASSERT_TRUE(token->submit_func([this] {
+ ++_executed;
+ return Status::OK();
+ }).ok());
+ }
+ const auto queued = _pool->get_queue_size();
+ const auto reason = Status::Cancelled("sender cancelled the load");
+ // Holding owner locks must not prevent publishing cancellation.
+ {
+ std::lock_guard channel_lock(_load_channel->_lock);
+ std::lock_guard builder_lock(_builders.front()->_lock);
+ ASSERT_TRUE(_load_channel->cancel(reason).ok());
+ }
+ EXPECT_EQ(_pool->get_queue_size(), queued);
+ for (auto* token : _tokens) {
+ EXPECT_EQ(token->submit_func([] { return Status::OK(); }), reason);
+ }
+ _release_worker.count_down();
+ for (auto* token : _tokens) {
+ EXPECT_EQ(token->wait(), reason);
+ }
+ EXPECT_EQ(_executed.load(), 0);
+}
+
+TEST_P(DeltaWriterCancelTest, FinalOwnerDiscardsBothQueuesAfterPartialInit) {
+ install_tokens();
+ ASSERT_FALSE(_writer->_is_init);
+ ASSERT_TRUE(_pool->submit_func([this] {
+ _worker_started.count_down();
+ _release_worker.wait();
+ }).ok());
+ ASSERT_TRUE(_worker_started.wait_for(std::chrono::seconds(10)));
+ for (auto* token : _tokens) {
+ ASSERT_TRUE(token->submit_func([this] {
+ ++_executed;
+ return Status::OK();
+ }).ok());
+ }
+ ASSERT_TRUE(_load_channel->cancel().ok());
+ // No bitmap callback is running. Final-owner cleanup must discard both
phases
+ // without waiting for the unrelated task to release the sole pool worker.
+ _writer.reset();
+ _tokens.clear();
+ EXPECT_EQ(_pool->get_queue_size(), 0);
+ EXPECT_EQ(_executed.load(), 0);
+}
+
+TEST_P(DeltaWriterCancelTest, FinalOwnerWaitsForRunningCallback) {
+ install_tokens();
+ CountDownLatch destroying(1);
+ std::future<void> destructor;
+ Defer cleanup {[&] {
+ _release_worker.count_down();
+ if (destructor.valid()) {
+ destructor.get();
+ }
+ }};
+ ASSERT_TRUE(_tokens.front()
+ ->submit_func([this] {
+ _worker_started.count_down();
+ _release_worker.wait();
+ ++_executed;
+ return Status::InternalError("late bitmap
failure");
+ })
+ .ok());
+ ASSERT_TRUE(_worker_started.wait_for(std::chrono::seconds(10)));
+ ASSERT_TRUE(_load_channel->cancel().ok());
+ destructor = std::async(std::launch::async, [&, resource_ctx =
_load_channel->_resource_ctx,
+ writer =
std::move(_writer)]() mutable {
+ SCOPED_ATTACH_TASK(resource_ctx);
+ destroying.count_down();
+ writer.reset();
+ });
+ ASSERT_TRUE(destroying.wait_for(std::chrono::seconds(10)));
+ EXPECT_EQ(destructor.wait_for(std::chrono::milliseconds(50)),
std::future_status::timeout);
+ _release_worker.count_down();
+ destructor.get();
+ _tokens.clear();
+ EXPECT_EQ(_executed.load(), 1);
+}
+
+TEST_P(DeltaWriterCancelTest, KeepsEarlierTokenFailureLocal) {
+ install_tokens();
+ const auto failure = Status::InternalError("bitmap failure");
+ ASSERT_TRUE(_tokens.front()->submit_func([&] { return failure; }).ok());
+ ASSERT_EQ(_tokens.front()->wait(), failure);
+ EXPECT_TRUE(_load_channel->cancel_status().ok());
+ const auto reason = Status::Cancelled("load cancelled");
+ ASSERT_TRUE(_load_channel->cancel(reason).ok());
+ EXPECT_EQ(_tokens.front()->wait(), failure);
+ for (size_t i = 1; i < _tokens.size(); ++i) {
+ EXPECT_EQ(_tokens[i]->wait(), reason);
+ }
+}
+
+TEST_P(DeltaWriterCancelTest, CancellationWinsOverLaterCallbackFailure) {
+ install_tokens();
+ ASSERT_TRUE(_tokens.front()
+ ->submit_func([this] {
+ _worker_started.count_down();
+ _release_worker.wait();
+ return Status::InternalError("late bitmap
failure");
+ })
+ .ok());
+ ASSERT_TRUE(_worker_started.wait_for(std::chrono::seconds(10)));
+ const auto reason = Status::Cancelled("load cancelled first");
+ ASSERT_TRUE(_load_channel->cancel(reason).ok());
+ _release_worker.count_down();
+ EXPECT_EQ(_tokens.front()->wait(), reason);
+}
+
+INSTANTIATE_TEST_SUITE_P(LocalCloudOrdinaryGroup, DeltaWriterCancelTest,
testing::Range(0, 4));
+
+} // namespace doris
diff --git a/be/test/load/memtable/memtable_flush_executor_test.cpp
b/be/test/load/memtable/memtable_flush_executor_test.cpp
index ad6e27dd0c5..8053cfcf9aa 100644
--- a/be/test/load/memtable/memtable_flush_executor_test.cpp
+++ b/be/test/load/memtable/memtable_flush_executor_test.cpp
@@ -26,6 +26,8 @@
#include <atomic>
#include <chrono>
+#include <functional>
+#include <future>
#include <string>
#include <thread>
@@ -48,6 +50,8 @@
#include "storage/tablet/tablet_meta_manager.h"
#include "storage/utils.h"
#include "testutil/creators.h"
+#include "util/countdown_latch.h"
+#include "util/defer_op.h"
namespace doris {
@@ -82,6 +86,9 @@ public:
Status flush_memtable(Block* block, int32_t segment_id, int64_t*
flush_size) override {
EXPECT_GT(block->rows(), 0);
+ if (_flush_hook) {
+ _flush_hook();
+ }
if (_flush_enter_cnt != nullptr) {
++(*_flush_enter_cnt);
}
@@ -126,11 +133,14 @@ public:
std::shared_ptr<PartialUpdateInfo> get_partial_update_info() override {
return nullptr; }
bool is_partial_update() override { return false; }
+ void set_flush_hook(std::function<void()> hook) { _flush_hook =
std::move(hook); }
+
int32_t last_segment_id() const { return _last_segment_id; }
ConstAllocatedLsnVectorSharedPtr last_seg_lsn() const { return
_last_seg_lsn; }
private:
+ std::function<void()> _flush_hook;
std::atomic<int>* _flush_cnt;
bool _fail_on_flush;
std::string _flush_error_msg;
@@ -723,4 +733,113 @@ TEST_F(MemTableFlushExecutorGroupFlushTest,
TestGroupFlushTokenCancelledCleanup)
drop_tablet(ctx.request);
}
+class MemTableFlushCancellationTest : public
MemTableFlushExecutorGroupFlushTest,
+ public testing::WithParamInterface<bool>
{
+protected:
+ static void expect_skipped_flush(const FlushStatistic& stats) {
+ EXPECT_EQ(stats.flush_submit_count.load(), 0);
+ EXPECT_EQ(stats.flush_running_count.load(), 0);
+ EXPECT_EQ(stats.flush_finish_count.load(), 0);
+ }
+};
+
+TEST_P(MemTableFlushCancellationTest, SharedCancellationSkipsQueuedFlushes) {
+ SCOPED_INIT_THREAD_CONTEXT();
+ const bool group_flush = GetParam();
+ GroupFlushTestContext ctx;
+ prepare_group_flush_test_context(10007 + group_flush, 270068379 +
group_flush, {7000, 7001},
+ &ctx);
+ std::atomic<int> data_flush_cnt = 0;
+ std::atomic<int> binlog_flush_cnt = 0;
+ auto data_writer = std::make_shared<MockRowsetWriter>(&data_flush_cnt);
+ auto binlog_writer = std::make_shared<MockRowsetWriter>(&binlog_flush_cnt);
+ std::shared_ptr<GroupRowsetWriter> group_writer;
+ ASSERT_TRUE(create_group_rowset_writer(ctx, 7, data_writer, binlog_writer,
&group_writer).ok());
+
+ std::unique_ptr<ThreadPool> pool;
+ ASSERT_TRUE(ThreadPoolBuilder("CancelledFlushTest")
+ .set_min_threads(1)
+ .set_max_threads(1)
+ .build(&pool)
+ .ok());
+ auto status = std::make_shared<AtomicStatus>();
+ auto token = FlushToken::create_shared(pool.get(), nullptr, status);
+ token->set_rowset_writer(group_flush ?
std::static_pointer_cast<RowsetWriter>(group_writer)
+ : data_writer);
+ token->set_table_schema_param(create_group_flush_table_schema_param(ctx));
+ CountDownLatch entered(1);
+ CountDownLatch release(1);
+ Defer cleanup {[&] {
+ release.count_down();
+ pool->wait();
+ }};
+ ASSERT_TRUE(pool->submit_func([&] {
+ entered.count_down();
+ release.wait();
+ }).ok());
+ ASSERT_TRUE(entered.wait_for(std::chrono::seconds(10)));
+ ASSERT_TRUE(token->submit(ctx.memtable).ok());
+ EXPECT_EQ(token->get_stats().flush_submit_count.load(), group_flush ? 2 :
1);
+ status->update(Status::Cancelled("cancel queued flush"));
+ // Both pre-cancel and post-cancel submissions must skip flushing when
dispatched.
+ ASSERT_TRUE(token->submit(ctx.memtable).ok());
+ release.count_down();
+ EXPECT_TRUE(token->wait().is<ErrorCode::CANCELLED>());
+ pool->wait();
+ EXPECT_EQ(data_flush_cnt.load(), 0);
+ EXPECT_EQ(binlog_flush_cnt.load(), 0);
+ expect_skipped_flush(token->get_stats());
+
EXPECT_FALSE(group_writer->context().allocated_lsn_map->contains_segment(0));
+ drop_tablet(ctx.request);
+}
+
+INSTANTIATE_TEST_SUITE_P(LoadCancellation, MemTableFlushCancellationTest,
testing::Bool());
+
+TEST_F(MemTableFlushExecutorGroupFlushTest,
SharedCancellationDuringGroupFlush) {
+ SCOPED_INIT_THREAD_CONTEXT();
+ GroupFlushTestContext ctx;
+ prepare_group_flush_test_context(10009, 270068381, {9000, 9001}, &ctx);
+ std::atomic<int> data_flush_cnt = 0;
+ std::atomic<int> binlog_flush_cnt = 0;
+ auto data_writer = std::make_shared<MockRowsetWriter>(&data_flush_cnt);
+ auto binlog_writer = std::make_shared<MockRowsetWriter>(&binlog_flush_cnt);
+ CountDownLatch entered(1);
+ CountDownLatch release(1);
+ data_writer->set_flush_hook([&] {
+ entered.count_down();
+ release.wait();
+ });
+ std::shared_ptr<GroupRowsetWriter> group_writer;
+ ASSERT_TRUE(create_group_rowset_writer(ctx, 9, data_writer, binlog_writer,
&group_writer).ok());
+ std::unique_ptr<ThreadPool> pool;
+ ASSERT_TRUE(ThreadPoolBuilder("RunningFlushCancellationTest")
+ .set_min_threads(1)
+ .set_max_threads(1)
+ .build(&pool)
+ .ok());
+ auto status = std::make_shared<AtomicStatus>();
+ auto token = FlushToken::create_shared(pool.get(), nullptr, status);
+ token->set_rowset_writer(group_writer);
+ token->set_table_schema_param(create_group_flush_table_schema_param(ctx));
+ Defer cleanup {[&] {
+ release.count_down();
+ pool->wait();
+ }};
+ ASSERT_TRUE(token->submit(ctx.memtable).ok());
+ ASSERT_TRUE(entered.wait_for(std::chrono::seconds(10)));
+ status->update(Status::Cancelled("cancel during data flush"));
+ auto waiter = std::async(std::launch::async, [&] { return token->wait();
});
+ EXPECT_EQ(waiter.wait_for(std::chrono::milliseconds(50)),
std::future_status::timeout);
+ release.count_down();
+ EXPECT_TRUE(waiter.get().is<ErrorCode::CANCELLED>());
+ pool->wait();
+ EXPECT_EQ(data_flush_cnt.load(), 1);
+ EXPECT_EQ(binlog_flush_cnt.load(), 0);
+ EXPECT_EQ(token->get_stats().flush_submit_count.load(), 0);
+ EXPECT_EQ(token->get_stats().flush_running_count.load(), 0);
+ EXPECT_FALSE(group_writer->context().allocated_lsn_map->contains_segment(
+ data_writer->last_segment_id()));
+ drop_tablet(ctx.request);
+}
+
} // namespace doris
diff --git a/be/test/load/memtable/memtable_memory_limiter_test.cpp
b/be/test/load/memtable/memtable_memory_limiter_test.cpp
index 141f2dd4041..457855d041c 100644
--- a/be/test/load/memtable/memtable_memory_limiter_test.cpp
+++ b/be/test/load/memtable/memtable_memory_limiter_test.cpp
@@ -20,6 +20,7 @@
#include "gtest/gtest_pred_impl.h"
#include "io/fs/local_file_system.h"
#include "load/delta_writer/delta_writer.h"
+#include "load/memtable/memtable_flush_executor.h"
#include "runtime/descriptor_helper.h"
#include "runtime/descriptors.h"
#include "storage/storage_engine.h"
@@ -182,4 +183,94 @@ TEST_F(MemTableMemoryLimiterTest,
handle_memtable_flush_test) {
res = _engine_ref->tablet_manager()->drop_tablet(request.tablet_id,
request.replica_id, false);
EXPECT_EQ(Status::OK(), res);
}
+
+class CancelledMemTableMemoryLimiterTest : public MemTableMemoryLimiterTest,
+ public
testing::WithParamInterface<bool> {};
+
+TEST_P(CancelledMemTableMemoryLimiterTest,
ReclaimActiveMemoryWhileWriterIsRetained) {
+ RuntimeProfile profile("CancelledMemTableWriter");
+ TCreateTabletReq tablet_request;
+ create_tablet_request(10001, 270068373, &tablet_request);
+ ASSERT_TRUE(_engine_ref->create_tablet(tablet_request, &profile).ok());
+
+ TDescriptorTable tdesc_tbl = create_descriptor_tablet();
+ ObjectPool obj_pool;
+ DescriptorTbl* desc_tbl = nullptr;
+ ASSERT_TRUE(DescriptorTbl::create(&obj_pool, tdesc_tbl, &desc_tbl).ok());
+ auto* tuple_desc = desc_tbl->get_tuple_descriptor(0);
+ auto cancel_status = std::make_shared<AtomicStatus>();
+ WriteRequest write_req;
+ write_req.tablet_id = tablet_request.tablet_id;
+ write_req.schema_hash = tablet_request.tablet_schema.schema_hash;
+ write_req.txn_id = 20003;
+ write_req.partition_id = tablet_request.partition_id;
+ write_req.tuple_desc = tuple_desc;
+ write_req.slots = &tuple_desc->slots();
+ write_req.table_schema_param = std::make_shared<OlapTableSchemaParam>();
+ write_req.load_cancel_status = cancel_status;
+ auto delta_writer =
+ std::make_unique<DeltaWriter>(*_engine_ref, write_req, &profile,
TUniqueId {});
+ Block block;
+ for (const auto* slot : tuple_desc->slots()) {
+ block.insert(ColumnWithTypeAndName(slot->get_empty_mutable_column(),
slot->type(),
+ slot->col_name()));
+ }
+ auto columns = std::move(block).mutate_columns();
+ for (auto& column : columns) {
+ column->insert_default();
+ }
+ block.set_columns(std::move(columns));
+ ASSERT_TRUE(delta_writer->write(&block, TabletAddRowsPayload {.row_idxs =
{0}}).ok());
+ auto writer = delta_writer->_memtable_writer;
+ std::weak_ptr<MemTable> memtable = writer->_mem_table;
+ auto token = writer->_flush_token;
+ auto active_memory = writer->active_memtable_mem_consumption();
+ ASSERT_GT(active_memory, 0);
+ auto segment_num = writer->_segment_num;
+
+ // add_batch failures can publish a non-CANCELLED status to the shared
load.
+ const auto failure = Status::InternalError("load failed");
+ ASSERT_TRUE(cancel_status->update(failure));
+ EXPECT_FALSE(writer->_is_cancelled);
+
+ auto* limiter = ExecEnv::GetInstance()->memtable_memory_limiter();
+ ASSERT_TRUE(limiter->init(100).ok());
+ limiter->refresh_mem_tracker();
+ ASSERT_GT(limiter->mem_usage(), limiter->_load_hard_mem_limit);
+ if (GetParam()) {
+ // An unrelated load must leave the hard-limit wait after reclaiming
the
+ // cancelled load's memory, without waiting for its retained writer.
+ // Bound retries so a regression fails instead of hanging the test.
+ int cancel_checks = 0;
+ limiter->handle_memtable_flush([&]() { return ++cancel_checks > 1; });
+ EXPECT_EQ(cancel_checks, 1);
+ } else {
+ // Discarded memory must not be reported as flushed memory.
+ EXPECT_EQ(limiter->_flush_active_memtables(active_memory), 0);
+ }
+ EXPECT_TRUE(memtable.expired());
+ EXPECT_EQ(writer->_mem_table, nullptr);
+ EXPECT_FALSE(writer->_is_cancelled);
+ EXPECT_FALSE(token->_is_shutdown());
+ EXPECT_EQ(writer->_segment_num, segment_num);
+ EXPECT_TRUE(writer->_freezed_mem_tables.empty());
+ EXPECT_EQ(writer->active_memtable_mem_consumption(), 0);
+ EXPECT_EQ(limiter->mem_usage(), 0);
+ EXPECT_TRUE(limiter->_active_writers.empty());
+
+ // An RPC may have passed DeltaWriter's cancellation check before
reclamation.
+ // The memtable-level checks must reject it before dereferencing
_mem_table.
+ EXPECT_EQ(writer->write(&block, TabletAddRowsPayload {.row_idxs = {0}},
nullptr), failure);
+ EXPECT_EQ(writer->close(), failure);
+ EXPECT_TRUE(writer->flush_async().is<ErrorCode::CANCELLED>());
+ EXPECT_EQ(writer->_segment_num, segment_num);
+ EXPECT_EQ(writer->_mem_table, nullptr);
+
+ // Reclaiming active memory must not suppress the final owner's token
cleanup.
+ delta_writer.reset();
+ EXPECT_TRUE(writer->_is_cancelled);
+ EXPECT_TRUE(token->_is_shutdown());
+}
+
+INSTANTIATE_TEST_SUITE_P(PressureFlush, CancelledMemTableMemoryLimiterTest,
testing::Bool());
} // namespace doris
diff --git a/be/test/storage/delete/calc_delete_bitmap_executor_test.cpp
b/be/test/storage/delete/calc_delete_bitmap_executor_test.cpp
new file mode 100644
index 00000000000..988dbb9cc0f
--- /dev/null
+++ b/be/test/storage/delete/calc_delete_bitmap_executor_test.cpp
@@ -0,0 +1,152 @@
+// 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.
+
+#include "storage/delete/calc_delete_bitmap_executor.h"
+
+#include <gtest/gtest.h>
+
+#include <chrono>
+#include <future>
+
+#include "cpp/sync_point.h"
+#include "runtime/memory/mem_tracker_limiter.h"
+#include "runtime/thread_context.h"
+#include "runtime/workload_management/resource_context.h"
+#include "util/countdown_latch.h"
+#include "util/defer_op.h"
+
+namespace doris {
+
+TEST(CalcDeleteBitmapTokenTest,
QueuedCallbacksKeepTheirSubmitterResourceContext) {
+ SCOPED_INIT_THREAD_CONTEXT();
+ std::unique_ptr<ThreadPool> pool;
+ ASSERT_TRUE(ThreadPoolBuilder("BitmapResourceContextTest")
+ .set_min_threads(1)
+ .set_max_threads(1)
+ .build(&pool)
+ .ok());
+ CalcDeleteBitmapToken
token(pool->new_token(ThreadPool::ExecutionMode::CONCURRENT));
+ CountDownLatch entered(1);
+ CountDownLatch release(1);
+ Defer cleanup {[&] {
+ release.count_down();
+ pool->wait();
+ }};
+ ASSERT_TRUE(pool->submit_func([&] {
+ entered.count_down();
+ release.wait();
+ }).ok());
+ ASSERT_TRUE(entered.wait_for(std::chrono::seconds(10)));
+
+ // Queue callbacks from different submitters before allowing either to run.
+ // A token-wide context would incorrectly attach the last submitter to
both.
+ for (const auto* label : {"BitmapSubmitter1", "BitmapSubmitter2"}) {
+ auto resource_ctx = ResourceContext::create_shared();
+ resource_ctx->memory_context()->set_mem_tracker(
+
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER, label));
+ SCOPED_ATTACH_TASK(resource_ctx);
+ ASSERT_TRUE(token.submit_func([resource_ctx] {
+ EXPECT_EQ(thread_context()->resource_ctx(),
resource_ctx);
+ return Status::OK();
+ }).ok());
+ }
+ release.count_down();
+ EXPECT_TRUE(token.wait().ok());
+}
+
+TEST(CalcDeleteBitmapTokenTest, PreservesTaskFailure) {
+ SCOPED_INIT_THREAD_CONTEXT();
+ CalcDeleteBitmapExecutor executor;
+ executor.init("BitmapFailureTest", 1);
+ auto token = executor.create_token();
+ const auto failure = Status::InternalError("bitmap calculation failed");
+ ASSERT_TRUE(token->submit_func([&] { return failure; }).ok());
+ EXPECT_EQ(token->wait().to_string(), failure.to_string());
+ EXPECT_EQ(token->submit_func([] { return Status::OK(); }).to_string(),
failure.to_string());
+}
+
+TEST(CalcDeleteBitmapTokenTest,
CreatedAfterCancellationRejectsEverySubmissionEntry) {
+ SCOPED_INIT_THREAD_CONTEXT();
+ CalcDeleteBitmapExecutor executor;
+ executor.init("BitmapLateTokenTest", 1);
+ auto status = std::make_shared<AtomicStatus>();
+ const auto reason = Status::Cancelled("load already cancelled");
+ status->update(reason);
+ auto token = executor.create_token(status);
+ EXPECT_EQ(token->submit_func([] { return Status::OK(); }), reason);
+ // Neither typed callback may dereference its inputs after cancellation.
+ EXPECT_EQ(token->submit(nullptr, nullptr, nullptr, {}, 0, nullptr,
nullptr, nullptr), reason);
+ EXPECT_EQ(token->submit(nullptr, nullptr, RowsetId {}, {}, nullptr),
reason);
+ EXPECT_EQ(token->wait(), reason);
+}
+
+TEST(CalcDeleteBitmapTokenTest, FailedSubmitKeepsCancellationReason) {
+ SCOPED_INIT_THREAD_CONTEXT();
+ CalcDeleteBitmapExecutor executor;
+ executor.init("BitmapSubmitCancelRaceTest", 1);
+ auto status = std::make_shared<AtomicStatus>();
+ auto token = executor.create_token(status);
+ const auto reason = Status::Cancelled("cancelled between check and
submit");
+ auto* sync_point = SyncPoint::get_instance();
+ SyncPoint::CallbackGuard guard;
+ Defer cleanup {[&] { sync_point->disable_processing(); }};
+ sync_point->set_call_back(
+ "CalcDeleteBitmapToken::submit_func:before_submit",
+ [&](auto&& args) {
+ if (try_any_cast<CalcDeleteBitmapToken*>(args[0]) ==
token.get()) {
+ status->update(reason);
+ token->cancel();
+ }
+ },
+ &guard);
+ sync_point->enable_processing();
+ EXPECT_EQ(token->submit_func([] { return Status::OK(); }), reason);
+ EXPECT_EQ(token->wait(), reason);
+}
+
+TEST(CalcDeleteBitmapTokenTest, DestructionWaitsForRunningCallback) {
+ SCOPED_INIT_THREAD_CONTEXT();
+ CalcDeleteBitmapExecutor executor;
+ executor.init("BitmapDestructionTest", 1);
+ auto token = executor.create_token();
+ CountDownLatch entered(1);
+ CountDownLatch release(1);
+ CountDownLatch destroying(1);
+ Defer cleanup {[&] {
+ release.count_down();
+ if (token) {
+ token->cancel();
+ }
+ }};
+ ASSERT_TRUE(token->submit_func([&] {
+ entered.count_down();
+ release.wait();
+ // The wrapper still accesses the token's status
after this callback.
+ return Status::InternalError("callback failed during
destruction");
+ }).ok());
+ ASSERT_TRUE(entered.wait_for(std::chrono::seconds(10)));
+ auto destructor = std::async(std::launch::async, [&, owned =
std::move(token)]() mutable {
+ destroying.count_down();
+ owned.reset();
+ });
+ EXPECT_TRUE(destroying.wait_for(std::chrono::seconds(10)));
+ EXPECT_EQ(destructor.wait_for(std::chrono::milliseconds(50)),
std::future_status::timeout);
+ release.count_down();
+ destructor.get();
+}
+
+} // namespace doris
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]