This is an automated email from the ASF dual-hosted git repository.
hello-stephen 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 ccbd46dae63 [improvement](be) Name the task of every resource context
still alive at graceful exit (#68942)
ccbd46dae63 is described below
commit ccbd46dae63b045ffff87020b1b9a62c870185f0
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Wed Oct 14 18:29:13 2026 +0800
[improvement](be) Name the task of every resource context still alive at
graceful exit (#68942)
Problem Summary:
**In short.** The NonConcurrent regression pipeline fails a few percent
of its runs with `Lsan fail` while every case passes. LeakSanitizer
finds, at the BE's graceful exit, one stream load's `ResourceContext`
that was never released, but its report only shows allocation stacks,
which are the same for every stream load, so nobody can tell which load
it was. This PR makes the BE print, at graceful exit in memleak-check
mode, every `ResourceContext` still alive with its task id, the
references still held, its memory tracker and workload group, and
whether its `QueryContext` is already gone. Nothing changes unless
`enable_graceful_exit_check=true`.
**Background.**
- With `enable_graceful_exit_check=true` (memleak-check mode, used by
the regression pipelines on the ASAN build), `doris_main` stops every
service and runs `ExecEnv::destroy()` instead of `_exit()`, so that
LeakSanitizer checks the heap when the process exits. The NonConcurrent
pipeline greps `be.out` for `detected memory leaks` after `stop_be.sh
--grace` and reports `Lsan fail`.
- Every query or load has a `ResourceContext`: CPU/memory/IO contexts, a
task controller, the query memory tracker and the workload group. The
`QueryContext` owns it, and the components that work for the task (load
channel, memtables, scanner context, `RuntimeQueryStatisticsMgr`,
threads attached to the task, ...) hold references of their own while
they run.
**The problem, and what it cost.**
- `Lsan fail` hits 1–5% of NonConcurrent runs depending on the month
(448 of the last 14000, back to March 2026), on PRs that have nothing to
do with it; authors rerun or skip. Every report TeamCity still keeps (10
runs from 2026-10-05 to 2026-10-12, on #68707, #68281, #68791, #68803,
#68798, #68543, #68901, #68919, #68879) has the same shape, down to the
byte counts, and all of its leaks are indirect:
| Leaked | Size | Origin |
|---|---|---|
| one `ResourceContext` with its CPU/memory/IO contexts,
`QueryTaskController` and both trackers (`Load#Id=…`,
`Memtable#Load#Id=…`) | 6.5 KB, 94 objects | an HTTP stream load:
`StreamLoadAction::_process_put` → `StreamLoadExecutor` →
`FragmentMgr::_get_or_create_query_ctx` |
| the `normal` workload group created by
`WorkloadGroupMgr::create_internal_wg()` | 6.1 KB + 4.3 KB | kept alive
by `ResourceContext::_workload_group` |
| the `std::make_shared` block of the load's `QueryContext` | 2.5 KB |
kept allocated by the `weak_ptr` in `QueryTaskController` |
- The `QueryContext` itself was destroyed: in `be.INFO`, each of the
21894 `query_id: … use wg:` lines of the run has its `Query …
deconstructed`. So one reference to the load's `ResourceContext` is
never released, from a place LeakSanitizer does not scan (a bthread
stack, whose memory brpc maps itself) or a count that is never
decremented. LeakSanitizer cannot tell which of these two it is.
- A NonConcurrent run issues about 430 HTTP stream loads (about 380
through the load channel, 150 of them audit-log loads), and the report
cannot say which one leaked: the stacks are identical, and the one thing
that would identify the load, the tracker label with its id, is inside
the leaked memory, which LeakSanitizer does not print. Without the load
there is nothing to follow, so the flake stays unexplained.
**How this PR fixes it.**
- In memleak-check mode each `ResourceContext` registers itself in a
process-wide set when it is constructed and removes itself when it is
destroyed. The set stores the addresses bitwise inverted, so
LeakSanitizer does not take it for a reference and still reports a
leaked context.
- `ResourceContext::describe_live_contexts()` returns one line per
context still alive: address, strong references still held, task id,
finished flag, running time, memory tracker (label, consumption, peak),
workload group and, for a query, whether its `QueryContext` is still
alive.
- `doris_main` calls `ResourceContext::log_live_contexts()` right after
`ExecEnv::destroy()` on the graceful exit path. It writes the
description to `be.WARNING` and to `be.out`, where the LeakSanitizer
report follows once `main()` returns, or one INFO line when no context
is alive. The printed address falls inside one of the `Objects leaked
above` ranges of that report.
- The description is built on a short-lived thread, and the main thread
only handles the resulting text. Describing a context touches it; done
on the main thread, it could leave a pointer to a leaked context in a
dead slot of the main thread's stack, which LeakSanitizer scans at exit
— it would then take the context for referenced and not report it, so
the run that reproduces the leak could pass. The stack of a finished
thread is not scanned.
**Results.** What the next `Lsan fail` will tell, compared with today:
| | Before | After |
|---|---|---|
| Which query or load leaked | unknown | `task_id=…`; its label, table
and test are then in `be.INFO` |
| How many references are left | unknown | `references=N` |
| Whether the `QueryContext` is gone | only by pairing ~20k log lines |
`query_context=destroyed` |
| Where to look | the LeakSanitizer report in `be.out` | the same
`be.out`, just before that report (the build log's `head -n 100 be.out`
shows it) |
| Cost with `enable_graceful_exit_check=false` (default) | — | one
boolean check per `ResourceContext` construction and destruction |
A line looks like this (values made up):
```
1 resource contexts are still alive. LeakSanitizer reports a leaked context
as the memory block that holds it, so match the addresses below against the
ranges it prints under 'Objects leaked above'.
0x7c0000001010: references=1, task_id=1eac0c4ec4ec0000-0000000000000003,
finished=true, running_ms=7569,
mem_tracker=Load#Id=1eac0c4ec4ec0000-0000000000000003 (consumption=0,
peak=17.18 MB), workload_group=normal(1), query_context=destroyed
```
**Classes, and how they call each other.**
- `ResourceContext` (`resource_context.{h,cpp}`): registers in the
constructor and unregisters in the destructor when memleak-check mode is
on; `describe_live_contexts()` formats the live ones;
`log_live_contexts()` runs that on its own thread and logs the result.
- `QueryTaskController`: new `is_query_context_alive()`.
- `doris_main`: one call to `log_live_contexts()` after
`ExecEnv::destroy()`.
```
doris_main (graceful exit, enable_graceful_exit_check=true)
├─ ExecEnv::destroy()
└─ ResourceContext::log_live_contexts() ──► be.WARNING + be.out (or one
INFO line)
└─ [short-lived thread] describe_live_contexts(100)
├─ registry of disguised addresses ◄── ResourceContext() /
~ResourceContext()
└─ per live context:
├─ TaskController: task id, finished, running time
├─ MemoryContext → MemTrackerLimiter: label, consumption, peak
├─ WorkloadGroup: name, id
└─ QueryTaskController::is_query_context_alive()
```
No holder of `ResourceContext` changes, and neither do the LeakSanitizer
options or suppressions. Once the leaked load is known, the fix for the
leak itself will be a separate PR.
---------
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../workload_management/query_task_controller.h | 2 +
.../workload_management/resource_context.cpp | 136 +++++++++++++++++
.../runtime/workload_management/resource_context.h | 27 +++-
be/src/service/doris_main.cpp | 6 +
.../workload_management/resource_context_test.cpp | 161 +++++++++++++++++++++
5 files changed, 331 insertions(+), 1 deletion(-)
diff --git a/be/src/runtime/workload_management/query_task_controller.h
b/be/src/runtime/workload_management/query_task_controller.h
index 4b11c042540..0e3e75a0c78 100644
--- a/be/src/runtime/workload_management/query_task_controller.h
+++ b/be/src/runtime/workload_management/query_task_controller.h
@@ -53,6 +53,8 @@ public:
void inc_finished_task_num();
int get_total_task_num() const;
int get_finished_task_num() const;
+ // Whether the QueryContext this controller belongs to has not been
destroyed yet.
+ bool is_query_context_alive() const { return !query_ctx_.expired(); }
protected:
QueryTaskController(const std::shared_ptr<QueryContext>& query_ctx) :
query_ctx_(query_ctx) {}
diff --git a/be/src/runtime/workload_management/resource_context.cpp
b/be/src/runtime/workload_management/resource_context.cpp
index 85378ad287e..3a440dd12fc 100644
--- a/be/src/runtime/workload_management/resource_context.cpp
+++ b/be/src/runtime/workload_management/resource_context.cpp
@@ -17,16 +17,152 @@
#include "runtime/workload_management/resource_context.h"
+#include <fmt/format.h>
#include <gen_cpp/FrontendService_types.h>
#include <gen_cpp/data.pb.h>
#include <glog/logging.h>
+#include <cstdint>
+#include <iostream>
+#include <mutex>
+#include <thread>
+#include <unordered_set>
+#include <vector>
+
+#include "common/check.h"
+#include "common/config.h"
+#include "runtime/memory/mem_tracker_limiter.h"
#include "runtime/workload_group/workload_group.h"
#include "runtime/workload_management/query_task_controller.h"
+#include "util/pretty_printer.h"
#include "util/time.h"
+#include "util/uid_util.h"
namespace doris {
+namespace {
+
+// The resource contexts created while memleak-check mode is on. Their
addresses are kept
+// disguised (bitwise inverted), so that LeakSanitizer does not take the
registry for a
+// reference: a context that nothing else references must still be reported as
leaked.
+struct LiveResourceContexts {
+ std::mutex lock;
+ std::unordered_set<uintptr_t> disguised_contexts;
+};
+
+LiveResourceContexts& live_resource_contexts() {
+ // Never destroyed, so that a context released during static destruction
can still
+ // unregister itself.
+ static auto* live = new LiveResourceContexts();
+ return *live;
+}
+
+uintptr_t disguise(const ResourceContext* context) {
+ return ~reinterpret_cast<uintptr_t>(context);
+}
+
+ResourceContext* undisguise(uintptr_t disguised_context) {
+ return reinterpret_cast<ResourceContext*>(~disguised_context);
+}
+
+} // namespace
+
+void ResourceContext::_track_if_leak_check_enabled() {
+ if (!config::enable_graceful_exit_check) {
+ return;
+ }
+ auto& live = live_resource_contexts();
+ std::lock_guard<std::mutex> l(live.lock);
+ live.disguised_contexts.insert(disguise(this));
+ _tracked = true;
+}
+
+ResourceContext::~ResourceContext() {
+ if (_tracked) {
+ auto& live = live_resource_contexts();
+ std::lock_guard<std::mutex> l(live.lock);
+ DORIS_CHECK(live.disguised_contexts.erase(disguise(this)) == 1);
+ }
+}
+
+std::string ResourceContext::describe_live_contexts(size_t max_contexts) {
+ // Meant for shutdown, when no context is being created any more: a
context that is still
+ // inside std::make_shared has not been handed its weak_from_this() yet.
+ //
+ // Describe the contexts after the registry is unlocked, keeping each one
alive meanwhile
+ // with a reference of our own. A context whose last reference is being
released right now
+ // cannot be referenced any more and is left out.
+ std::vector<std::shared_ptr<ResourceContext>> contexts;
+ {
+ auto& live = live_resource_contexts();
+ std::lock_guard<std::mutex> l(live.lock);
+ contexts.reserve(live.disguised_contexts.size());
+ for (uintptr_t disguised_context : live.disguised_contexts) {
+ if (auto reference =
undisguise(disguised_context)->weak_from_this().lock()) {
+ contexts.push_back(std::move(reference));
+ }
+ }
+ }
+ if (contexts.empty()) {
+ return "";
+ }
+ fmt::memory_buffer out;
+ fmt::format_to(std::back_inserter(out),
+ "{} resource contexts are still alive. LeakSanitizer
reports a leaked context "
+ "as the memory block that holds it, so match the addresses
below against the "
+ "ranges it prints under 'Objects leaked above'.",
+ contexts.size());
+ for (size_t i = 0; i < contexts.size() && i < max_contexts; ++i) {
+ // The reference taken above is not counted.
+ fmt::format_to(std::back_inserter(out), "\n {}",
+ contexts[i]->_describe(contexts[i].use_count() - 1));
+ }
+ if (contexts.size() > max_contexts) {
+ fmt::format_to(std::back_inserter(out), "\n ... and {} more",
+ contexts.size() - max_contexts);
+ }
+ return fmt::to_string(out);
+}
+
+std::string ResourceContext::log_live_contexts() {
+ std::string live_contexts;
+ std::thread describer([&live_contexts]() { live_contexts =
describe_live_contexts(100); });
+ describer.join();
+ if (live_contexts.empty()) {
+ LOG(INFO) << "No resource context is alive after shutdown";
+ } else {
+ LOG(WARNING) << live_contexts;
+ std::cerr << live_contexts << '\n';
+ }
+ return live_contexts;
+}
+
+std::string ResourceContext::_describe(long references_held_elsewhere) const {
+ std::string mem_tracker = "none";
+ if (auto tracker = memory_context_->mem_tracker()) {
+ mem_tracker = fmt::format("{} (consumption={}, peak={})",
tracker->label(),
+
PrettyPrinter::print_bytes(tracker->consumption()),
+
PrettyPrinter::print_bytes(tracker->peak_consumption()));
+ }
+ std::string workload_group = "none";
+ if (_workload_group != nullptr) {
+ workload_group = fmt::format("{}({})", _workload_group->name(),
_workload_group->id());
+ }
+ // A query's context outlives its QueryContext when something else still
holds it.
+ std::string query_context;
+ if (auto* query_task_controller =
dynamic_cast<QueryTaskController*>(task_controller_.get())) {
+ query_context = query_task_controller->is_query_context_alive()
+ ? ", query_context=alive"
+ : ", query_context=destroyed";
+ }
+ return fmt::format(
+ "{}: references={}, task_id={}, finished={}, running_ms={},
mem_tracker={}, "
+ "workload_group={}{}",
+ fmt::ptr(this), references_held_elsewhere,
print_id(task_controller_->task_id()),
+ task_controller_->is_finished(), task_controller_->running_time(),
mem_tracker,
+ workload_group, query_context);
+}
+
void ResourceContext::to_thrift_query_statistics(TQueryStatistics* statistics)
const {
DCHECK(statistics != nullptr);
statistics->__set_scan_rows(io_context()->scan_rows());
diff --git a/be/src/runtime/workload_management/resource_context.h
b/be/src/runtime/workload_management/resource_context.h
index 114b5b88367..2c2d1c0fc7b 100644
--- a/be/src/runtime/workload_management/resource_context.h
+++ b/be/src/runtime/workload_management/resource_context.h
@@ -17,7 +17,9 @@
#pragma once
+#include <cstddef>
#include <memory>
+#include <string>
#include "common/factory_creator.h"
#include "common/multi_version.h"
@@ -51,8 +53,9 @@ public:
memory_context_->set_resource_ctx(this);
io_context_->set_resource_ctx(this);
task_controller_->set_resource_ctx(this);
+ _track_if_leak_check_enabled();
}
- ~ResourceContext() = default;
+ ~ResourceContext();
// Only return the raw pointer to the caller, so that the caller should
not save it to other variables.
CPUContext* cpu_context() const { return cpu_context_.get(); }
@@ -99,7 +102,27 @@ public:
resource_profile_.set(std::move(resource_profile));
}
+ // Memleak-check mode only (config::enable_graceful_exit_check, where
doris_main destroys
+ // everything on exit so that LeakSanitizer can check the heap): returns
one line per
+ // resource context that is still alive, at most `max_contexts` of them,
or an empty string
+ // if there is none. LeakSanitizer reports a context that was never
released only by its
+ // allocation stack, which is the same for every query; the task id
printed here tells which
+ // query or load it belonged to. Call it at shutdown, when no context is
being created.
+ static std::string describe_live_contexts(size_t max_contexts);
+
+ // For the end of a graceful exit in memleak-check mode: logs
describe_live_contexts() to
+ // be.WARNING and to stderr (be.out, where the LeakSanitizer report
follows), or one INFO line
+ // if no context is alive, and returns what it logged. The description is
built on a thread
+ // of its own and the caller never touches a context: a pointer to a
leaked context left on
+ // the caller's stack would be found by LeakSanitizer, which would then
take the context for
+ // referenced and stop reporting it, while the stack of a finished thread
is not scanned.
+ static std::string log_live_contexts();
+
private:
+ // Registers this context for describe_live_contexts() when memleak-check
mode is on.
+ void _track_if_leak_check_enabled();
+ std::string _describe(long references_held_elsewhere) const;
+
// The controller's init value is nullptr, it means the resource context
will ignore this controller.
std::unique_ptr<CPUContext> cpu_context_ = nullptr;
std::unique_ptr<MemoryContext> memory_context_ = nullptr;
@@ -108,6 +131,8 @@ private:
WorkloadGroupPtr _workload_group = nullptr;
MultiVersion<RuntimeProfile> resource_profile_;
+ // Whether this context is registered for describe_live_contexts().
+ bool _tracked = false;
};
} // namespace doris
diff --git a/be/src/service/doris_main.cpp b/be/src/service/doris_main.cpp
index 5193e666d71..5b0903027b0 100644
--- a/be/src/service/doris_main.cpp
+++ b/be/src/service/doris_main.cpp
@@ -76,6 +76,7 @@
#include "load/stream_load/stream_load_recorder_manager.h"
#include "runtime/exec_env.h"
#include "runtime/user_function_cache.h"
+#include "runtime/workload_management/resource_context.h"
#include "service/arrow_flight/flight_sql_service.h"
#include "service/backend_options.h"
#include "service/backend_service.h"
@@ -863,6 +864,11 @@ int main(int argc, char** argv) {
service.reset();
LOG(INFO) << "Backend Service stopped";
exec_env->destroy();
+ // Everything above should have released the resource context of every
query and load. One
+ // that is still alive is held either by a thread that has not finished or
by a reference
+ // that is never released; LeakSanitizer reports the latter after main()
returns, but cannot
+ // tell which task it belonged to, so name the task here.
+ doris::ResourceContext::log_live_contexts();
LOG(INFO) << "All service stopped, doris main exited.";
return 0;
}
diff --git a/be/test/runtime/workload_management/resource_context_test.cpp
b/be/test/runtime/workload_management/resource_context_test.cpp
new file mode 100644
index 00000000000..da0a2e08d62
--- /dev/null
+++ b/be/test/runtime/workload_management/resource_context_test.cpp
@@ -0,0 +1,161 @@
+// 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 "runtime/workload_management/resource_context.h"
+
+#include <fmt/format.h>
+#include <gtest/gtest.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "common/config.h"
+#include "runtime/memory/mem_tracker_limiter.h"
+#include "runtime/workload_group/workload_group.h"
+#include "testutil/mock/mock_query_context.h"
+#include "util/uid_util.h"
+
+namespace doris {
+
+class ResourceContextLeakCheckTest : public testing::Test {
+protected:
+ void SetUp() override {
+ _saved_graceful_exit_check = config::enable_graceful_exit_check;
+ config::enable_graceful_exit_check = true;
+ }
+
+ void TearDown() override { config::enable_graceful_exit_check =
_saved_graceful_exit_check; }
+
+ static TUniqueId make_task_id(int64_t lo) {
+ TUniqueId task_id;
+ task_id.__set_hi(0x1eac0c4ec4ec0000);
+ task_id.__set_lo(lo);
+ return task_id;
+ }
+
+ // The line that describe_live_contexts() printed for the task, or an
empty string.
+ static std::string line_of(const std::string& description, const
TUniqueId& task_id) {
+ size_t pos = description.find("task_id=" + print_id(task_id) + ",");
+ if (pos == std::string::npos) {
+ return "";
+ }
+ size_t begin = description.rfind('\n', pos) + 1;
+ size_t end = description.find('\n', pos);
+ return description.substr(begin, end == std::string::npos ? end : end
- begin);
+ }
+
+private:
+ bool _saved_graceful_exit_check = false;
+};
+
+TEST_F(ResourceContextLeakCheckTest, DescribesContextUntilItIsReleased) {
+ TUniqueId task_id = make_task_id(1);
+ auto context = ResourceContext::create_shared();
+ context->task_controller()->set_task_id(task_id);
+
context->memory_context()->set_mem_tracker(MemTrackerLimiter::create_shared(
+ MemTrackerLimiter::Type::LOAD, "Load#Id=" + print_id(task_id)));
+
+ std::string description = ResourceContext::describe_live_contexts(1000);
+ EXPECT_NE(description.find("resource contexts are still alive"),
std::string::npos)
+ << description;
+ std::string line = line_of(description, task_id);
+ EXPECT_EQ(0, line.find(fmt::format(" {}: references=1,",
fmt::ptr(context.get())))) << line;
+ EXPECT_NE(line.find("finished=false"), std::string::npos) << line;
+ EXPECT_NE(line.find("mem_tracker=Load#Id=" + print_id(task_id) + " ("),
std::string::npos)
+ << line;
+ EXPECT_NE(line.find("workload_group=none"), std::string::npos) << line;
+ // Only a query's controller knows its QueryContext.
+ EXPECT_EQ(line.find("query_context="), std::string::npos) << line;
+
+ {
+ auto another_holder = context;
+ line = line_of(ResourceContext::describe_live_contexts(1000), task_id);
+ EXPECT_NE(line.find("references=2,"), std::string::npos) << line;
+ }
+
+ context->task_controller()->finish();
+ line = line_of(ResourceContext::describe_live_contexts(1000), task_id);
+ EXPECT_NE(line.find("finished=true"), std::string::npos) << line;
+
+ // What doris_main logs at exit is the same description.
+ EXPECT_NE(line_of(ResourceContext::log_live_contexts(), task_id), "");
+
+ context.reset();
+ EXPECT_EQ("", line_of(ResourceContext::describe_live_contexts(1000),
task_id));
+ EXPECT_EQ("", line_of(ResourceContext::log_live_contexts(), task_id));
+}
+
+TEST_F(ResourceContextLeakCheckTest, ShowsTheWorkloadGroup) {
+ TUniqueId task_id = make_task_id(4);
+ auto context = ResourceContext::create_shared();
+ context->task_controller()->set_task_id(task_id);
+ WorkloadGroupInfo wg_info {.id = 6894204, .name =
"resource_context_test_wg"};
+ context->set_workload_group(std::make_shared<WorkloadGroup>(wg_info));
+
+ std::string line = line_of(ResourceContext::describe_live_contexts(1000),
task_id);
+ EXPECT_NE(line.find("workload_group=resource_context_test_wg(6894204)"),
std::string::npos)
+ << line;
+ EXPECT_NE(line.find("mem_tracker=none"), std::string::npos) << line;
+}
+
+TEST_F(ResourceContextLeakCheckTest,
DoesNotTrackContextsCreatedOutsideLeakCheckMode) {
+ config::enable_graceful_exit_check = false;
+ TUniqueId task_id = make_task_id(2);
+ auto context = ResourceContext::create_shared();
+ context->task_controller()->set_task_id(task_id);
+
+ config::enable_graceful_exit_check = true;
+ EXPECT_EQ("", line_of(ResourceContext::describe_live_contexts(1000),
task_id));
+ // Must not try to unregister a context that was never registered.
+ context.reset();
+}
+
+TEST_F(ResourceContextLeakCheckTest, TellsWhetherTheQueryContextIsStillAlive) {
+ TUniqueId task_id = make_task_id(3);
+ auto query_ctx = MockQueryContext::create(task_id);
+ std::shared_ptr<ResourceContext> context = query_ctx->resource_ctx();
+
+ std::string line = line_of(ResourceContext::describe_live_contexts(1000),
task_id);
+ EXPECT_NE(line.find("query_context=alive"), std::string::npos) << line;
+
+ // The shape of the leak this is meant to explain: the QueryContext is
gone, but its
+ // resource context is still held by someone else.
+ query_ctx.reset();
+ line = line_of(ResourceContext::describe_live_contexts(1000), task_id);
+ EXPECT_NE(line.find("references=1,"), std::string::npos) << line;
+ EXPECT_NE(line.find("finished=true"), std::string::npos) << line;
+ EXPECT_NE(line.find("query_context=destroyed"), std::string::npos) << line;
+}
+
+TEST_F(ResourceContextLeakCheckTest, LimitsTheNumberOfDescribedContexts) {
+ std::vector<std::shared_ptr<ResourceContext>> contexts;
+ for (int64_t i = 0; i < 3; ++i) {
+ contexts.push_back(ResourceContext::create_shared());
+ contexts.back()->task_controller()->set_task_id(make_task_id(10 + i));
+ }
+
+ std::string description = ResourceContext::describe_live_contexts(1);
+ int described = 0;
+ for (int64_t i = 0; i < 3; ++i) {
+ described += line_of(description, make_task_id(10 + i)).empty() ? 0 :
1;
+ }
+ EXPECT_LE(described, 1) << description;
+ EXPECT_NE(description.find("\n ... and "), std::string::npos) <<
description;
+}
+
+} // namespace doris
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]