Coverage Report

Created: 2026-10-26 18:20

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