Skip to content

Commit 46d27a2

Browse files
authored
[fix](pipeline) Crashing caused by repeated spill operations (#56755)
### What problem does this PR solve? ```text #0 __GI___pthread_sigmask (how=2, newmask=<optimized out>, oldmask=0x0) at ./nptl/pthread_sigmask.c:43 #1 0x00007f91fd6c471e in PosixSignals::chained_handler(int, siginfo*, void*) [clone .part.0] () from /usr/lib/jvm/java-17-openjdk-amd64/lib/server/libjvm.so #2 0x00007f91fd6c5206 in JVM_handle_linux_signal () from /usr/lib/jvm/java-17-openjdk-amd64/lib/server/libjvm.so #3 <signal handler called> #4 __gnu_cxx::__exchange_and_add (__mem=0xe, __val=-1) at /usr/local/ldb-toolchain-v0.26/bin/../lib/gcc/x86_64-pc-linux-gnu/15/include/g++-v15/ext/atomicity.h:68 #5 __gnu_cxx::__exchange_and_add_dispatch (__mem=0xe, __val=-1) at /usr/local/ldb-toolchain-v0.26/bin/../lib/gcc/x86_64-pc-linux-gnu/15/include/g++-v15/ext/atomicity.h:103 #6 std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_weak_release (this=0x2) at /usr/local/ldb-toolchain-v0.26/bin/../lib/gcc/x86_64-pc-linux-gnu/15/include/g++-v15/bits/shared_ptr_base.h:211 #7 std::__weak_count<(__gnu_cxx::_Lock_policy)2>::~__weak_count (this=<optimized out>) at /usr/local/ldb-toolchain-v0.26/bin/../lib/gcc/x86_64-pc-linux-gnu/15/include/g++-v15/bits/shared_ptr_base.h:1168 #8 std::__weak_ptr<doris::QueryContext, (__gnu_cxx::_Lock_policy)2>::~__weak_ptr (this=<optimized out>) at /usr/local/ldb-toolchain-v0.26/bin/../lib/gcc/x86_64-pc-linux-gnu/15/include/g++-v15/bits/shared_ptr_base.h:2003 #9 doris::QueryTaskController::revoke_memory()::$_1::~$_1() (this=<optimized out>) at /root/doris/be/src/runtime/workload_management/query_task_controller.cpp:168 #10 std::_Function_base::_Base_manager<doris::QueryTaskController::revoke_memory()::$_1>::_M_destroy(std::_Any_data&, std::integral_constant<bool, false>) (__victim=...) at /usr/local/ldb-toolchain-v0.26/bin/../lib/gcc/x86_64-pc-linux-gnu/15/include/g++-v15/bits/std_function.h:177 #11 std::_Function_base::_Base_manager<doris::QueryTaskController::revoke_memory()::$_1>::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation) (__dest=..., __op=std::__destroy_functor, __source=...) at /usr/local/ldb-toolchain-v0.26/bin/../lib/gcc/x86_64-pc-linux-gnu/15/include/g++-v15/bits/std_function.h:205 #12 std::_Function_handler<void (doris::pipeline::SpillContext*), doris::QueryTaskController::revoke_memory()::$_1>::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation) (__dest=..., __source=..., __op=std::__destroy_functor) at /usr/local/ldb-toolchain-v0.26/bin/../lib/gcc/x86_64-pc-linux-gnu/15/include/g++-v15/bits/std_function.h:284 #13 0x000055f37fba60c5 in std::_Function_base::~_Function_base (this=0x7f8dbb0c9fe0) at /usr/local/ldb-toolchain-v0.26/bin/../lib/gcc/x86_64-pc-linux-gnu/15/include/g++-v15/bits/std_function.h:246 #14 doris::pipeline::SpillContext::~SpillContext (this=0x7f91536f0150) at /root/doris/be/src/pipeline/exec/spill_utils.h:57 #15 0x000055f384259e4b in std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release (this=0x7f91536f0140) at /usr/local/ldb-toolchain-v0.26/bin/../lib/gcc/x86_64-pc-linux-gnu/15/include/g++-v15/bits/shared_ptr_base.h:345 #16 std::__shared_count<(__gnu_cxx::_Lock_policy)2>::operator= (this=0x7f8d61302518, __r=...) at /usr/local/ldb-toolchain-v0.26/bin/../lib/gcc/x86_64-pc-linux-gnu/15/include/g++-v15/bits/shared_ptr_base.h:1088 #17 std::__shared_ptr<doris::pipeline::SpillContext, (__gnu_cxx::_Lock_policy)2>::operator= (this=0x7f8d61302510) at /usr/local/ldb-toolchain-v0.26/bin/../lib/gcc/x86_64-pc-linux-gnu/15/include/g++-v15/bits/shared_ptr_base.h:1530 #18 std::shared_ptr<doris::pipeline::SpillContext>::operator= (this=0x7f8d61302510) at /usr/local/ldb-toolchain-v0.26/bin/../lib/gcc/x86_64-pc-linux-gnu/15/include/g++-v15/bits/shared_ptr.h:413 #19 doris::pipeline::PipelineTask::revoke_memory (this=0x7f8d61302490, spill_context=...) at /root/doris/be/src/pipeline/pipeline_task.cpp:812 #20 0x000055f37fba3c2d in doris::QueryTaskController::revoke_memory (this=<optimized out>) at /root/doris/be/src/runtime/workload_management/query_task_controller.cpp:185 #21 0x000055f37fb99d08 in doris::WorkloadGroupMgr::handle_single_query_ (this=<optimized out>, requestor=..., size_to_reserve=size_to_reserve@entry=1024000, time_in_queue=time_in_queue@entry=27, paused_reason=...) at /root/doris/be/src/runtime/workload_group/workload_group_manager.cpp:820 #22 0x000055f37fb981d0 in doris::WorkloadGroupMgr::handle_paused_queries (this=0x7f9141d8a800) at /root/doris/be/src/runtime/workload_group/workload_group_manager.cpp:381 #23 0x000055f37eb97297 in doris::Daemon::memory_maintenance_thread (this=0x7ffe63cad730) at /root/doris/be/src/common/daemon.cpp:354 #24 0x000055f37fd812fc in std::function<void ()>::operator()() const (this=0x7f8dbb0c9fe0) at /usr/local/ldb-toolchain-v0.26/bin/../lib/gcc/x86_64-pc-linux-gnu/15/include/g++-v15/bits/std_function.h:593 #25 doris::Thread::supervise_thread (arg=0x7f914ba25e10) at /root/doris/be/src/util/thread.cpp:460 #26 0x00007f91fc75fac3 in start_thread (arg=<optimized out>) at ./nptl/pthread_create.c:442 #27 0x00007f91fc7f1850 in __closefrom_fallback (from=1674236160, dirfd_fallback=<optimized out>) at ../sysdeps/unix/sysv/linux/closefrom_fallback.c:45 #28 0x0000000000000000 in ?? () ``` Related PR: #xxx Problem Summary: ### Release note None ### Check List (For Author) - Test <!-- At least one of them must be included. --> - [ ] Regression test - [ ] Unit Test - [ ] Manual test (add detailed scripts or steps below) - [ ] No need to test or manual test. Explain why: - [ ] This is a refactor/code format and no logic has been changed. - [ ] Previous test can cover this change. - [ ] No code files have been changed. - [ ] Other reason <!-- Add your reason? --> - Behavior changed: - [ ] No. - [ ] Yes. <!-- Explain the behavior change --> - Does this need documentation? - [ ] No. - [ ] Yes. <!-- Add document PR link here. eg: apache/doris-website#1214 --> ### Check List (For Reviewer who merge this PR) - [ ] Confirm the release note - [ ] Confirm test cases - [ ] Confirm document - [ ] Add branch pick label <!-- Add branch pick label that this PR should merge into -->
1 parent 01f8339 commit 46d27a2

File tree

5 files changed

+157
-47
lines changed

5 files changed

+157
-47
lines changed

be/src/pipeline/exec/spill_utils.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,8 @@ class SpillRecoverRunnable : public SpillRunnable {
201201
}
202202

203203
void _on_task_started() override {
204-
LOG(INFO) << "SpillRecoverRunnable, Query: " << print_id(_state->query_id())
205-
<< " spill task started, pipeline task id: " << _state->task_id();
204+
VLOG_DEBUG << "SpillRecoverRunnable, Query: " << print_id(_state->query_id())
205+
<< " spill task started, pipeline task id: " << _state->task_id();
206206
COUNTER_UPDATE(_read_wait_in_queue_task_count, -1);
207207
COUNTER_UPDATE(_reading_task_count, 1);
208208
}

be/src/pipeline/pipeline_task.cpp

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <glog/logging.h>
2424

2525
#include <algorithm>
26+
#include <memory>
2627
#include <ostream>
2728
#include <vector>
2829

@@ -35,6 +36,7 @@
3536
#include "pipeline/pipeline_fragment_context.h"
3637
#include "pipeline/task_queue.h"
3738
#include "pipeline/task_scheduler.h"
39+
#include "revokable_task.h"
3840
#include "runtime/descriptors.h"
3941
#include "runtime/exec_env.h"
4042
#include "runtime/query_context.h"
@@ -99,14 +101,15 @@ PipelineTask::~PipelineTask() {
99101
// But pipeline task hold some objects, like operators, shared state, etc. So that should release
100102
// memory manually.
101103
#ifndef BE_TEST
102-
SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(_query_mem_tracker);
104+
if (_query_mem_tracker) {
105+
SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(_query_mem_tracker);
106+
}
103107
#endif
104108
_shared_state_map.clear();
105109
_sink_shared_state.reset();
106110
_op_shared_states.clear();
107111
_sink.reset();
108112
_operators.clear();
109-
_spill_context.reset();
110113
_block.reset();
111114
_pipeline.reset();
112115
}
@@ -306,17 +309,12 @@ bool PipelineTask::is_blockable() const {
306309
}
307310
}
308311

