Coverage Report

Created: 2026-08-28 14:02

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/load/channel/load_stream.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_stream.h"
19
20
#include <errno.h>
21
22
#include <brpc/stream.h>
23
#include <bthread/bthread.h>
24
#include <bthread/condition_variable.h>
25
#include <bthread/mutex.h>
26
27
#include <memory>
28
#include <sstream>
29
30
#include "bvar/bvar.h"
31
#include "cloud/config.h"
32
#include "common/signal_handler.h"
33
#include "load/channel/load_channel.h"
34
#include "load/channel/load_stream_mgr.h"
35
#include "load/channel/load_stream_writer.h"
36
#include "load/delta_writer/delta_writer.h"
37
#include "runtime/exec_env.h"
38
#include "runtime/fragment_mgr.h"
39
#include "runtime/runtime_profile.h"
40
#include "runtime/workload_group/workload_group_manager.h"
41
#include "storage/rowset/rowset_factory.h"
42
#include "storage/rowset/rowset_meta.h"
43
#include "storage/storage_engine.h"
44
#include "storage/tablet/tablet.h"
45
#include "storage/tablet/tablet_fwd.h"
46
#include "storage/tablet/tablet_manager.h"
47
#include "storage/tablet/tablet_schema.h"
48
#include "storage/tablet_info.h"
49
#include "util/debug_points.h"
50
#include "util/defer_op.h"
51
#include "util/thrift_util.h"
52
#include "util/uid_util.h"
53
54
#define UNKNOWN_ID_FOR_TEST 0x7c00
55
56
namespace doris {
57
58
bvar::Adder<int64_t> g_load_stream_cnt("load_stream_count");
59
bvar::LatencyRecorder g_load_stream_flush_wait_ms("load_stream_flush_wait_ms");
60
bvar::Adder<int> g_load_stream_flush_running_threads("load_stream_flush_wait_threads");
61
62
TabletStream::TabletStream(const PUniqueId& load_id, int64_t id, int64_t txn_id,
63
                           LoadStreamMgr* load_stream_mgr, RuntimeProfile* profile)
64
15
        : _id(id),
65
15
          _next_segid(0),
66
15
          _load_id(load_id),
67
15
          _txn_id(txn_id),
68
15
          _load_stream_mgr(load_stream_mgr) {
69
15
    load_stream_mgr->create_token(_flush_token);
70
15
    _profile = profile->create_child(fmt::format("TabletStream {}", id), true, true);
71
15
    _append_data_timer = ADD_TIMER(_profile, "AppendDataTime");
72
15
    _add_segment_timer = ADD_TIMER(_profile, "AddSegmentTime");
73
15
    _close_wait_timer = ADD_TIMER(_profile, "CloseWaitTime");
74
15
}
75
76
10
inline std::ostream& operator<<(std::ostream& ostr, const TabletStream& tablet_stream) {
77
10
    ostr << "load_id=" << print_id(tablet_stream._load_id) << ", txn_id=" << tablet_stream._txn_id
78
10
         << ", tablet_id=" << tablet_stream._id << ", status=" << tablet_stream._status.status();
79
10
    return ostr;
80
10
}
81
82
26
Status TabletStream::_wait_for_task_slot(size_t max_tasks, int64_t timeout_ms) {
83
26
    DCHECK_GT(timeout_ms, 0);
84
26
    std::unique_lock<bthread::Mutex> lock(_flush_task_lock);
85
86
26
    while (_pending_flush_tasks >= max_tasks) {
87
0
        int ret = _flush_task_cv.wait_for(lock, timeout_ms * 1000);
88
89
0
        if (ret == ETIMEDOUT) {
90
0
            return Status::Error<true>(
91
0
                    ErrorCode::INTERNAL_ERROR,
92
0
                    "wait flush token back pressure time is more than "
93
0
                    "load_stream_max_wait_flush_token_time {}, pending_flush_tasks={}, "
94
0
                    "flush_token_max_tasks={}",
95
0
                    timeout_ms, _pending_flush_tasks, max_tasks);
96
0
        }
97
0
        if (ret != 0) {
98
0
            return Status::Error<true>(ErrorCode::INTERNAL_ERROR,
99
0
                                       "wait flush task slot failed, ret={}", ret);
100
0
        }
101
0
    }
102
26
    ++_pending_flush_tasks;
103
26
    return Status::OK();
104
26
}
105
106
26
void TabletStream::_release_task_slot() {
107
26
    DCHECK_GT(_pending_flush_tasks, 0);
108
109
26
    std::lock_guard<bthread::Mutex> lock(_flush_task_lock);
110
26
    --_pending_flush_tasks;
111
26
    _flush_task_cv.notify_one();
112
26
}
113
114
Status TabletStream::init(std::shared_ptr<OlapTableSchemaParam> schema, int64_t index_id,
115
15
                          int64_t partition_id) {
116
15
    WriteRequest req {
117
15
            .tablet_id = _id,
118
15
            .txn_id = _txn_id,
119
15
            .index_id = index_id,
120
15
            .partition_id = partition_id,
121
15
            .load_id = _load_id,
122
15
            .table_schema_param = schema,
123
            // TODO(plat1ko): write_file_cache
124
15
            .storage_vault_id {},
125
15
    };
126
127
15
    _load_stream_writer = std::make_shared<LoadStreamWriter>(&req, _profile);
128
15
    DBUG_EXECUTE_IF("TabletStream.init.uninited_writer", {
129
15
        _status.update(Status::Uninitialized("fault injection"));
130
15
        return _status.status();
131
15
    });
132
15
    _status.update(_load_stream_writer->init());
133
15
    if (!_status.ok()) {
134
1
        LOG(INFO) << "failed to init rowset builder due to " << *this;
135
1
    }
136
15
    return _status.status();
137
15
}
138
139
27
Status TabletStream::append_data(const PStreamHeader& header, butil::IOBuf* data) {
140
27
    if (!_status.ok()) {
141
1
        return _status.status();
142
1
    }
143
144
    // dispatch add_segment request
145
26
    if (header.opcode() == PStreamHeader::ADD_SEGMENT) {
146
0
        return add_segment(header, data);
147
0
    }
148
149
26
    SCOPED_TIMER(_append_data_timer);
150
151
26
    int64_t src_id = header.src_id();
152
26
    uint32_t segid = header.segment_id();
153
    // Ensure there are enough space and mapping are built.
154
26
    SegIdMapping* mapping = nullptr;
155
26
    {
156
26
        std::lock_guard lock_guard(_lock);
157
26
        if (!_segids_mapping.contains(src_id)) {
158
14
            _segids_mapping[src_id] = std::make_unique<SegIdMapping>();
159
14
        }
160
26
        mapping = _segids_mapping[src_id].get();
161
26
    }
162
26
    if (segid + 1 > mapping->size()) {
163
        // TODO: Each sender lock is enough.
164
15
        std::lock_guard lock_guard(_lock);
165
15
        ssize_t origin_size = mapping->size();
166
15
        if (segid + 1 > origin_size) {
167
15
            mapping->resize(segid + 1, std::numeric_limits<uint32_t>::max());
168
39
            for (size_t index = origin_size; index <= segid; index++) {
169
24
                mapping->at(index) = _next_segid;
170
24
                _next_segid++;
171
24
                VLOG_DEBUG << "src_id=" << src_id << ", segid=" << index << " to "
172
0
                           << " segid=" << _next_segid - 1 << ", " << *this;
173
24
            }
174
15
        }
175
15
    }
176
177
    // Each sender sends data in one segment sequential, so we also do not
178
    // need a lock here.
179
26
    bool eos = header.segment_eos();
180
26
    FileType file_type = header.file_type();
181
26
    uint32_t new_segid = mapping->at(segid);
182
26
    DCHECK(new_segid != std::numeric_limits<uint32_t>::max());
183
26
    butil::IOBuf buf = data->movable();
184
26
    auto flush_func = [this, new_segid, eos, buf, header, file_type]() mutable {
185
26
        signal::set_signal_task_id(_load_id);
186
26
        g_load_stream_flush_running_threads << -1;
187
26
        Defer defer {[this]() {
188
26
            _release_task_slot();
189
26
        }};
190
26
        auto st = _load_stream_writer->append_data(new_segid, header.offset(), buf, file_type);
191
26
        if (!st.ok() && !config::is_cloud_mode()) {
192
1
            auto res = ExecEnv::get_tablet(_id);
193
1
            TabletSharedPtr tablet =
194
1
                    res.has_value() ? std::dynamic_pointer_cast<Tablet>(res.value()) : nullptr;
195
1
            if (tablet) {
196
1
                tablet->report_error(st);
197
1
            }
198
1
        }
199
26
        if (eos && st.ok()) {
200
21
            DBUG_EXECUTE_IF("TabletStream.append_data.unknown_file_type",
201
21
                            { file_type = static_cast<FileType>(-1); });
202
21
            if (file_type == FileType::SEGMENT_FILE || file_type == FileType::INVERTED_INDEX_FILE) {
203
21
                st = _load_stream_writer->close_writer(new_segid, file_type);
204
21
            } else {
205
0
                st = Status::InternalError(
206
0
                        "appent data failed, file type error, file type = {}, "
207
0
                        "segment_id={}",
208
0
                        file_type, new_segid);
209
0
            }
210
21
        }
211
26
        DBUG_EXECUTE_IF("TabletStream.append_data.append_failed",
212
26
                        { st = Status::InternalError("fault injection"); });
213
26
        if (!st.ok()) {
214
2
            _status.update(st);
215
2
            LOG(WARNING) << "write data failed " << st << ", " << *this;
216
2
        }
217
26
    };
218
26
    auto load_stream_flush_token_max_tasks = config::load_stream_flush_token_max_tasks;
219
26
    auto load_stream_max_wait_flush_token_time_ms =
220
26
            config::load_stream_max_wait_flush_token_time_ms;
221
26
    DBUG_EXECUTE_IF("TabletStream.append_data.long_wait", {
222
26
        load_stream_flush_token_max_tasks = 0;
223
26
        load_stream_max_wait_flush_token_time_ms = 1000;
224
26
    });
225
26
    MonotonicStopWatch timer;
226
26
    timer.start();
227
26
    auto wait_st = _wait_for_task_slot(load_stream_flush_token_max_tasks,
228
26
                                       load_stream_max_wait_flush_token_time_ms);
229
26
    if (!wait_st.ok()) {
230
0
        _status.update(wait_st);
231
0
        return _status.status();
232
0
    }
233
26
    timer.stop();
234
26
    int64_t time_ms = timer.elapsed_time() / 1000 / 1000;
235
26
    g_load_stream_flush_wait_ms << time_ms;
236
26
    g_load_stream_flush_running_threads << 1;
237
26
    Status st = Status::OK();
238
26
    DBUG_EXECUTE_IF("TabletStream.append_data.submit_func_failed",
239
26
                    { st = Status::InternalError("fault injection"); });
240
26
    if (st.ok()) {
241
26
        st = _flush_token->submit_func(flush_func);
242
26
    }
243
26
    if (!st.ok()) {
244
0
        _release_task_slot();
245
0
        g_load_stream_flush_running_threads << -1;
246
0
        _status.update(st);
247
0
    }
248
26
    return _status.status();
249
26
}
250
251
0
Status TabletStream::add_segment(const PStreamHeader& header, butil::IOBuf* data) {
252
0
    if (!_status.ok()) {
253
0
        return _status.status();
254
0
    }
255
256
0
    SCOPED_TIMER(_add_segment_timer);
257
0
    DCHECK(header.has_segment_statistics());
258
0
    SegmentStatistics stat(header.segment_statistics());
259
260
0
    int64_t src_id = header.src_id();
261
0
    uint32_t segid = header.segment_id();
262
0
    uint32_t new_segid;
263
0
    DBUG_EXECUTE_IF("TabletStream.add_segment.unknown_segid", { segid = UNKNOWN_ID_FOR_TEST; });
264
0
    {
265
0
        std::lock_guard lock_guard(_lock);
266
0
        if (!_segids_mapping.contains(src_id)) {
267
0
            _status.update(Status::InternalError(
268
0
                    "add segment failed, no segment written by this src be yet, src_id={}, "
269
0
                    "segment_id={}",
270
0
                    src_id, segid));
271
0
            return _status.status();
272
0
        }
273
0
        DBUG_EXECUTE_IF("TabletStream.add_segment.segid_never_written",
274
0
                        { segid = static_cast<uint32_t>(_segids_mapping[src_id]->size()); });
275
0
        if (segid >= _segids_mapping[src_id]->size()) {
276
0
            _status.update(Status::InternalError(
277
0
                    "add segment failed, segment is never written, src_id={}, segment_id={}",
278
0
                    src_id, segid));
279
0
            return _status.status();
280
0
        }
281
0
        new_segid = _segids_mapping[src_id]->at(segid);
282
0
    }
283
0
    DCHECK(new_segid != std::numeric_limits<uint32_t>::max());
284
285
0
    auto add_segment_func = [this, new_segid, stat]() {
286
0
        signal::set_signal_task_id(_load_id);
287
0
        Defer defer {[this]() { _release_task_slot(); }};
288
0
        auto st = _load_stream_writer->add_segment(new_segid, stat);
289
0
        DBUG_EXECUTE_IF("TabletStream.add_segment.add_segment_failed",
290
0
                        { st = Status::InternalError("fault injection"); });
291
0
        if (!st.ok()) {
292
0
            _status.update(st);
293
0
            LOG(INFO) << "add segment failed " << *this;
294
0
        }
295
0
    };
296
0
    auto wait_st = _wait_for_task_slot(config::load_stream_flush_token_max_tasks,
297
0
                                       config::load_stream_max_wait_flush_token_time_ms);
298
0
    if (!wait_st.ok()) {
299
0
        _status.update(wait_st);
300
0
        return _status.status();
301
0
    }
302
0
    Status st = Status::OK();
303
0
    DBUG_EXECUTE_IF("TabletStream.add_segment.submit_func_failed",
304
0
                    { st = Status::InternalError("fault injection"); });
305
0
    if (st.ok()) {
306
0
        st = _flush_token->submit_func(add_segment_func);
307
0
    }
308
0
    if (!st.ok()) {
309
0
        _release_task_slot();
310
0
        _status.update(st);
311
0
    }
312
0
    return _status.status();
313
0
}
314
315
33
Status TabletStream::_run_in_heavy_work_pool(std::function<Status()> fn) {
316
33
    bthread::Mutex mu;
317
33
    std::unique_lock<bthread::Mutex> lock(mu);
318
33
    bthread::ConditionVariable cv;
319
33
    auto st = Status::OK();
320
33
    auto func = [this, &mu, &cv, &st, &fn] {
321
33
        signal::set_signal_task_id(_load_id);
322
33
        st = fn();
323
33
        std::lock_guard<bthread::Mutex> lock(mu);
324
33
        cv.notify_one();
325
33
    };
326
33
    bool ret = _load_stream_mgr->heavy_work_pool()->try_offer(func);
327
33
    if (!ret) {
328
0
        return Status::Error<ErrorCode::INTERNAL_ERROR>(
329
0
                "there is not enough thread resource for close load");
330
0
    }
331
33
    cv.wait(lock);
332
33
    return st;
333
33
}
334
335
30
void TabletStream::wait_for_flush_tasks() {
336
30
    {
337
30
        std::lock_guard lock_guard(_lock);
338
30
        if (_flush_tasks_done) {
339
15
            return;
340
15
        }
341
15
        _flush_tasks_done = true;
342
15
    }
343
344
15
    if (!_status.ok()) {
345
1
        _flush_token->shutdown();
346
1
        return;
347
1
    }
348
349
    // Use heavy_work_pool to avoid blocking bthread
350
14
    auto st = _run_in_heavy_work_pool([this]() {
351
14
        _flush_token->wait();
352
14
        return Status::OK();
353
14
    });
354
14
    if (!st.ok()) {
355
        // If heavy_work_pool is unavailable, fall back to shutdown
356
        // which will cancel pending tasks and wait for running tasks
357
0
        _flush_token->shutdown();
358
0
        _status.update(st);
359
0
    }
360
14
}
361
362
15
void TabletStream::pre_close() {
363
15
    SCOPED_TIMER(_close_wait_timer);
364
15
    wait_for_flush_tasks();
365
366
15
    if (!_status.ok()) {
367
3
        return;
368
3
    }
369
370
12
    DBUG_EXECUTE_IF("TabletStream.close.segment_num_mismatch", { _num_segments++; });
371
12
    if (_check_num_segments && (_next_segid.load() != _num_segments)) {
372
2
        _status.update(Status::Corruption(
373
2
                "segment num mismatch in tablet {}, expected: {}, actual: {}, load_id: {}", _id,
374
2
                _num_segments, _next_segid.load(), print_id(_load_id)));
375
2
        return;
376
2
    }
377
378
10
    _status.update(_run_in_heavy_work_pool([this]() { return _load_stream_writer->pre_close(); }));
379
10
}
380
381
15
Status TabletStream::close() {
382
15
    if (!_status.ok()) {
383
6
        return _status.status();
384
6
    }
385
386
9
    SCOPED_TIMER(_close_wait_timer);
387
9
    _status.update(_run_in_heavy_work_pool([this]() { return _load_stream_writer->close(); }));
388
9
    return _status.status();
389
15
}
390
391
IndexStream::IndexStream(const PUniqueId& load_id, int64_t id, int64_t txn_id,
392
                         std::shared_ptr<OlapTableSchemaParam> schema,
393
                         LoadStreamMgr* load_stream_mgr, RuntimeProfile* profile)
394
30
        : _id(id),
395
30
          _load_id(load_id),
396
30
          _txn_id(txn_id),
397
30
          _schema(schema),
398
30
          _load_stream_mgr(load_stream_mgr) {
399
30
    _profile = profile->create_child(fmt::format("IndexStream {}", id), true, true);
400
30
    _append_data_timer = ADD_TIMER(_profile, "AppendDataTime");
401
30
    _close_wait_timer = ADD_TIMER(_profile, "CloseWaitTime");
402
30
}
403
404
30
IndexStream::~IndexStream() {
405
    // Ensure all TabletStreams have their flush tokens properly handled before destruction.
406
    // In normal flow, close() should have called pre_close() on all tablet streams.
407
    // But if IndexStream is destroyed without close() being called (e.g., on_idle_timeout),
408
    // we need to wait for flush tasks here to ensure flush tokens are properly shut down.
409
30
    for (auto& [_, tablet_stream] : _tablet_streams_map) {
410
15
        tablet_stream->wait_for_flush_tasks();
411
15
    }
412
30
}
413
414
27
Status IndexStream::append_data(const PStreamHeader& header, butil::IOBuf* data) {
415
27
    SCOPED_TIMER(_append_data_timer);
416
27
    int64_t tablet_id = header.tablet_id();
417
27
    TabletStreamSharedPtr tablet_stream;
418
27
    {
419
27
        std::lock_guard lock_guard(_lock);
420
27
        auto it = _tablet_streams_map.find(tablet_id);
421
27
        if (it == _tablet_streams_map.end()) {
422
13
            _init_tablet_stream(tablet_stream, tablet_id, header.partition_id());
423
14
        } else {
424
14
            tablet_stream = it->second;
425
14
        }
426
27
    }
427
428
27
    return tablet_stream->append_data(header, data);
429
27
}
430
431
void IndexStream::_init_tablet_stream(TabletStreamSharedPtr& tablet_stream, int64_t tablet_id,
432
15
                                      int64_t partition_id) {
433
15
    tablet_stream = std::make_shared<TabletStream>(_load_id, tablet_id, _txn_id, _load_stream_mgr,
434
15
                                                   _profile);
435
15
    _tablet_streams_map[tablet_id] = tablet_stream;
436
15
    auto st = tablet_stream->init(_schema, _id, partition_id);
437
15
    if (!st.ok()) {
438
1
        LOG(WARNING) << "tablet stream init failed " << *tablet_stream;
439
1
    }
440
15
}
441
442
0
void IndexStream::get_all_write_tablet_ids(std::vector<int64_t>* tablet_ids) {
443
0
    std::lock_guard lock_guard(_lock);
444
0
    for (const auto& [tablet_id, _] : _tablet_streams_map) {
445
0
        tablet_ids->push_back(tablet_id);
446
0
    }
447
0
}
448
449
void IndexStream::close(const std::vector<PTabletID>& tablets_to_commit,
450
30
                        std::vector<int64_t>* success_tablet_ids, FailedTablets* failed_tablets) {
451
30
    std::lock_guard lock_guard(_lock);
452
30
    SCOPED_TIMER(_close_wait_timer);
453
    // open all need commit tablets
454
40
    for (const auto& tablet : tablets_to_commit) {
455
40
        if (_id != tablet.index_id()) {
456
21
            continue;
457
21
        }
458
19
        TabletStreamSharedPtr tablet_stream;
459
19
        auto it = _tablet_streams_map.find(tablet.tablet_id());
460
19
        if (it == _tablet_streams_map.end()) {
461
2
            _init_tablet_stream(tablet_stream, tablet.tablet_id(), tablet.partition_id());
462
17
        } else {
463
17
            tablet_stream = it->second;
464
17
        }
465
19
        if (tablet.has_num_segments()) {
466
16
            tablet_stream->add_num_segments(tablet.num_segments());
467
16
        } else {
468
            // for compatibility reasons (sink from old version BE)
469
3
            tablet_stream->disable_num_segments_check();
470
3
        }
471
19
    }
472
473
30
    for (auto& [_, tablet_stream] : _tablet_streams_map) {
474
15
        tablet_stream->pre_close();
475
15
    }
476
477
30
    for (auto& [_, tablet_stream] : _tablet_streams_map) {
478
15
        auto st = tablet_stream->close();
479
15
        if (st.ok()) {
480
9
            success_tablet_ids->push_back(tablet_stream->id());
481
9
        } else {
482
6
            LOG(INFO) << "close tablet stream " << *tablet_stream << ", status=" << st;
483
6
            failed_tablets->emplace_back(tablet_stream->id(), st);
484
6
        }
485
15
    }
486
30
}
487
488
// TODO: Profile is temporary disabled, because:
489
// 1. It's not being processed by the upstream for now
490
// 2. There are some problems in _profile->to_thrift()
491
LoadStream::LoadStream(const PUniqueId& load_id, LoadStreamMgr* load_stream_mgr,
492
                       bool enable_profile)
493
15
        : _load_id(load_id), _enable_profile(false), _load_stream_mgr(load_stream_mgr) {
494
15
    g_load_stream_cnt << 1;
495
15
    _profile = std::make_unique<RuntimeProfile>("LoadStream");
496
15
    _append_data_timer = ADD_TIMER(_profile, "AppendDataTime");
497
15
    _close_wait_timer = ADD_TIMER(_profile, "CloseWaitTime");
498
15
    TUniqueId load_tid = ((UniqueId)load_id).to_thrift();
499
#ifndef BE_TEST
500
    std::shared_ptr<QueryContext> query_context =
501
            ExecEnv::GetInstance()->fragment_mgr()->get_query_ctx(load_tid);
502
    if (query_context != nullptr) {
503
        _resource_ctx = query_context->resource_ctx();
504
    } else {
505
        _resource_ctx = ResourceContext::create_shared();
506
        _resource_ctx->task_controller()->set_task_id(load_tid);
507
        std::shared_ptr<MemTrackerLimiter> mem_tracker = MemTrackerLimiter::create_shared(
508
                MemTrackerLimiter::Type::LOAD,
509
                fmt::format("(FromLoadStream)Load#Id={}", ((UniqueId)load_id).to_string()));
510
        _resource_ctx->memory_context()->set_mem_tracker(mem_tracker);
511
    }
512
#else
513
15
    _resource_ctx = ResourceContext::create_shared();
514
15
    _resource_ctx->task_controller()->set_task_id(load_tid);
515
15
    std::shared_ptr<MemTrackerLimiter> mem_tracker = MemTrackerLimiter::create_shared(
516
15
            MemTrackerLimiter::Type::LOAD,
517
15
            fmt::format("(FromLoadStream)Load#Id={}", ((UniqueId)load_id).to_string()));
518
15
    _resource_ctx->memory_context()->set_mem_tracker(mem_tracker);
519
15
#endif
520
15
}
521
522
15
LoadStream::~LoadStream() {
523
15
    g_load_stream_cnt << -1;
524
15
    LOG(INFO) << "load stream is deconstructed " << *this;
525
15
}
526
527
15
Status LoadStream::init(const POpenLoadStreamRequest* request) {
528
15
    _txn_id = request->txn_id();
529
15
    _total_streams = static_cast<int32_t>(request->total_streams());
530
15
    _is_incremental = (_total_streams == 0);
531
532
15
    _schema = std::make_shared<OlapTableSchemaParam>();
533
15
    RETURN_IF_ERROR(_schema->init(request->schema()));
534
30
    for (auto& index : request->schema().indexes()) {
535
30
        _index_streams_map[index.id()] = std::make_shared<IndexStream>(
536
30
                _load_id, index.id(), _txn_id, _schema, _load_stream_mgr, _profile.get());
537
30
    }
538
15
    LOG(INFO) << "succeed to init load stream " << *this;
539
15
    return Status::OK();
540
15
}
541
542
bool LoadStream::close(int64_t src_id, const std::vector<PTabletID>& tablets_to_commit,
543
19
                       std::vector<int64_t>* success_tablet_ids, FailedTablets* failed_tablets) {
544
19
    std::lock_guard<bthread::Mutex> lock_guard(_lock);
545
19
    SCOPED_TIMER(_close_wait_timer);
546
547
    // we do nothing until recv CLOSE_LOAD from all stream to ensure all data are handled before ack
548
19
    _open_streams[src_id]--;
549
19
    if (_open_streams[src_id] == 0) {
550
19
        _open_streams.erase(src_id);
551
19
    }
552
19
    _close_load_cnt++;
553
19
    LOG(INFO) << "received CLOSE_LOAD from sender " << src_id << ", remaining "
554
19
              << _total_streams - _close_load_cnt << " senders, " << *this;
555
556
19
    _tablets_to_commit.insert(_tablets_to_commit.end(), tablets_to_commit.begin(),
557
19
                              tablets_to_commit.end());
558
559
19
    if (_close_load_cnt < _total_streams) {
560
        // do not return commit info if there is remaining streams.
561
4
        return false;
562
4
    }
563
564
30
    for (auto& [_, index_stream] : _index_streams_map) {
565
30
        index_stream->close(_tablets_to_commit, success_tablet_ids, failed_tablets);
566
30
    }
567
15
    LOG(INFO) << "close load " << *this << ", success_tablet_num=" << success_tablet_ids->size()
568
15
              << ", failed_tablet_num=" << failed_tablets->size();
569
15
    return true;
570
19
}
571
572
19
std::vector<int64_t> LoadStream::mark_eos_sent_and_collect(int64_t stream_id, bool is_incremental) {
573
19
    std::lock_guard<bthread::Mutex> lock_guard(_lock);
574
19
    std::vector<int64_t> to_close;
575
    // A non-incremental stream is closed as soon as its own CLOSE_LOAD (and EOS)
576
    // is handled -- this is the first batch of streams, known up front, not subject
577
    // to fencing. Closing it promptly also means a duplicate/late CLOSE_LOAD lands
578
    // on an already-closed stream and is dropped, instead of being counted again.
579
    // An incremental stream must be deferred (fencing #56120: it may only close once
580
    // every non-incremental stream is closed), so it is parked in _eos_sent_stream_ids
581
    // until all CLOSE_LOADs have been received.
582
19
    if (is_incremental) {
583
        // Parked only after the caller sent this stream's EOS via _report_result,
584
        // so every parked id is safe to close.
585
3
        _eos_sent_stream_ids.push_back(stream_id);
586
16
    } else {
587
16
        to_close.push_back(stream_id);
588
16
    }
589
    // `_close_load_cnt == _total_streams` means every CLOSE_LOAD has been counted by
590
    // close(). Latch it so that any thread reaching here afterwards also drains the
591
    // parked incremental streams, guaranteeing none is left un-closed regardless of
592
    // thread interleaving (fixes the split-lock leak race).
593
19
    if (_close_load_cnt >= _total_streams) {
594
17
        _all_close_load_received = true;
595
17
    }
596
19
    if (_all_close_load_received) {
597
17
        for (const auto& parked_id : _eos_sent_stream_ids) {
598
3
            to_close.push_back(parked_id);
599
3
        }
600
17
        _eos_sent_stream_ids.clear();
601
17
    }
602
19
    return to_close;
603
19
}
604
605
void LoadStream::_report_result(StreamId stream, const Status& status,
606
                                const std::vector<int64_t>& success_tablet_ids,
607
39
                                const FailedTablets& failed_tablets, bool eos) {
608
39
    LOG(INFO) << "report result " << *this << ", success tablet num " << success_tablet_ids.size()
609
39
              << ", failed tablet num " << failed_tablets.size();
610
39
    butil::IOBuf buf;
611
39
    PLoadStreamResponse response;
612
39
    response.set_eos(eos);
613
39
    status.to_protobuf(response.mutable_status());
614
39
    for (auto& id : success_tablet_ids) {
615
9
        response.add_success_tablet_ids(id);
616
9
    }
617
39
    for (auto& [id, st] : failed_tablets) {
618
10
        auto pb = response.add_failed_tablets();
619
10
        pb->set_id(id);
620
10
        st.to_protobuf(pb->mutable_status());
621
10
    }
622
623
39
    if (_enable_profile && _close_load_cnt == _total_streams) {
624
0
        TRuntimeProfileTree tprofile;
625
0
        ThriftSerializer ser(false, 4096);
626
0
        uint8_t* profile_buf = nullptr;
627
0
        uint32_t len = 0;
628
0
        std::unique_lock<bthread::Mutex> l(_lock);
629
630
0
        _profile->to_thrift(&tprofile);
631
0
        auto st = ser.serialize(&tprofile, &len, &profile_buf);
632
0
        if (st.ok()) {
633
0
            response.set_load_stream_profile(profile_buf, len);
634
0
        } else {
635
0
            LOG(WARNING) << "TRuntimeProfileTree serialize failed, errmsg=" << st << ", " << *this;
636
0
        }
637
0
    }
638
639
39
    buf.append(response.SerializeAsString());
640
39
    auto wst = _write_stream(stream, buf);
641
39
    if (!wst.ok()) {
642
0
        LOG(WARNING) << " report result failed with " << wst << ", " << *this;
643
0
    }
644
39
}
645
646
0
void LoadStream::_report_schema(StreamId stream, const PStreamHeader& hdr) {
647
0
    butil::IOBuf buf;
648
0
    PLoadStreamResponse response;
649
0
    Status st = Status::OK();
650
0
    for (const auto& req : hdr.tablets()) {
651
0
        BaseTabletSPtr tablet;
652
0
        if (auto res = ExecEnv::get_tablet(req.tablet_id()); res.has_value()) {
653
0
            tablet = std::move(res).value();
654
0
        } else {
655
0
            st = std::move(res).error();
656
0
            break;
657
0
        }
658
0
        auto* resp = response.add_tablet_schemas();
659
0
        resp->set_index_id(req.index_id());
660
0
        resp->set_enable_unique_key_merge_on_write(tablet->enable_unique_key_merge_on_write());
661
0
        tablet->tablet_schema()->to_schema_pb(resp->mutable_tablet_schema());
662
0
    }
663
0
    st.to_protobuf(response.mutable_status());
664
665
0
    buf.append(response.SerializeAsString());
666
0
    auto wst = _write_stream(stream, buf);
667
0
    if (!wst.ok()) {
668
0
        LOG(WARNING) << " report result failed with " << wst << ", " << *this;
669
0
    }
670
0
}
671
672
0
void LoadStream::_report_tablet_load_info(StreamId stream, int64_t index_id) {
673
0
    std::vector<int64_t> write_tablet_ids;
674
0
    auto it = _index_streams_map.find(index_id);
675
0
    if (it != _index_streams_map.end()) {
676
0
        it->second->get_all_write_tablet_ids(&write_tablet_ids);
677
0
    }
678
679
0
    if (!write_tablet_ids.empty()) {
680
0
        butil::IOBuf buf;
681
0
        PLoadStreamResponse response;
682
0
        auto* tablet_load_infos = response.mutable_tablet_load_rowset_num_infos();
683
0
        _collect_tablet_load_info_from_tablets(write_tablet_ids, tablet_load_infos);
684
0
        if (tablet_load_infos->empty()) {
685
0
            return;
686
0
        }
687
0
        buf.append(response.SerializeAsString());
688
0
        auto wst = _write_stream(stream, buf);
689
0
        if (!wst.ok()) {
690
0
            LOG(WARNING) << "report tablet load info failed with " << wst << ", " << *this;
691
0
        }
692
0
    }
693
0
}
694
695
void LoadStream::_collect_tablet_load_info_from_tablets(
696
        const std::vector<int64_t>& tablet_ids,
697
0
        google::protobuf::RepeatedPtrField<PTabletLoadRowsetInfo>* tablet_load_infos) {
698
0
    for (auto tablet_id : tablet_ids) {
699
0
        BaseTabletSPtr tablet;
700
0
        if (auto res = ExecEnv::get_tablet(tablet_id); res.has_value()) {
701
0
            tablet = std::move(res).value();
702
0
        } else {
703
0
            continue;
704
0
        }
705
0
        BaseDeltaWriter::collect_tablet_load_rowset_num_info(tablet.get(), tablet_load_infos);
706
0
    }
707
0
}
708
709
39
Status LoadStream::_write_stream(StreamId stream, butil::IOBuf& buf) {
710
39
    for (;;) {
711
39
        int ret = 0;
712
39
        DBUG_EXECUTE_IF("LoadStream._write_stream.EAGAIN", { ret = EAGAIN; });
713
39
        if (ret == 0) {
714
39
            ret = brpc::StreamWrite(stream, buf);
715
39
        }
716
39
        switch (ret) {
717
39
        case 0:
718
39
            return Status::OK();
719
0
        case EAGAIN: {
720
0
            const timespec time = butil::seconds_from_now(config::load_stream_eagain_wait_seconds);
721
0
            int wait_ret = brpc::StreamWait(stream, &time);
722
0
            if (wait_ret != 0) {
723
0
                return Status::InternalError("StreamWait failed, err={}", wait_ret);
724
0
            }
725
0
            break;
726
0
        }
727
0
        default:
728
0
            return Status::InternalError("StreamWrite failed, err={}", ret);
729
39
        }
730
39
    }
731
0
    return Status::OK();
732
39
}
733
734
65
void LoadStream::_parse_header(butil::IOBuf* const message, PStreamHeader& hdr) {
735
65
    butil::IOBufAsZeroCopyInputStream wrapper(*message);
736
65
    hdr.ParseFromZeroCopyStream(&wrapper);
737
65
    VLOG_DEBUG << "header parse result: " << hdr.DebugString();
738
65
}
739
740
28
Status LoadStream::_append_data(const PStreamHeader& header, butil::IOBuf* data) {
741
28
    SCOPED_TIMER(_append_data_timer);
742
28
    IndexStreamSharedPtr index_stream;
743
744
28
    int64_t index_id = header.index_id();
745
28
    DBUG_EXECUTE_IF("TabletStream._append_data.unknown_indexid",
746
28
                    { index_id = UNKNOWN_ID_FOR_TEST; });
747
28
    auto it = _index_streams_map.find(index_id);
748
28
    if (it == _index_streams_map.end()) {
749
1
        return Status::Error<ErrorCode::INVALID_ARGUMENT>("unknown index_id {}", index_id);
750
27
    } else {
751
27
        index_stream = it->second;
752
27
    }
753
754
27
    return index_stream->append_data(header, data);
755
28
}
756
757
52
int LoadStream::on_received_messages(StreamId id, butil::IOBuf* const messages[], size_t size) {
758
52
    VLOG_DEBUG << "on_received_messages " << id << " " << size;
759
117
    for (size_t i = 0; i < size; ++i) {
760
130
        while (messages[i]->size() > 0) {
761
            // step 1: parse header
762
65
            size_t hdr_len = 0;
763
65
            messages[i]->cutn((void*)&hdr_len, sizeof(size_t));
764
65
            butil::IOBuf hdr_buf;
765
65
            PStreamHeader hdr;
766
65
            messages[i]->cutn(&hdr_buf, hdr_len);
767
65
            _parse_header(&hdr_buf, hdr);
768
769
            // step 2: cut data
770
65
            size_t data_len = 0;
771
65
            messages[i]->cutn((void*)&data_len, sizeof(size_t));
772
65
            butil::IOBuf data_buf;
773
65
            PStreamHeader data;
774
65
            messages[i]->cutn(&data_buf, data_len);
775
776
            // step 3: dispatch
777
65
            _dispatch(id, hdr, &data_buf);
778
65
        }
779
65
    }
780
52
    return 0;
781
52
}
782
783
65
void LoadStream::_dispatch(StreamId id, const PStreamHeader& hdr, butil::IOBuf* data) {
784
65
    VLOG_DEBUG << PStreamHeader_Opcode_Name(hdr.opcode()) << " from " << hdr.src_id()
785
0
               << " with tablet " << hdr.tablet_id();
786
65
    SCOPED_ATTACH_TASK(_resource_ctx);
787
    // CLOSE_LOAD message should not be fault injected,
788
    // otherwise the message will be ignored and causing close wait timeout
789
65
    if (hdr.opcode() != PStreamHeader::CLOSE_LOAD) {
790
30
        DBUG_EXECUTE_IF("LoadStream._dispatch.unknown_loadid", {
791
30
            PStreamHeader& t_hdr = const_cast<PStreamHeader&>(hdr);
792
30
            PUniqueId* load_id = t_hdr.mutable_load_id();
793
30
            load_id->set_hi(UNKNOWN_ID_FOR_TEST);
794
30
            load_id->set_lo(UNKNOWN_ID_FOR_TEST);
795
30
        });
796
30
        DBUG_EXECUTE_IF("LoadStream._dispatch.unknown_srcid", {
797
30
            PStreamHeader& t_hdr = const_cast<PStreamHeader&>(hdr);
798
30
            t_hdr.set_src_id(UNKNOWN_ID_FOR_TEST);
799
30
        });
800
30
    }
801
65
    if (UniqueId(hdr.load_id()) != UniqueId(_load_id)) {
802
1
        Status st = Status::Error<ErrorCode::INVALID_ARGUMENT>(
803
1
                "invalid load id {}, expected {}", print_id(hdr.load_id()), print_id(_load_id));
804
1
        _report_failure(id, st, hdr);
805
1
        return;
806
1
    }
807
808
64
    {
809
64
        std::lock_guard lock_guard(_lock);
810
64
        if (!_open_streams.contains(hdr.src_id())) {
811
17
            Status st = Status::Error<ErrorCode::INVALID_ARGUMENT>("no open stream from source {}",
812
17
                                                                   hdr.src_id());
813
17
            _report_failure(id, st, hdr);
814
17
            return;
815
17
        }
816
64
    }
817
818
47
    switch (hdr.opcode()) {
819
0
    case PStreamHeader::ADD_SEGMENT: {
820
0
        auto st = _append_data(hdr, data);
821
0
        if (!st.ok()) {
822
0
            _report_failure(id, st, hdr);
823
0
        } else {
824
            // Report tablet load info only on ADD_SEGMENT to reduce frequency.
825
            // ADD_SEGMENT is sent once per segment, while APPEND_DATA is sent
826
            // for every data batch. This reduces unnecessary writes and avoids
827
            // potential stream write failures when the sender is closing.
828
0
            _report_tablet_load_info(id, hdr.index_id());
829
0
        }
830
0
    } break;
831
28
    case PStreamHeader::APPEND_DATA: {
832
28
        auto st = _append_data(hdr, data);
833
28
        if (!st.ok()) {
834
2
            _report_failure(id, st, hdr);
835
2
        }
836
28
    } break;
837
19
    case PStreamHeader::CLOSE_LOAD: {
838
19
        DBUG_EXECUTE_IF("LoadStream.close_load.block", DBUG_BLOCK);
839
19
        std::vector<int64_t> success_tablet_ids;
840
19
        FailedTablets failed_tablets;
841
19
        std::vector<PTabletID> tablets_to_commit(hdr.tablets().begin(), hdr.tablets().end());
842
        // Step 1: count this CLOSE_LOAD and, if this is the last one, commit. Under _lock.
843
19
        bool all_received =
844
19
                close(hdr.src_id(), tablets_to_commit, &success_tablet_ids, &failed_tablets);
845
        // Step 2: send THIS stream's EOS (network IO, must be outside _lock). A stream
846
        // must not be StreamClose'd before its own EOS is delivered, otherwise the
847
        // sender sees on_closed without EOS and reports "Stream closed without EOS".
848
19
        _report_result(id, Status::OK(), success_tablet_ids, failed_tablets, true);
849
19
        bool is_incremental =
850
19
                hdr.has_num_incremental_streams() && hdr.num_incremental_streams() > 0;
851
        // Test-only: delay every incremental stream except the one that made
852
        // all_received, so a non-last incremental stream parks after the last
853
        // stream drained the list. On the buggy code this orphans it and the
854
        // load hangs; on the fix the latch drains the late registration under
855
        // the same lock. Inert unless enable_debug_points=true.
856
19
        if (is_incremental && !all_received) {
857
2
            DBUG_EXECUTE_IF("LoadStream.close_load.delay_incremental_register",
858
2
                            { bthread_usleep(3000000); });
859
2
        }
860
        // Step 3: close this stream (non-incremental) or park it for deferred close
861
        // (incremental, fencing), then collect everything that is now safe to close.
862
        // Registration happens only after step 2, so a collected stream already had
863
        // its EOS delivered (fixes the close-before-EOS race); the all-received latch
864
        // inside makes any late thread drain the parked streams (fixes the leak race).
865
19
        auto streams_to_close = mark_eos_sent_and_collect(id, is_incremental);
866
19
        for (auto& closing_id : streams_to_close) {
867
19
            brpc::StreamClose(closing_id);
868
19
        }
869
19
    } break;
870
0
    case PStreamHeader::GET_SCHEMA: {
871
0
        _report_schema(id, hdr);
872
0
    } break;
873
0
    default:
874
0
        LOG(WARNING) << "unexpected stream message " << hdr.opcode() << ", " << *this;
875
0
        DCHECK(false);
876
47
    }
877
47
}
878
879
0
void LoadStream::on_idle_timeout(StreamId id) {
880
0
    LOG(WARNING) << "closing load stream on idle timeout, " << *this;
881
0
    brpc::StreamClose(id);
882
0
}
883
884
19
void LoadStream::on_closed(StreamId id) {
885
    // `this` may be freed by other threads after increasing `_close_rpc_cnt`,
886
    // format string first to prevent use-after-free
887
19
    std::stringstream ss;
888
19
    ss << *this;
889
19
    auto remaining_streams = _total_streams - _close_rpc_cnt.fetch_add(1) - 1;
890
19
    LOG(INFO) << "stream " << id << " on_closed, remaining streams = " << remaining_streams << ", "
891
19
              << ss.str();
892
19
    if (remaining_streams == 0) {
893
15
        _load_stream_mgr->clear_load(_load_id);
894
15
    }
895
19
}
896
897
122
inline std::ostream& operator<<(std::ostream& ostr, const LoadStream& load_stream) {
898
122
    ostr << "load_id=" << print_id(load_stream._load_id) << ", txn_id=" << load_stream._txn_id;
899
122
    return ostr;
900
122
}
901
902
} // namespace doris