be/src/load/channel/load_channel_mgr.cpp
Line | Count | Source |
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 | | #include "load/channel/load_channel_mgr.h" |
19 | | |
20 | | #include <fmt/format.h> |
21 | | #include <gen_cpp/internal_service.pb.h> |
22 | | |
23 | | #include <algorithm> |
24 | | // IWYU pragma: no_include <bits/chrono.h> |
25 | | #include <chrono> // IWYU pragma: keep |
26 | | #include <ctime> |
27 | | #include <memory> |
28 | | #include <ostream> |
29 | | #include <string> |
30 | | #include <vector> |
31 | | |
32 | | #include "common/config.h" |
33 | | #include "common/logging.h" |
34 | | #include "common/metrics/doris_metrics.h" |
35 | | #include "common/metrics/metrics.h" |
36 | | #include "load/channel/load_channel.h" |
37 | | #include "runtime/exec_env.h" |
38 | | #include "util/debug_points.h" |
39 | | #include "util/defer_op.h" |
40 | | #include "util/thread.h" |
41 | | |
42 | | namespace doris { |
43 | | |
44 | | #ifndef BE_TEST |
45 | | constexpr uint32_t START_BG_INTERVAL = 60; |
46 | | #else |
47 | | constexpr uint32_t START_BG_INTERVAL = 1; |
48 | | #endif |
49 | | |
50 | | DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(load_channel_count, MetricUnit::NOUNIT); |
51 | | DEFINE_GAUGE_METRIC_PROTOTYPE_5ARG(load_channel_mem_consumption, MetricUnit::BYTES, "", |
52 | | mem_consumption, Labels({{"type", "load"}})); |
53 | | |
54 | 32.3k | static int64_t calc_channel_timeout_s(int64_t timeout_in_req_s) { |
55 | 32.3k | int64_t load_channel_timeout_s = config::streaming_load_rpc_max_alive_time_sec; |
56 | 32.3k | if (timeout_in_req_s > 0) { |
57 | 32.3k | load_channel_timeout_s = std::max<int64_t>(load_channel_timeout_s, timeout_in_req_s); |
58 | 32.3k | } |
59 | 32.3k | return load_channel_timeout_s; |
60 | 32.3k | } |
61 | | |
62 | 13 | LoadChannelMgr::LoadChannelMgr() : _stop_background_threads_latch(1) { |
63 | 13 | REGISTER_HOOK_METRIC(load_channel_count, [this]() { |
64 | | // std::lock_guard<std::mutex> l(_lock); |
65 | 13 | return _load_channels.size(); |
66 | 13 | }); |
67 | 13 | } |
68 | | |
69 | 9 | void LoadChannelMgr::stop() { |
70 | 9 | DEREGISTER_HOOK_METRIC(load_channel_count); |
71 | 9 | DEREGISTER_HOOK_METRIC(load_channel_mem_consumption); |
72 | 9 | _stop_background_threads_latch.count_down(); |
73 | 9 | if (_load_channels_clean_thread) { |
74 | 3 | _load_channels_clean_thread->join(); |
75 | 3 | } |
76 | 9 | } |
77 | | |
78 | 7 | Status LoadChannelMgr::init(int64_t process_mem_limit) { |
79 | 7 | _load_state_channels = std::make_unique<LoadStateChannelCache>(1024); |
80 | 7 | _final_tablet_result_cache = std::make_unique<FinalTabletResultCache>(); |
81 | 7 | RETURN_IF_ERROR(_start_bg_worker()); |
82 | 7 | return Status::OK(); |
83 | 7 | } |
84 | | |
85 | 48.9k | Status LoadChannelMgr::open(const PTabletWriterOpenRequest& params) { |
86 | 48.9k | UniqueId load_id(params.id()); |
87 | 48.9k | std::shared_ptr<LoadChannel> channel; |
88 | 48.9k | { |
89 | 48.9k | std::lock_guard<std::mutex> l(_lock); |
90 | 48.9k | auto it = _load_channels.find(load_id); |
91 | 48.9k | if (it != _load_channels.end()) { |
92 | 16.5k | channel = it->second; |
93 | 32.3k | } else { |
94 | | // create a new load channel |
95 | 32.3k | int64_t timeout_in_req_s = |
96 | 18.4E | params.has_load_channel_timeout_s() ? params.load_channel_timeout_s() : -1; |
97 | 32.3k | int64_t channel_timeout_s = calc_channel_timeout_s(timeout_in_req_s); |
98 | 32.3k | bool is_high_priority = (params.has_is_high_priority() && params.is_high_priority()); |
99 | | |
100 | 32.3k | int64_t wg_id = -1; |
101 | 32.3k | if (params.has_workload_group_id()) { |
102 | 32.3k | wg_id = params.workload_group_id(); |
103 | 32.3k | } |
104 | 32.3k | channel.reset(new LoadChannel(load_id, channel_timeout_s, is_high_priority, |
105 | 32.3k | params.sender_ip(), params.backend_id(), |
106 | 32.3k | params.enable_profile(), wg_id)); |
107 | 32.3k | _load_channels.insert({load_id, channel}); |
108 | 32.3k | } |
109 | 48.9k | } |
110 | | |
111 | 48.9k | RETURN_IF_ERROR(channel->open(params)); |
112 | | |
113 | 48.9k | return Status::OK(); |
114 | 48.9k | } |
115 | | |
116 | | Status LoadChannelMgr::_copy_cached_final_tablet_result(const UniqueId& load_id, |
117 | | const PTabletWriterAddBlockRequest& request, |
118 | | Cache::Handle* result_handle, |
119 | 6 | PTabletWriterAddBlockResult* response) { |
120 | 6 | DORIS_CHECK(result_handle != nullptr); |
121 | 6 | Defer release_result_handle( |
122 | 6 | [this, result_handle] { _final_tablet_result_cache->release(result_handle); }); |
123 | 6 | const auto* result_cache_value = reinterpret_cast<FinalTabletResultCache::CacheValue*>( |
124 | 6 | _final_tablet_result_cache->value(result_handle)); |
125 | 6 | const auto result = result_cache_value->_results.find(request.index_id()); |
126 | 6 | if (result == result_cache_value->_results.end()) { |
127 | 1 | LOG(WARNING) << "Final tablet result is unavailable for retried load " << load_id |
128 | 1 | << ", index_id=" << request.index_id() |
129 | 1 | << ", sender_id=" << request.sender_id(); |
130 | 1 | return Status::InternalError<false>( |
131 | 1 | "Final tablet result is unavailable for retried load {}, index_id={}, sender_id={}", |
132 | 1 | load_id.to_string(), request.index_id(), request.sender_id()); |
133 | 1 | } |
134 | 5 | DBUG_EXECUTE_IF("LoadChannelMgr.get.before_copy", DBUG_RUN_CALLBACK()); |
135 | 5 | response->CopyFrom(result->second.result); |
136 | 5 | response->set_final_tablet_result_fanout(request.sender_id() != result->second.owner_sender_id); |
137 | 5 | return Status::OK(); |
138 | 6 | } |
139 | | |
140 | | Status LoadChannelMgr::_get_load_channel(std::shared_ptr<LoadChannel>& channel, bool& is_eof, |
141 | | const UniqueId& load_id, |
142 | | const PTabletWriterAddBlockRequest& request, |
143 | 55.0k | PTabletWriterAddBlockResult* response) { |
144 | 55.0k | is_eof = false; |
145 | 55.0k | std::unique_lock<std::mutex> l(_lock); |
146 | 55.0k | auto it = _load_channels.find(load_id); |
147 | 55.0k | if (it == _load_channels.end()) { |
148 | 9 | Cache::Handle* handle = _load_state_channels->lookup(load_id.to_string()); |
149 | 9 | if (handle != nullptr) { |
150 | | // load is cancelled |
151 | 9 | if (auto* value = _load_state_channels->value(handle); value != nullptr) { |
152 | 1 | const auto* cache_value = reinterpret_cast<CacheValue*>(value); |
153 | 1 | const auto& cancel_reason = cache_value->_cancel_reason; |
154 | 1 | _load_state_channels->release(handle); |
155 | 1 | if (!cancel_reason.empty()) { |
156 | 1 | LOG(INFO) << fmt::format( |
157 | 1 | "The channel has been cancelled, load_id = {}, error = {}", |
158 | 1 | print_id(load_id), cancel_reason); |
159 | 1 | return Status::Cancelled(cancel_reason); |
160 | 1 | } |
161 | 8 | } else { |
162 | | // load is success, success only when eos be true |
163 | 8 | _load_state_channels->release(handle); |
164 | 8 | if (request.has_eos() && request.eos()) { |
165 | 8 | if (request.need_final_tablet_result()) { |
166 | 7 | auto* result_handle = |
167 | 7 | _final_tablet_result_cache->lookup(load_id.to_string()); |
168 | 7 | if (result_handle == nullptr) { |
169 | 1 | LOG(WARNING) << "Final tablet result is unavailable for retried load " |
170 | 1 | << load_id << ", sender_id=" << request.sender_id(); |
171 | 1 | return Status::InternalError<false>( |
172 | 1 | "Final tablet result is unavailable for retried load {}, " |
173 | 1 | "index_id={}, sender_id={}", |
174 | 1 | load_id.to_string(), request.index_id(), request.sender_id()); |
175 | 1 | } |
176 | 6 | l.unlock(); |
177 | 6 | RETURN_IF_ERROR(_copy_cached_final_tablet_result(load_id, request, |
178 | 6 | result_handle, response)); |
179 | 6 | } |
180 | 6 | is_eof = true; |
181 | 6 | return Status::OK(); |
182 | 8 | } |
183 | 8 | } |
184 | 9 | } |
185 | | |
186 | 0 | return Status::InternalError<false>( |
187 | 0 | "Fail to add batch in load channel: unknown load_id={}. " |
188 | 0 | "This may be due to a BE restart. Please retry the load.", |
189 | 0 | load_id.to_string()); |
190 | 9 | } |
191 | 55.0k | channel = it->second; |
192 | 55.0k | if (request.eos() && request.need_final_tablet_result()) { |
193 | 1 | channel->_reserve_final_tablet_result(request.index_id()); |
194 | 1 | } |
195 | 55.0k | return Status::OK(); |
196 | 55.0k | } |
197 | | |
198 | | Status LoadChannelMgr::add_batch(const PTabletWriterAddBlockRequest& request, |
199 | | PTabletWriterAddBlockResult* response, |
200 | 55.0k | google::protobuf::Closure** done) { |
201 | 55.0k | UniqueId load_id(request.id()); |
202 | | // 1. get load channel |
203 | 55.0k | std::shared_ptr<LoadChannel> channel; |
204 | 55.0k | bool is_eof; |
205 | 55.0k | auto status = _get_load_channel(channel, is_eof, load_id, request, response); |
206 | 55.0k | if (!status.ok() || is_eof) { |
207 | 9 | return status; |
208 | 9 | } |
209 | 55.0k | SCOPED_TIMER(channel->get_mgr_add_batch_timer()); |
210 | | |
211 | 55.0k | if (!channel->is_high_priority()) { |
212 | | // 2. check if mem consumption exceed limit |
213 | | // If this is a high priority load task, do not handle this. |
214 | | // because this may block for a while, which may lead to rpc timeout. |
215 | 54.4k | SCOPED_TIMER(channel->get_handle_mem_limit_timer()); |
216 | 54.4k | ExecEnv::GetInstance()->memtable_memory_limiter()->handle_memtable_flush( |
217 | 54.4k | [channel]() { return channel->is_cancelled(); }, channel->workload_group().get()); |
218 | 54.4k | if (channel->is_cancelled()) { |
219 | 0 | return Status::Cancelled("LoadChannel has been cancelled: {}.", load_id.to_string()); |
220 | 0 | } |
221 | 54.4k | } |
222 | | |
223 | | // 3. add batch to load channel |
224 | | // batch may not exist in request(eg: eos request without batch), |
225 | | // this case will be handled in load channel's add batch method. |
226 | 55.0k | Status st = channel->add_batch(request, response, done); |
227 | 55.0k | if (UNLIKELY(!st.ok())) { |
228 | 43 | RETURN_IF_ERROR(channel->cancel(st)); |
229 | 43 | return st; |
230 | 43 | } |
231 | | |
232 | | // 4. handle finish |
233 | 55.0k | if (channel->is_finished()) { |
234 | 32.5k | _finish_load_channel(load_id, channel); |
235 | 32.5k | } |
236 | 55.0k | return Status::OK(); |
237 | 55.0k | } |
238 | | |
239 | | void LoadChannelMgr::_finish_load_channel(const UniqueId load_id, |
240 | 32.5k | const std::shared_ptr<LoadChannel>& channel) { |
241 | 32.5k | const std::string cache_key = load_id.to_string(); |
242 | 32.5k | bool need_final_tablet_result = false; |
243 | 32.5k | { |
244 | 32.5k | std::lock_guard<std::mutex> l(_lock); |
245 | 32.5k | const auto channel_it = _load_channels.find(load_id); |
246 | 32.5k | if (channel_it == _load_channels.end() || channel_it->second != channel) { |
247 | 343 | return; |
248 | 343 | } |
249 | 32.1k | need_final_tablet_result = channel->need_final_tablet_result(); |
250 | 32.2k | if (!need_final_tablet_result) { |
251 | 32.2k | _load_channels.erase(channel_it); |
252 | 32.2k | auto* handle = _load_state_channels->insert(cache_key, nullptr, 1, 1); |
253 | 32.2k | _load_state_channels->release(handle); |
254 | 18.4E | } else if (!_finishing_load_channels.emplace(load_id).second) { |
255 | 0 | return; |
256 | 0 | } |
257 | 32.1k | } |
258 | 32.2k | if (!need_final_tablet_result) { |
259 | 32.2k | VLOG_CRITICAL << "removed load channel " << load_id; |
260 | 32.2k | return; |
261 | 32.2k | } |
262 | 18.4E | bool finishing_registered = true; |
263 | 18.4E | Defer clear_finishing([this, load_id, &finishing_registered] { |
264 | 3 | if (finishing_registered) { |
265 | 0 | std::lock_guard<std::mutex> l(_lock); |
266 | 0 | _finishing_load_channels.erase(load_id); |
267 | 0 | } |
268 | 3 | }); |
269 | 18.4E | std::unique_ptr<FinalTabletResultCache::CacheValue> cache_value; |
270 | 18.4E | DBUG_EXECUTE_IF("LoadChannelMgr.finish.before_copy", DBUG_RUN_CALLBACK()); |
271 | 18.4E | size_t cache_size = 0; |
272 | 18.4E | std::unordered_map<int64_t, LoadChannel::FinalTabletResult> final_tablet_results; |
273 | 18.4E | bool oversized = false; |
274 | 18.4E | size_t result_bytes = 0; |
275 | 18.4E | const size_t cache_overhead = |
276 | 18.4E | sizeof(FinalTabletResultCache::CacheValue) + sizeof(LRUHandle) - 1 + cache_key.size(); |
277 | 18.4E | DORIS_CHECK(channel->copy_final_tablet_results( |
278 | 18.4E | &final_tablet_results, FinalTabletResultCache::MAX_BYTES - cache_overhead, &oversized, |
279 | 18.4E | &result_bytes)); |
280 | 18.4E | if (!oversized && !final_tablet_results.empty()) { |
281 | 3 | cache_value = std::make_unique<FinalTabletResultCache::CacheValue>(); |
282 | 3 | cache_value->_results = std::move(final_tablet_results); |
283 | 3 | cache_size = sizeof(FinalTabletResultCache::CacheValue) + result_bytes; |
284 | 18.4E | } else if (oversized) { |
285 | 0 | LOG(WARNING) << "Skip caching final tablet result for load " << load_id |
286 | 0 | << ", oversized=" << oversized; |
287 | 0 | } |
288 | 18.4E | Cache::Handle* result_handle = nullptr; |
289 | 18.4E | Defer release_result_handle([this, &result_handle] { |
290 | 3 | if (result_handle != nullptr) { |
291 | 3 | _final_tablet_result_cache->release(result_handle); |
292 | 3 | } |
293 | 3 | }); |
294 | 18.4E | _final_tablet_result_cache->erase(cache_key); |
295 | 18.4E | if (cache_value != nullptr) { |
296 | 3 | result_handle = _final_tablet_result_cache->insert(cache_key, cache_value.get(), cache_size, |
297 | 3 | cache_size); |
298 | 3 | cache_value.release(); |
299 | 3 | } |
300 | 18.4E | bool published = false; |
301 | 18.4E | { |
302 | 18.4E | std::lock_guard<std::mutex> l(_lock); |
303 | 18.4E | const auto channel_it = _load_channels.find(load_id); |
304 | 18.4E | if (channel_it != _load_channels.end() && channel_it->second == channel) { |
305 | 3 | _load_channels.erase(channel_it); |
306 | 3 | auto* handle = _load_state_channels->insert(cache_key, nullptr, 1, 1); |
307 | 3 | _load_state_channels->release(handle); |
308 | 3 | published = true; |
309 | 3 | } |
310 | 18.4E | _finishing_load_channels.erase(load_id); |
311 | 18.4E | finishing_registered = false; |
312 | 18.4E | } |
313 | 18.4E | if (!published) { |
314 | 0 | _final_tablet_result_cache->erase(cache_key); |
315 | 0 | return; |
316 | 0 | } |
317 | 18.4E | VLOG_CRITICAL << "removed load channel " << load_id; |
318 | 18.4E | } |
319 | | |
320 | 128 | Status LoadChannelMgr::cancel(const PTabletWriterCancelRequest& params) { |
321 | 128 | UniqueId load_id(params.id()); |
322 | 128 | std::shared_ptr<LoadChannel> cancelled_channel; |
323 | 128 | { |
324 | 128 | std::lock_guard<std::mutex> l(_lock); |
325 | 128 | const bool finish_in_progress = |
326 | 128 | !_finishing_load_channels.empty() && _finishing_load_channels.contains(load_id); |
327 | | // Once final-result publication starts, finish wins so a success marker can never outlive |
328 | | // its reproducible result because of a concurrent cancel. |
329 | 128 | if (const auto channel_it = _load_channels.find(load_id); |
330 | 128 | !finish_in_progress && channel_it != _load_channels.end()) { |
331 | 77 | cancelled_channel = channel_it->second; |
332 | 77 | _load_channels.erase(channel_it); |
333 | 77 | } |
334 | 128 | if (!finish_in_progress) { |
335 | | // We just need to record the first cancel msg |
336 | 127 | auto* existing_handle = _load_state_channels->lookup(load_id.to_string()); |
337 | 127 | if (existing_handle == nullptr) { |
338 | 75 | if (params.has_cancel_reason() && !params.cancel_reason().empty()) { |
339 | 75 | std::unique_ptr<CacheValue> cancel_reason_ptr = std::make_unique<CacheValue>(); |
340 | 75 | cancel_reason_ptr->_cancel_reason = params.cancel_reason(); |
341 | 75 | size_t cache_capacity = |
342 | 75 | cancel_reason_ptr->_cancel_reason.capacity() + sizeof(CacheValue); |
343 | 75 | auto* handle = _load_state_channels->insert( |
344 | 75 | load_id.to_string(), cancel_reason_ptr.get(), 1, cache_capacity); |
345 | 75 | cancel_reason_ptr.release(); |
346 | 75 | _load_state_channels->release(handle); |
347 | 75 | LOG(INFO) << fmt::format("load_id = {}, record_error reason = {}", |
348 | 75 | print_id(load_id), params.cancel_reason()); |
349 | 75 | } |
350 | 75 | } else { |
351 | 52 | _load_state_channels->release(existing_handle); |
352 | 52 | } |
353 | 127 | } |
354 | 128 | } |
355 | | |
356 | 128 | if (cancelled_channel != nullptr) { |
357 | 77 | const Status reason = params.has_cancel_reason() |
358 | 77 | ? Status::Cancelled(params.cancel_reason()) |
359 | 77 | : Status::Cancelled("Load channel cancelled"); |
360 | 77 | RETURN_IF_ERROR(cancelled_channel->cancel(reason)); |
361 | 77 | LOG(INFO) << "load channel has been cancelled: " << load_id; |
362 | 77 | } |
363 | | |
364 | 128 | return Status::OK(); |
365 | 128 | } |
366 | | |
367 | 7 | Status LoadChannelMgr::_start_bg_worker() { |
368 | 7 | RETURN_IF_ERROR(Thread::create( |
369 | 7 | "LoadChannelMgr", "cancel_timeout_load_channels", |
370 | 7 | [this]() { |
371 | 7 | while (!_stop_background_threads_latch.wait_for( |
372 | 7 | std::chrono::seconds(START_BG_INTERVAL))) { |
373 | 7 | static_cast<void>(_start_load_channels_clean()); |
374 | 7 | } |
375 | 7 | }, |
376 | 7 | &_load_channels_clean_thread)); |
377 | | |
378 | 7 | return Status::OK(); |
379 | 7 | } |
380 | | |
381 | 225 | Status LoadChannelMgr::_start_load_channels_clean() { |
382 | 225 | std::vector<std::shared_ptr<LoadChannel>> need_delete_channels; |
383 | 225 | LOG(INFO) << "cleaning timed out load channels"; |
384 | 225 | time_t now = time(nullptr); |
385 | 225 | { |
386 | 225 | std::vector<UniqueId> need_delete_channel_ids; |
387 | 225 | std::lock_guard<std::mutex> l(_lock); |
388 | 225 | int i = 0; |
389 | 246 | for (auto& kv : _load_channels) { |
390 | 246 | if (!_finishing_load_channels.empty() && _finishing_load_channels.contains(kv.first)) { |
391 | 0 | continue; |
392 | 0 | } |
393 | 246 | VLOG_CRITICAL << "load channel[" << i++ << "]: " << *(kv.second); |
394 | 246 | time_t last_updated_time = kv.second->last_updated_time(); |
395 | 246 | if (difftime(now, last_updated_time) >= kv.second->timeout()) { |
396 | 0 | need_delete_channel_ids.emplace_back(kv.first); |
397 | 0 | need_delete_channels.emplace_back(kv.second); |
398 | 0 | } |
399 | 246 | } |
400 | | |
401 | 225 | for (auto& key : need_delete_channel_ids) { |
402 | 0 | _load_channels.erase(key); |
403 | 0 | LOG(INFO) << "erase timeout load channel: " << key; |
404 | 0 | } |
405 | 225 | } |
406 | | |
407 | | // we must cancel these load channels before destroying them. |
408 | | // otherwise some object may be invalid before trying to visit it. |
409 | | // eg: MemTracker in load channel |
410 | 225 | for (auto& channel : need_delete_channels) { |
411 | 0 | RETURN_IF_ERROR(channel->cancel(Status::TimedOut("Load channel timed out"))); |
412 | 0 | LOG(INFO) << "load channel has been safely deleted: " << channel->load_id() |
413 | 0 | << ", timeout(s): " << channel->timeout(); |
414 | 0 | } |
415 | | |
416 | 225 | return Status::OK(); |
417 | 225 | } |
418 | | } // namespace doris |