309-
return _need_to_revoke_memory ||
310-
std::ranges::any_of(_operators,
312+
return std::ranges::any_of(_operators,
311313
[&](OperatorPtr op) -> bool { return op->is_blockable(_state); }) ||
312314
_sink->is_blockable(_state);
313315
}
314316

315317
bool PipelineTask::_is_blocked() {
316-
if (_need_to_revoke_memory) {
317-
return false;
318-
}
319-
320318
// `_dry_run = true` means we do not need data from source operator.
321319
if (!_dry_run) {
322320
for (int i = cast_set<int>(_read_dependencies.size() - 1); i >= 0; i--) {
@@ -378,11 +376,15 @@ void PipelineTask::terminate() {
378376
* @return
379377
*/
380378
Status PipelineTask::execute(bool* done) {
381-
if (!_need_to_revoke_memory && (_exec_state != State::RUNNABLE || _blocked_dep != nullptr))
382-
[[unlikely]] {
379+
if (_exec_state != State::RUNNABLE || _blocked_dep != nullptr) [[unlikely]] {
380+
#ifdef BE_TEST
383381
return Status::InternalError("Pipeline task is not runnable! Task info: {}",
384382
debug_string());
383+
#else
384+
return Status::FatalError("Pipeline task is not runnable! Task info: {}", debug_string());
385+
#endif
385386
}
387+
386388
auto fragment_context = _fragment_context.lock();
387389
if (!fragment_context) {
388390
return Status::InternalError("Fragment already finished! Query: {}", print_id(_query_id));
@@ -477,11 +479,6 @@ Status PipelineTask::execute(bool* done) {
477479
break;
478480
}
479481

480-
if (_need_to_revoke_memory) {
481-
_need_to_revoke_memory = false;
482-
return _sink->revoke_memory(_state, _spill_context);
483-
}
484-
485482
if (time_spent > _exec_time_slice) {
486483
COUNTER_UPDATE(_yield_counts, 1);
487484
break;
@@ -610,6 +607,33 @@ Status PipelineTask::execute(bool* done) {
610607
return Status::OK();
611608
}
612609

610+
Status PipelineTask::do_revoke_memory(const std::shared_ptr<SpillContext>& spill_context) {
611+
auto fragment_context = _fragment_context.lock();
612+
if (!fragment_context) {
613+
return Status::InternalError("Fragment already finished! Query: {}", print_id(_query_id));
614+
}
615+
616+
SCOPED_ATTACH_TASK(_state);
617+
ThreadCpuStopWatch cpu_time_stop_watch;
618+
cpu_time_stop_watch.start();
619+
Defer running_defer {[&]() {
620+
int64_t delta_cpu_time = cpu_time_stop_watch.elapsed_time();
621+
_task_cpu_timer->update(delta_cpu_time);
622+
fragment_context->get_query_ctx()->resource_ctx()->cpu_context()->update_cpu_cost_ms(
623+
delta_cpu_time);
624+
625+
// If task is woke up early, we should terminate all operators, and this task could be closed immediately.
626+
if (_wake_up_early) {
627+
terminate();
628+
THROW_IF_ERROR(_root->terminate(_state));
629+
THROW_IF_ERROR(_sink->terminate(_state));
630+
_eos = true;
631+
}
632+
}};
633+
634+
return _sink->revoke_memory(_state, spill_context);
635+
}
636+
613637
bool PipelineTask::_try_to_reserve_memory(const size_t reserve_size, OperatorBase* op) {
614638
auto st = thread_context()->thread_mem_tracker_mgr->try_reserve(reserve_size);
615639
COUNTER_UPDATE(_memory_reserve_times, 1);
@@ -794,30 +818,27 @@ std::string PipelineTask::debug_string() {
794818
}
795819

796820
size_t PipelineTask::get_revocable_size() const {
797-
if (is_finalized() || _running || (_eos && !_spilling)) {
821+
if (!_opened || is_finalized() || _running || (_eos && !_spilling)) {
798822
return 0;
799823
}
800824

801825
return _sink->revocable_mem_size(_state);
802826
}
803827

804828
Status PipelineTask::revoke_memory(const std::shared_ptr<SpillContext>& spill_context) {
829+
DCHECK(spill_context);
805830
if (is_finalized()) {
806-
if (spill_context) {
807-
spill_context->on_task_finished();
808-
VLOG_DEBUG << "Query: " << print_id(_state->query_id()) << ", task: " << ((void*)this)
809-
<< " finalized";
810-
}
831+
spill_context->on_task_finished();
832+
VLOG_DEBUG << "Query: " << print_id(_state->query_id()) << ", task: " << ((void*)this)
833+
<< " finalized";
811834
return Status::OK();
812835
}
813836

814837
const auto revocable_size = _sink->revocable_mem_size(_state);
815838
if (revocable_size >= vectorized::SpillStream::MIN_SPILL_WRITE_BATCH_MEM) {
816-
_need_to_revoke_memory = true;
817-
_spill_context = spill_context;
818-
RETURN_IF_ERROR(
819-
_state->get_query_ctx()->get_pipe_exec_scheduler()->submit(shared_from_this()));
820-
} else if (spill_context) {
839+
auto revokable_task = std::make_shared<RevokableTask>(shared_from_this(), spill_context);
840+
RETURN_IF_ERROR(_state->get_query_ctx()->get_pipe_exec_scheduler()->submit(revokable_task));
841+
} else {
821842
spill_context->on_task_finished();
822843
LOG(INFO) << "Query: " << print_id(_state->query_id()) << ", task: " << ((void*)this)
823844
<< " has not enough data to revoke: " << revocable_size;

be/src/pipeline/pipeline_task.h

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -55,32 +55,32 @@ class PipelineTask : public std::enable_shared_from_this<PipelineTask> {
5555
shared_state_map,
5656
int task_idx);
5757

58-
~PipelineTask();
58+
virtual ~PipelineTask();
5959

6060
Status prepare(const std::vector<TScanRangeParams>& scan_range, const int sender_id,
6161
const TDataSink& tsink);
6262

63-
Status execute(bool* done);
63+
virtual Status execute(bool* done);
6464

6565
// if the pipeline create a bunch of pipeline task
6666
// must be call after all pipeline task is finish to release resource
67-
Status close(Status exec_status, bool close_sink = true);
67+
virtual Status close(Status exec_status, bool close_sink = true);
6868

69-
std::weak_ptr<PipelineFragmentContext>& fragment_context() { return _fragment_context; }
69+
virtual std::weak_ptr<PipelineFragmentContext>& fragment_context() { return _fragment_context; }
7070

7171
int get_thread_id(int num_threads) const {
7272
return _thread_id == -1 ? _thread_id : _thread_id % num_threads;
7373
}
7474

75-
PipelineTask& set_thread_id(int thread_id) {
75+
virtual PipelineTask& set_thread_id(int thread_id) {
7676
_thread_id = thread_id;
7777
if (thread_id != _thread_id) {
7878
COUNTER_UPDATE(_core_change_times, 1);
7979
}
8080
return *this;
8181
}
8282

83-
Status finalize();
83+
virtual Status finalize();
8484

8585
std::string debug_string();
8686

@@ -94,7 +94,7 @@ class PipelineTask : public std::enable_shared_from_this<PipelineTask> {
9494
* Pipeline task is blockable means it will be blocked in the next run. So we should put it into
9595
* the blocking task scheduler.
9696
*/
97-
bool is_blockable() const;
97+
virtual bool is_blockable() const;
9898

9999
/**
100100
* `shared_state` is shared by different pipeline tasks. This function aims to establish
@@ -125,7 +125,7 @@ class PipelineTask : public std::enable_shared_from_this<PipelineTask> {
125125
DataSinkOperatorPtr sink() const { return _sink; }
126126

127127
int task_id() const { return _index; };
128-
bool is_finalized() const { return _exec_state == State::FINALIZED; }
128+
virtual bool is_finalized() const { return _exec_state == State::FINALIZED; }
129129

130130
void set_wake_up_early(PipelineId wake_by = -1) {
131131
_wake_up_early = true;
@@ -153,19 +153,24 @@ class PipelineTask : public std::enable_shared_from_this<PipelineTask> {
153153
void pop_out_runnable_queue() { _wait_worker_watcher.stop(); }
154154

155155
bool is_running() { return _running.load(); }
156-
PipelineTask& set_running(bool running) {
157-
_running.exchange(running);
158-
return *this;
156+
virtual bool set_running(bool running) {
157+
bool old_value = !running;
158+
_running.compare_exchange_weak(old_value, running);
159+
return old_value;
159160
}
160161

161-
RuntimeState* runtime_state() const { return _state; }
162+
virtual RuntimeState* runtime_state() const { return _state; }
163+
164+
virtual std::string task_name() const {
165+
return fmt::format("task{}({})", _index, _pipeline->_name);
166+
}
162167

163-
std::string task_name() const { return fmt::format("task{}({})", _index, _pipeline->_name); }
168+
[[nodiscard]] Status do_revoke_memory(const std::shared_ptr<SpillContext>& spill_context);
164169

165170
// TODO: Maybe we do not need this safe code anymore
166171
void stop_if_finished();
167172

168-
PipelineId pipeline_id() const { return _pipeline->id(); }
173+
virtual PipelineId pipeline_id() const { return _pipeline->id(); }
169174
[[nodiscard]] size_t get_revocable_size() const;
170175
[[nodiscard]] Status revoke_memory(const std::shared_ptr<SpillContext>& spill_context);
171176

@@ -175,6 +180,10 @@ class PipelineTask : public std::enable_shared_from_this<PipelineTask> {
175180
return _state_transition(PipelineTask::State::BLOCKED);
176181
}
177182

183+
protected:
184+
// Only used for RevokableTask
185+
PipelineTask() : _index(0) {}
186+
178187
private:
179188
// Whether this task is blocked before execution (FE 2-phase commit trigger, runtime filters)
180189
bool _wait_to_start();
@@ -214,9 +223,6 @@ class PipelineTask : public std::enable_shared_from_this<PipelineTask> {
214223
// 3 update task statistics(update _queue_level/_core_id)
215224
int _queue_level = 0;
216225

217-
bool _need_to_revoke_memory = false;
218-
std::shared_ptr<SpillContext> _spill_context;
219-
220226
RuntimeProfile* _parent_profile = nullptr;
221227
std::unique_ptr<RuntimeProfile> _task_profile;
222228
RuntimeProfile::Counter* _task_cpu_timer = nullptr;

be/src/pipeline/revokable_task.h

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
#pragma once
19+
20+
#include <memory>
21+
#include <string>
22+
23+
#include "common/status.h"
24+
#include "pipeline/dependency.h"
25+
#include "pipeline/exec/operator.h"
26+
#include "pipeline/exec/spill_utils.h"
27+
#include "pipeline/pipeline.h"
28+
#include "pipeline/pipeline_task.h"
29+
#include "pipeline_task.h"
30+
31+
namespace doris {
32+
class RuntimeState;
33+
34+
namespace pipeline {
35+
class PipelineFragmentContext;
36+
37+
class RevokableTask : public PipelineTask {
38+
public:
39+
RevokableTask(PipelineTaskSPtr task, std::shared_ptr<SpillContext> spill_context)
40+
: _task(std::move(task)), _spill_context(std::move(spill_context)) {}
41+
42+
~RevokableTask() override = default;
43+
44+
RuntimeState* runtime_state() const override { return _task->runtime_state(); }
45+
46+
Status close(Status exec_status, bool close_sink) override {
47+
return _task->close(exec_status, close_sink);
48+
}
49+
50+
Status finalize() override { return _task->finalize(); }
51+
52+
bool set_running(bool running) override { return _task->set_running(running); }
53+
54+
bool is_finalized() const override { return _task->is_finalized(); }
55+
56+
std::weak_ptr<PipelineFragmentContext>& fragment_context() override {
57+
return _task->fragment_context();
58+
}
59+
60+
PipelineTask& set_thread_id(int thread_id) override { return _task->set_thread_id(thread_id); }
61+
62+
PipelineId pipeline_id() const override { return _task->pipeline_id(); }
63+
64+
std::string task_name() const override { return _task->task_name(); }
65+
66+
Status execute(bool* done) override { return _task->do_revoke_memory(_spill_context); }
67+
68+
bool is_blockable() const override { return true; }
69+
70+
private:
71+
PipelineTaskSPtr _task;
72+
std::shared_ptr<SpillContext> _spill_context;
73+
};
74+
75+
} // namespace pipeline
76+
} // namespace doris

be/src/pipeline/task_scheduler.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,19 +104,26 @@ void TaskScheduler::_do_work(int index) {
104104
// The task is already running, maybe block in now dependency wake up by other thread
105105
// but the block thread still hold the task, so put it back to the queue, until the hold
106106
// thread set task->set_running(false)
107-
if (task->is_running()) {
107+
// set_running return the old value
108+
if (task->set_running(true)) {
108109
static_cast<void>(_task_queue.push_back(task, index));
109110
continue;
110111
}
112+
111113
if (task->is_finalized()) {
114+
task->set_running(false);
112115
continue;
113116
}
117+
114118
auto fragment_context = task->fragment_context().lock();
115119
if (!fragment_context) {
116120
// Fragment already finished
121+
task->set_running(false);
117122
continue;
118123
}
119-
task->set_running(true).set_thread_id(index);
124+
125+
task->set_thread_id(index);
126+
120127
bool done = false;
121128
auto status = Status::OK();
122129
int64_t exec_ns = 0;

0 commit comments

Comments
 (0)