Line data Source code
1 : /**
2 : * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3 : * This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4 : * CANN Open Software License Agreement Version 2.0 (the "License").
5 : * Please refer to the License for details. You may not use this file except in compliance with the License.
6 : * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7 : * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8 : * See LICENSE in the root of the software repository for the full text of the License.
9 : */
10 :
11 : #include "rank_info_detect_service.h"
12 :
13 : #include <stdio.h>
14 : #include "rank_info_dispatcher.h"
15 : #include "env_config/env_config_v2.h"
16 : #include "host_buffer.h"
17 : #include "root_handle_v2.h"
18 : #include "hccp_peer_manager.h"
19 : #include "orion_adapter_rts.h"
20 : #include "preempt_port_manager_v2.h"
21 : #include "host_socket_handle_manager.h"
22 : #include "adapter_error_manager_pub.h"
23 :
24 : namespace Hccl {
25 :
26 : const u32 DISPLAY_RANKNUM_PERLINE = 8;
27 : const u32 SOCKET_ACCEPT_TIMEOUT = 60; // Server调用Accept等待的最大超时时间 60s
28 : const u32 SOCKET_PRINT_COUNT = 3; // 未建链打印的数量
29 : const u32 MAX_AGENT_BUF_SIZE = 256;
30 :
31 0 : void RankInfoDetectService::Setup()
32 : {
33 : // 1. 连接所有rank
34 0 : GetConnections();
35 :
36 : // 2. 接收所有rank发来的localRankTable并整合为全局RankTable
37 0 : GetRankTable();
38 :
39 : // 3. 将完整RankTable广播给所有rank
40 0 : BroadcastRankTable();
41 0 : }
42 :
43 5 : void RankInfoDetectService::GetConnections()
44 : {
45 5 : HCCL_INFO("[RankInfoDetectService::%s] start.", __func__);
46 :
47 : // 超时参数
48 5 : auto startTime = std::chrono::steady_clock::now();
49 5 : auto timeout = std::chrono::seconds(EnvConfig::GetInstance().GetSocketConfig().GetLinkTimeOut());
50 5 : bool isFirstAcceptTimeOut = false;
51 :
52 : // 期望等待连接的rank数量
53 5 : u32 expectedSocketNum = 1;
54 :
55 : // 首个connect获取到的rankSize
56 5 : u32 previousRankNum = 0;
57 :
58 : // 获取server端socket信息
59 5 : u32 hostPort = serverSocket_->GetListenPort();
60 5 : auto hccpHostSocketHandle = HostSocketHandleManager::GetInstance().Get(devPhyId_, hostIp_);
61 5 : CHK_PRT_THROW(
62 : hccpHostSocketHandle == nullptr,
63 : HCCL_ERROR("[RankInfoDetectService::%s] Get hccpHostSocketHandle fail.", __func__), InternalException,
64 : "get socket handle error");
65 5 : std::string connSocketTag = RANK_INFO_DETECT_TAG + "_" + identifier_ + "_" + std::to_string(hostPort);
66 5 : SocketStatus status = SocketStatus::INVALID;
67 :
68 : // 连接rankSize个client
69 22 : while (expectedSocketNum > 0) {
70 22 : auto topoExUsedTime = std::chrono::steady_clock::now() - startTime;
71 22 : if (topoExUsedTime >= timeout) {
72 14 : RPT_INPUT_ERR(
73 : true, "EI0015", std::vector<std::string>({"error_reason"}),
74 : std::vector<std::string>({StringFormat(
75 : "Receiving message from the root node timed out. "
76 : "Timeout was set to %lld seconds. expected %u nodes, received %u nodes. "
77 : "Check whether worker nodes are reachable and report errors.",
78 : static_cast<long long>(timeout.count()), expectedSocketNum + previousRankNum, previousRankNum)}));
79 2 : HCCL_ERROR("[RankInfoDetectService::%s] server get sockets timeout[%lld s]", __func__, timeout);
80 5 : break;
81 : }
82 :
83 : // duration_cast<seconds> 会进行向下取整,不足 1s
84 : // 时提前跳出,确保建链超时场景,server端在client端退出前发送临终遗言
85 20 : auto topoExResTime = timeout - topoExUsedTime;
86 20 : u32 topoExRes_i = std::chrono::duration_cast<std::chrono::seconds>(topoExResTime).count();
87 20 : if (topoExRes_i == 0) {
88 28 : RPT_INPUT_ERR(
89 : true, "EI0016", std::vector<std::string>({"value", "variable", "expect"}),
90 : std::vector<std::string>(
91 : {std::to_string(topoExRes_i), "waiting time for rank connections",
92 : "at least 1 second to establish rank connections"}));
93 2 : HCCL_ERROR(
94 : "[RankInfoDetectService::%s] timeout[%lld s] is exhausted, expected[%u], received[%u]", __func__,
95 : timeout, expectedSocketNum + previousRankNum, previousRankNum);
96 2 : break;
97 : }
98 : std::shared_ptr<Socket> connSocket = std::make_shared<Socket>(
99 0 : hccpHostSocketHandle, hostIp_, hostPort, hostIp_, connSocketTag, SocketRole::SERVER,
100 18 : NicType::HOST_NIC_TYPE);
101 : // GetStatus 是阻塞接口,传入剩余时间作为超时上限,避免其内部超时导致外层循环超时处理失效
102 18 : EXCEPTION_CATCH(status = connSocket->GetStatus(topoExRes_i), {
103 : // 非本端client首次连接异常,直接重试
104 : if (status == SocketStatus::OK) {
105 : status = SocketStatus::CONNECTING;
106 : }
107 : HCCL_ERROR("[RankInfoDetectService::%s] server get socket fail", __func__);
108 : });
109 18 : if (status == SocketStatus::OK) {
110 6 : if (!RecvAndVerifyRemoteAgentIdAndRankSize(connSocket, expectedSocketNum, previousRankNum)) {
111 1 : break;
112 : }
113 5 : expectedSocketNum--;
114 5 : isFirstAcceptTimeOut = false;
115 5 : HCCL_INFO("[RankInfoDetectService::%s] socket[%s] connect ok.", __func__, connSocket->Describe().c_str());
116 12 : } else if (status == SocketStatus::CONNECTING) {
117 7 : SaluSleep(ONE_MILLISECOND_OF_USLEEP);
118 5 : } else if (status == SocketStatus::TIMEOUT) {
119 : // 避免重复打印
120 5 : if (isFirstAcceptTimeOut) {
121 3 : continue;
122 : }
123 2 : HCCL_ERROR(
124 : "[RankInfoDetectService::%s] rank info detect server get socket timeout[%lld s]", __func__, timeout);
125 2 : DisplayConnectingStatus(previousRankNum, expectedSocketNum);
126 2 : isFirstAcceptTimeOut = true;
127 : } else {
128 0 : HCCL_ERROR("[RankInfoDetectService::%s] SocketStatus[%s] error", __func__, status.Describe().c_str());
129 0 : break;
130 : }
131 18 : }
132 :
133 : // 如果没有连接成功的rank则退出
134 9 : CHK_PRT_THROW(
135 : connSockets_.size() == 0, HCCL_ERROR("[RankInfoDetectService::%s] no rank connection success.", __func__),
136 : InternalException, "no rank connection success");
137 :
138 : // 处理异常流程
139 3 : if (expectedSocketNum > 0) {
140 : // 将建立连接超时的client信息添加到failedAgentIdList_
141 3 : FailedConnectionAgentIdString(previousRankNum);
142 3 : DisplayConnectedRanks();
143 3 : HCCL_INFO("[RankInfoDetectService::%s] end, there exist non-connected ranks.", __func__);
144 : } else {
145 0 : HCCL_INFO("[RankInfoDetectService::%s] end, all agentId get connection socket success.", __func__);
146 : }
147 11 : }
148 :
149 1 : void RankInfoDetectService::GetRankTable()
150 : {
151 1 : HCCL_INFO("[RankInfoDetectService::%s] start.", __func__);
152 :
153 : // 接收localRankTable并组全局RankTableInfo
154 1 : rankTable_ = RankTableInfo{};
155 2 : for (auto& iter : connSockets_) {
156 1 : vector<char> rankInfoMsg{};
157 1 : SocketAgent socketAgent(iter.second.get());
158 1 : RecvRankInfoMsg(socketAgent, rankInfoMsg);
159 1 : ParseRankTable(rankInfoMsg);
160 1 : }
161 :
162 : // 按照rankid排序
163 1 : SortRankTable();
164 :
165 : // 更新当前阶段
166 1 : currentStep_++;
167 :
168 1 : HCCL_INFO("[RankInfoDetectService::%s] end.", __func__);
169 1 : }
170 :
171 2 : void RankInfoDetectService::BroadcastRankTable()
172 : {
173 2 : HCCL_INFO("[RankInfoDetectService::%s] start.", __func__);
174 :
175 : // 广播全局ranktable
176 2 : std::shared_ptr<RankInfoDispather> dispatcher = std::make_shared<RankInfoDispather>(this);
177 2 : dispatcher->BroadcastRankTable(connSockets_, rankTable_, failedAgentIdList_, currentStep_);
178 :
179 2 : HCCL_INFO("[RankInfoDetectService::%s] end.", __func__);
180 2 : }
181 :
182 31 : void RankInfoDetectService::Disconnect()
183 : {
184 40 : for (auto iter = connSockets_.begin(); iter != connSockets_.end();) {
185 9 : iter->second.get()->Close();
186 9 : iter = connSockets_.erase(iter);
187 : }
188 31 : }
189 :
190 2 : bool RankInfoDetectService::RecvRemoteAgentId(SocketAgent& connSocketAgent, std::string& agentId)
191 : {
192 2 : HCCL_INFO("[RankInfoDetectService::%s] start.", __func__);
193 :
194 : // 接收消息
195 2 : u64 revMsgLen = 0;
196 2 : char msg[MAX_AGENT_BUF_SIZE] = {0};
197 2 : bool ret = connSocketAgent.RecvMsg(msg, revMsgLen);
198 2 : CHK_PRT_RET(
199 : !ret || revMsgLen >= MAX_AGENT_BUF_SIZE,
200 : HCCL_ERROR("[RankInfoDetectService::%s] recv error, revMsgLen[%llu].", __func__, revMsgLen), false);
201 :
202 : // 解析agentId
203 2 : msg[revMsgLen] = '\0';
204 2 : agentId = msg;
205 :
206 2 : HCCL_INFO("[RankInfoDetectService::%s] agentId[%s]", __func__, agentId.c_str());
207 2 : return true;
208 : }
209 :
210 1 : bool RankInfoDetectService::RecvRemoteRankSize(SocketAgent& connSocketAgent, u32& rankSize)
211 : {
212 1 : HCCL_INFO("[RankInfoDetectService::%s] start.", __func__);
213 :
214 : // 接收rankSize
215 1 : u64 revMsgLen = 0;
216 1 : bool ret = connSocketAgent.RecvMsg(&rankSize, revMsgLen);
217 1 : CHK_PRT_RET(
218 : !ret, HCCL_ERROR("[RankInfoDetectService::%s] RecvMsg fail, revMsgLen[%llu].", __func__, revMsgLen), false);
219 :
220 1 : HCCL_INFO("[RankInfoDetectService::%s] rankSize[%u]", __func__, rankSize);
221 1 : return true;
222 : }
223 :
224 : // 接收客户端发送的字节流形式的rankinfo消息
225 1 : void RankInfoDetectService::RecvRankInfoMsg(SocketAgent& connSocketAgent, vector<char>& rankInfoMsg)
226 : {
227 1 : HCCL_INFO("[RankInfoDetectService::%s] start.", __func__);
228 :
229 1 : u64 revMsgLen = 0;
230 1 : std::unique_ptr<HostBuffer> msg = std::make_unique<HostBuffer>(MAX_BUFFER_LEN);
231 1 : char* msgAddr = reinterpret_cast<char*>(msg->GetAddr());
232 1 : CHK_PRT_THROW(
233 : !connSocketAgent.RecvMsg(msgAddr, revMsgLen),
234 : HCCL_ERROR("[RankInfoDetectService::%s] RecvMsg fail, revMsgLen[%llu]", __func__, revMsgLen),
235 : InvalidParamsException, "RecvMsg fail");
236 :
237 : // 以vector<char>格式保存
238 1 : rankInfoMsg.resize(revMsgLen);
239 1 : rankInfoMsg.assign(msgAddr, msgAddr + revMsgLen);
240 :
241 1 : HCCL_INFO("[RankInfoDetectService::%s] end.", __func__);
242 1 : }
243 :
244 : // 解析接收到的rank table信息
245 2 : void RankInfoDetectService::ParseRankTable(vector<char>& rankInfoMsg)
246 : {
247 2 : HCCL_INFO("[RankInfoDetectService::%s] start.", __func__);
248 :
249 : // 消息格式: [ranktable数据(n字节)][step(4字节)]
250 2 : BinaryStream binStream(rankInfoMsg);
251 :
252 : // 解析localRankInfo
253 2 : RankTableInfo localRankInfo(binStream);
254 2 : localRankInfo.Dump();
255 :
256 : // 解析step
257 : u32 receivedStep;
258 2 : binStream >> receivedStep;
259 :
260 : // 校验step是否匹配
261 2 : CHK_PRT_THROW(
262 : receivedStep != currentStep_,
263 : HCCL_ERROR(
264 : "[RankInfoDetectService::%s] Step mismatch: received %u, expected %u", __func__, receivedStep,
265 : currentStep_),
266 : InvalidParamsException, "Step mismatch");
267 :
268 : // 添加到rankTable_
269 2 : rankTable_.UpdateRankTable(localRankInfo);
270 :
271 2 : HCCL_INFO("[RankInfoDetectService::%s] end.", __func__);
272 2 : }
273 :
274 0 : bool RankIdCompare(const NewRankInfo& i, const NewRankInfo& j) { return (i.rankId < j.rankId); }
275 :
276 1 : void RankInfoDetectService::SortRankTable()
277 : {
278 1 : std::sort(rankTable_.ranks.begin(), rankTable_.ranks.end(), RankIdCompare);
279 1 : }
280 :
281 3 : void RankInfoDetectService::FailedConnectionAgentIdString(u32 rankSize)
282 : {
283 3 : HCCL_INFO("[RankInfoDetectService::%s] start.", __func__);
284 :
285 3 : std::vector<bool> connectedRank(rankSize, false);
286 7 : for (auto it : connSockets_) {
287 5 : u32 rankid = 0;
288 5 : HcclResult ret = SalStrToULong(it.first, HCCL_BASE_DECIMAL, rankid);
289 5 : CHK_PRT_RET(
290 : ret != HCCL_SUCCESS,
291 : HCCL_ERROR("[RankInfoDetectService::%s] agentId[%s] strToULong fail.", __func__, it.first.c_str()), );
292 4 : CHK_PRT_RET(
293 : rankid >= rankSize,
294 : HCCL_ERROR("[RankInfoDetectService::%s] invalid rank id[%u], rankSize[%u].", __func__, rankid, rankSize), );
295 4 : connectedRank[rankid] = true;
296 5 : }
297 :
298 10 : for (u32 i = 0; i < rankSize; i++) {
299 8 : if (!connectedRank[i]) {
300 4 : if (!failedAgentIdList_.empty()) {
301 2 : failedAgentIdList_ += ',';
302 : }
303 4 : failedAgentIdList_ += std::to_string(i);
304 : }
305 : }
306 :
307 2 : HCCL_INFO("[RankInfoDetectService::%s] end.", __func__);
308 3 : }
309 :
310 : // 校验相关方法
311 6 : bool RankInfoDetectService::RecvAndVerifyRemoteAgentIdAndRankSize(
312 : std::shared_ptr<Socket> connSocket, u32& expectedSocketNum, u32& previousRankSize)
313 : {
314 6 : HCCL_INFO("[RankInfoDetectService::%s] start.", __func__);
315 6 : SocketAgent socketAgent(connSocket.get());
316 :
317 : // 接收AgentId
318 6 : std::string agentId = "";
319 6 : bool ret = RecvRemoteAgentId(socketAgent, agentId);
320 6 : CHK_PRT_RET(!ret, HCCL_ERROR("[RankInfoDetectService::%s] RecvRemoteAgentId fail.", __func__), false);
321 :
322 : // 保存connSocket
323 6 : auto iter = connSockets_.find(agentId);
324 6 : CHK_PRT_RET(
325 : iter != connSockets_.end(),
326 : HCCL_ERROR("[RankInfoDetectService::%s] agentId[%s] has been connected.", __func__, agentId.c_str()), false);
327 5 : connSockets_.insert({agentId, connSocket});
328 :
329 : // 接收RankSize
330 5 : u32 rankSize = 0;
331 5 : ret = RecvRemoteRankSize(socketAgent, rankSize);
332 5 : CHK_PRT_RET(!ret, HCCL_ERROR("[RankInfoDetectService::%s] RecvRemoteAgentId fail.", __func__), false);
333 :
334 : // 校验
335 5 : expectedSocketNum = (previousRankSize == 0) ? rankSize : expectedSocketNum;
336 5 : CHK_PRT_RET(
337 : !VerifyRemoteRankSize(previousRankSize, rankSize),
338 : HCCL_ERROR("[RankInfoDetectService::%s] VerifyRemoteRankSize fail, rankSize[%u]", __func__, rankSize), false);
339 :
340 5 : HCCL_INFO("[RankInfoDetectService::%s] end.", __func__);
341 5 : return true;
342 6 : }
343 :
344 5 : bool RankInfoDetectService::VerifyRemoteRankSize(u32& previousRankSize, u32 remoteRankSize) const
345 : {
346 5 : HCCL_INFO("[RankInfoDetectService::%s] start.", __func__);
347 :
348 5 : if (previousRankSize == 0) {
349 3 : previousRankSize = remoteRankSize;
350 : } else {
351 2 : if (previousRankSize != remoteRankSize) {
352 0 : HCCL_ERROR(
353 : "[RankInfoDetectService::%s] VerifyRemoteRankSize failed. remoteRankSize[%u] is different "
354 : "from previousRankSize[%u].",
355 : __func__, remoteRankSize, previousRankSize);
356 0 : return false;
357 : }
358 : }
359 :
360 5 : HCCL_INFO("[RankInfoDetectService::%s] end.", __func__);
361 5 : return true;
362 : }
363 :
364 : // DFX相关方法
365 3 : void RankInfoDetectService::DisplayConnectedRanks()
366 : {
367 3 : vector<std::string> ranksInfo;
368 8 : for (const auto& it : connSockets_) {
369 5 : ranksInfo.push_back(it.first);
370 : }
371 3 : u64 ranksLen = ranksInfo.size();
372 3 : u64 lineNum = (ranksInfo.size() % DISPLAY_RANKNUM_PERLINE == 0) ? (ranksInfo.size() / DISPLAY_RANKNUM_PERLINE) :
373 3 : (ranksInfo.size() / DISPLAY_RANKNUM_PERLINE + 1);
374 3 : HCCL_ERROR(
375 : "[RankInfoDetectService::%s] total connected num is [%llu],line num is [%llu]", __func__, ranksLen, lineNum);
376 6 : for (u64 i = 0; i < lineNum; i++) {
377 3 : std::string tmpRankList;
378 8 : for (u32 j = 0; j < DISPLAY_RANKNUM_PERLINE; j++) {
379 8 : u32 ranksInfoIndex = i * DISPLAY_RANKNUM_PERLINE + j;
380 8 : if (ranksInfoIndex < ranksInfo.size()) {
381 5 : tmpRankList += "[" + ranksInfo[ranksInfoIndex] + "]";
382 : } else {
383 3 : break;
384 : }
385 5 : tmpRankList += ((j == DISPLAY_RANKNUM_PERLINE - 1 || ranksInfoIndex == ranksInfo.size() - 1) ? ";" : ",");
386 : }
387 3 : HCCL_ERROR("[RankInfoDetectService::%s] connected rankinfo[LINE %llu]: %s", __func__, i, tmpRankList.c_str());
388 3 : }
389 3 : }
390 :
391 2 : void RankInfoDetectService::DisplayConnectingStatus(u32 totalSockets, u32 waitSockets)
392 : {
393 2 : if (totalSockets == 0 && waitSockets == 1) {
394 0 : HCCL_INFO("[RankInfoDetectService::%s] wait for first connection.", __func__);
395 0 : return;
396 : }
397 :
398 2 : std::vector<bool> rankinfos(totalSockets, false);
399 5 : for (auto it : connSockets_) { // 建立映射
400 3 : u32 rankid = 0;
401 3 : HcclResult ret = SalStrToULong(it.first, HCCL_BASE_DECIMAL, rankid);
402 3 : CHK_PRT_RET(
403 : ret != HCCL_SUCCESS,
404 : HCCL_ERROR("[RankInfoDetectService::%s] agentId[%s] strToULong fail.", __func__, it.first.c_str()), );
405 3 : CHK_PRT_RET(
406 : rankid >= totalSockets,
407 : HCCL_ERROR(
408 : "[RankInfoDetectService::%s] invalid rankid[%u], rankSize[%u].", __func__, rankid, totalSockets), );
409 3 : rankinfos[rankid] = true;
410 3 : }
411 :
412 2 : u32 unRankCount = 0; // 只打印前三条未建链的rank
413 2 : std::vector<std::string> unsocketinfos;
414 9 : for (u32 rankid = 0; rankid < totalSockets; rankid++) {
415 8 : if (unRankCount >= SOCKET_PRINT_COUNT) {
416 1 : break;
417 : }
418 7 : if (!rankinfos[rankid]) {
419 5 : unRankCount++;
420 5 : std::string rankID = std::to_string(rankid);
421 5 : std::string agentID = std::string(16 - rankID.length(), '0') + rankID;
422 5 : unsocketinfos.push_back(agentID);
423 5 : }
424 : }
425 :
426 4 : std::string infoStr = "succ sockets is [" + std::to_string((totalSockets - waitSockets)) + "], waiting sockets is ["
427 6 : + std::to_string(waitSockets) + "], wait sockets rankid: ";
428 7 : for (u32 index = 0; index < unsocketinfos.size(); index++) {
429 5 : if (index == (unsocketinfos.size() - 1)) {
430 2 : infoStr += "[" + unsocketinfos[index] + "]";
431 : } else {
432 3 : infoStr += "[" + unsocketinfos[index] + "],";
433 : }
434 : }
435 :
436 2 : HCCL_INFO("[RankInfoDetectService::%s] %s", __func__, infoStr.c_str());
437 2 : }
438 :
439 30 : void RankInfoDetectService::TearDown()
440 : {
441 30 : HCCL_INFO("[RankInfoDetectService::%s] start.", __func__);
442 :
443 30 : CHK_PRT_RET(!serverSocket_, HCCL_INFO("[RankInfoDetectService::%s] serverSocket is null", __func__), );
444 :
445 : // close socket
446 30 : Disconnect();
447 :
448 : // 如果白名单使能则删除白名单
449 30 : if (!EnvConfig::GetInstance().GetHostNicConfig().GetWhitelistDisable()) {
450 0 : CHK_PRT_CONT(wlistInfo_.size() == 0, HCCL_ERROR("whitelist is empty"); break);
451 0 : SocketHandle hostSocketHandle = HostSocketHandleManager::GetInstance().Get(devPhyId_, hostIp_);
452 0 : HrtRaSocketWhiteListDel(hostSocketHandle, wlistInfo_);
453 : }
454 :
455 30 : s32 deviceLogicId = HrtGetDevice();
456 30 : if (EnvConfig::GetInstance().GetHostNicConfig().GetHostSocketPortRange().size() > 0
457 30 : || EnvConfig::GetInstance().GetHostNicConfig().GetIfBasePort() == HCCL_INVALID_PORT) {
458 : // 若开启抢占监听端口
459 30 : PreemptPortManager::GetInstance(deviceLogicId).Release(serverSocket_);
460 : } else {
461 : // 停止监听
462 0 : serverSocket_->StopListen();
463 : }
464 :
465 : // deinit handle
466 30 : HostSocketHandleManager::GetInstance().Destroy(devPhyId_, hostIp_);
467 :
468 : // deinit ra
469 30 : HccpPeerManager::GetInstance().DeInit(deviceLogicId);
470 :
471 30 : HCCL_INFO("[RankInfoDetectService::%s] end.", __func__);
472 : }
473 :
474 30 : RankInfoDetectService::~RankInfoDetectService() { DECTOR_TRY_CATCH("RankInfoDetectService", TearDown()); }
475 :
476 : } // namespace Hccl
|