stream.c 15.5 KB
Newer Older
L
Liu Jicong 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/*
 * Copyright (c) 2019 TAOS Data, Inc. <jhtao@taosdata.com>
 *
 * This program is free software: you can use, redistribute, and/or modify
 * it under the terms of the GNU Affero General Public License, version 3
 * or later ("AGPL"), as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

16
#include "streamInt.h"
17
#include "ttimer.h"
L
Liu Jicong 已提交
18

19
#define STREAM_TASK_INPUT_QUEUE_CAPACITY          20480
20
#define STREAM_TASK_INPUT_QUEUE_CAPACITY_IN_SIZE  (30)
H
Haojun Liao 已提交
21
#define ONE_MB_F                                  (1048576.0)
dengyihao's avatar
dengyihao 已提交
22
#define QUEUE_MEM_SIZE_IN_MB(_q)                  (taosQueueMemorySize(_q) / ONE_MB_F)
23

24 25
SStreamGlobalEnv streamEnv;

26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
int32_t streamInit() {
  int8_t old;
  while (1) {
    old = atomic_val_compare_exchange_8(&streamEnv.inited, 0, 2);
    if (old != 2) break;
  }

  if (old == 0) {
    streamEnv.timer = taosTmrInit(10000, 100, 10000, "STREAM");
    if (streamEnv.timer == NULL) {
      atomic_store_8(&streamEnv.inited, 0);
      return -1;
    }
    atomic_store_8(&streamEnv.inited, 1);
  }
41

42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
  return 0;
}

void streamCleanUp() {
  int8_t old;
  while (1) {
    old = atomic_val_compare_exchange_8(&streamEnv.inited, 1, 2);
    if (old != 2) break;
  }

  if (old == 1) {
    taosTmrCleanUp(streamEnv.timer);
    atomic_store_8(&streamEnv.inited, 0);
  }
}

58 59 60 61 62 63
char* createStreamTaskIdStr(int64_t streamId, int32_t taskId) {
  char buf[128] = {0};
  sprintf(buf, "0x%" PRIx64 "-0x%x", streamId, taskId);
  return taosStrdup(buf);
}

H
Haojun Liao 已提交
64
static void streamSchedByTimer(void* param, void* tmrId) {
65 66
  SStreamTask* pTask = (void*)param;

67
  int8_t status = atomic_load_8(&pTask->triggerStatus);
68
  qDebug("s-task:%s in scheduler, trigger status:%d, next:%dms", pTask->id.idStr, status, (int32_t)pTask->triggerParam);
69

L
liuyao 已提交
70
  if (streamTaskShouldStop(&pTask->status) || streamTaskShouldPause(&pTask->status)) {
L
Liu Jicong 已提交
71
    streamMetaReleaseTask(NULL, pTask);
72
    qDebug("s-task:%s jump out of schedTimer", pTask->id.idStr);
L
Liu Jicong 已提交
73 74 75
    return;
  }

76
  if (status == TASK_TRIGGER_STATUS__ACTIVE) {
77 78
    SStreamTrigger* pTrigger = taosAllocateQitem(sizeof(SStreamTrigger), DEF_QITEM, 0);
    if (pTrigger == NULL) {
79 80 81
      return;
    }

82 83 84 85
    pTrigger->type = STREAM_INPUT__GET_RES;
    pTrigger->pBlock = taosMemoryCalloc(1, sizeof(SSDataBlock));
    if (pTrigger->pBlock == NULL) {
      taosFreeQitem(pTrigger);
86 87 88
      return;
    }

89
    atomic_store_8(&pTask->triggerStatus, TASK_TRIGGER_STATUS__INACTIVE);
90 91 92
    pTrigger->pBlock->info.type = STREAM_GET_ALL;
    if (tAppendDataToInputQueue(pTask, (SStreamQueueItem*)pTrigger) < 0) {
      taosFreeQitem(pTrigger);
93
      taosTmrReset(streamSchedByTimer, (int32_t)pTask->triggerParam, pTask, streamEnv.timer, &pTask->schedTimer);
L
Liu Jicong 已提交
94 95
      return;
    }
96

L
Liu Jicong 已提交
97
    streamSchedExec(pTask);
98 99
  }

100
  taosTmrReset(streamSchedByTimer, (int32_t)pTask->triggerParam, pTask, streamEnv.timer, &pTask->schedTimer);
101 102
}

103
int32_t streamSetupScheduleTrigger(SStreamTask* pTask) {
104
  if (pTask->triggerParam != 0 && pTask->info.fillHistory == 0) {
L
Liu Jicong 已提交
105
    int32_t ref = atomic_add_fetch_32(&pTask->refCnt, 1);
106 107
    ASSERT(ref == 2 && pTask->schedTimer == NULL);

H
Haojun Liao 已提交
108
    qDebug("s-task:%s setup scheduler trigger, delay:%"PRId64" ms", pTask->id.idStr, pTask->triggerParam);
109 110

    pTask->schedTimer = taosTmrStart(streamSchedByTimer, (int32_t)pTask->triggerParam, pTask, streamEnv.timer);
111
    pTask->triggerStatus = TASK_TRIGGER_STATUS__INACTIVE;
112
  }
113

114 115 116
  return 0;
}

L
Liu Jicong 已提交
117
int32_t streamSchedExec(SStreamTask* pTask) {
dengyihao's avatar
dengyihao 已提交
118 119
  int8_t schedStatus = atomic_val_compare_exchange_8(&pTask->status.schedStatus, TASK_SCHED_STATUS__INACTIVE,
                                                     TASK_SCHED_STATUS__WAITING);
120

L
Liu Jicong 已提交
121 122 123
  if (schedStatus == TASK_SCHED_STATUS__INACTIVE) {
    SStreamTaskRunReq* pRunReq = rpcMallocCont(sizeof(SStreamTaskRunReq));
    if (pRunReq == NULL) {
124
      terrno = TSDB_CODE_OUT_OF_MEMORY;
125
      atomic_store_8(&pTask->status.schedStatus, TASK_SCHED_STATUS__INACTIVE);
126
      qError("failed to create msg to aunch s-task:%s, reason out of memory", pTask->id.idStr);
L
Liu Jicong 已提交
127 128
      return -1;
    }
129

130
    pRunReq->head.vgId = pTask->info.nodeId;
131 132
    pRunReq->streamId = pTask->id.streamId;
    pRunReq->taskId = pTask->id.taskId;
133

134 135
    qDebug("trigger to run s-task:%s", pTask->id.idStr);

dengyihao's avatar
dengyihao 已提交
136
    SRpcMsg msg = {.msgType = TDMT_STREAM_TASK_RUN, .pCont = pRunReq, .contLen = sizeof(SStreamTaskRunReq)};
L
Liu Jicong 已提交
137
    tmsgPutToQueue(pTask->pMsgCb, STREAM_QUEUE, &msg);
138 139
  } else {
    qDebug("s-task:%s not launch task since sched status:%d", pTask->id.idStr, pTask->status.schedStatus);
L
Liu Jicong 已提交
140
  }
141

L
Liu Jicong 已提交
142 143 144
  return 0;
}

L
Liu Jicong 已提交
145
int32_t streamTaskEnqueueRetrieve(SStreamTask* pTask, SStreamRetrieveReq* pReq, SRpcMsg* pRsp) {
S
Shengliang Guan 已提交
146
  SStreamDataBlock* pData = taosAllocateQitem(sizeof(SStreamDataBlock), DEF_QITEM, 0);
L
Liu Jicong 已提交
147 148 149 150
  int8_t            status = TASK_INPUT_STATUS__NORMAL;

  // enqueue
  if (pData != NULL) {
151 152
    qDebug("s-task:%s (child %d) recv retrieve req from task:0x%x(vgId:%d), reqId:0x%" PRIx64, pTask->id.idStr, pTask->info.selfChildId,
           pReq->srcTaskId, pReq->srcNodeId, pReq->reqId);
L
Liu Jicong 已提交
153

154
    pData->type = STREAM_INPUT__DATA_RETRIEVE;
L
Liu Jicong 已提交
155 156
    pData->srcVgId = 0;
    streamRetrieveReqToData(pReq, pData);
157
    if (tAppendDataToInputQueue(pTask, (SStreamQueueItem*)pData) == 0) {
L
Liu Jicong 已提交
158 159 160 161
      status = TASK_INPUT_STATUS__NORMAL;
    } else {
      status = TASK_INPUT_STATUS__FAILED;
    }
162
  } else {  // todo handle oom
L
Liu Jicong 已提交
163 164 165 166 167 168 169 170 171 172 173
    /*streamTaskInputFail(pTask);*/
    /*status = TASK_INPUT_STATUS__FAILED;*/
  }

  // rsp by input status
  void* buf = rpcMallocCont(sizeof(SMsgHead) + sizeof(SStreamRetrieveRsp));
  ((SMsgHead*)buf)->vgId = htonl(pReq->srcNodeId);
  SStreamRetrieveRsp* pCont = POINTER_SHIFT(buf, sizeof(SMsgHead));
  pCont->streamId = pReq->streamId;
  pCont->rspToTaskId = pReq->srcTaskId;
  pCont->rspFromTaskId = pReq->dstTaskId;
174
  pRsp->pCont = buf;
175
  pRsp->contLen = sizeof(SMsgHead) + sizeof(SStreamRetrieveRsp);
L
Liu Jicong 已提交
176
  tmsgSendRsp(pRsp);
177

L
Liu Jicong 已提交
178 179 180
  return status == TASK_INPUT_STATUS__NORMAL ? 0 : -1;
}

181 182
// todo add log
int32_t streamTaskOutputResultBlock(SStreamTask* pTask, SStreamDataBlock* pBlock) {
dengyihao's avatar
dengyihao 已提交
183
  int32_t code = 0;
184 185
  int32_t type = pTask->outputInfo.type;
  if (type == TASK_OUTPUT__TABLE) {
L
Liu Jicong 已提交
186
    pTask->tbSink.tbSinkFunc(pTask, pTask->tbSink.vnode, 0, pBlock->blocks);
187
    destroyStreamDataBlock(pBlock);
188
  } else if (type == TASK_OUTPUT__SMA) {
L
Liu Jicong 已提交
189
    pTask->smaSink.smaSink(pTask->smaSink.vnode, pTask->smaSink.smaId, pBlock->blocks);
190
    destroyStreamDataBlock(pBlock);
L
Liu Jicong 已提交
191
  } else {
192 193
    ASSERT(type == TASK_OUTPUT__FIXED_DISPATCH || type == TASK_OUTPUT__SHUFFLE_DISPATCH);
    code = taosWriteQitem(pTask->outputInfo.queue->queue, pBlock);
H
Haojun Liao 已提交
194
    if (code != 0) {  // todo failed to add it into the output queue, free it.
dengyihao's avatar
dengyihao 已提交
195 196
      return code;
    }
197

198
    streamDispatchStreamBlock(pTask);
L
Liu Jicong 已提交
199
  }
200

L
Liu Jicong 已提交
201 202 203
  return 0;
}

204 205


206 207 208
static int32_t streamTaskAppendInputBlocks(SStreamTask* pTask, const SStreamDispatchReq* pReq) {
  int8_t status = 0;

209
  SStreamDataBlock* pBlock = createStreamDataFromDispatchMsg(pReq, pReq->type, pReq->srcVgId);
210 211 212 213 214 215
  if (pBlock == NULL) {
    streamTaskInputFail(pTask);
    status = TASK_INPUT_STATUS__FAILED;
    qError("vgId:%d, s-task:%s failed to receive dispatch msg, reason: out of memory", pTask->pMeta->vgId,
           pTask->id.idStr);
  } else {
216 217 218 219
    if (pBlock->type == STREAM_INPUT__TRANS_STATE) {
      pTask->status.appendTranstateBlock = true;
    }

220 221 222 223 224 225 226 227
    int32_t code = tAppendDataToInputQueue(pTask, (SStreamQueueItem*)pBlock);
    // input queue is full, upstream is blocked now
    status = (code == TSDB_CODE_SUCCESS) ? TASK_INPUT_STATUS__NORMAL : TASK_INPUT_STATUS__BLOCKED;
  }

  return status;
}

228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
static int32_t buildDispatchRsp(const SStreamTask* pTask, const SStreamDispatchReq* pReq, int32_t status, void** pBuf) {
  *pBuf = rpcMallocCont(sizeof(SMsgHead) + sizeof(SStreamDispatchRsp));
  if (*pBuf == NULL) {
    return TSDB_CODE_OUT_OF_MEMORY;
  }

  ((SMsgHead*)(*pBuf))->vgId = htonl(pReq->upstreamNodeId);
  SStreamDispatchRsp* pDispatchRsp = POINTER_SHIFT((*pBuf), sizeof(SMsgHead));

  pDispatchRsp->inputStatus = status;
  pDispatchRsp->streamId = htobe64(pReq->streamId);
  pDispatchRsp->upstreamNodeId = htonl(pReq->upstreamNodeId);
  pDispatchRsp->upstreamTaskId = htonl(pReq->upstreamTaskId);
  pDispatchRsp->downstreamNodeId = htonl(pTask->info.nodeId);
  pDispatchRsp->downstreamTaskId = htonl(pTask->id.taskId);

  return TSDB_CODE_SUCCESS;
}

247 248 249 250 251 252 253
void streamTaskCloseUpstreamInput(SStreamTask* pTask, int32_t taskId) {
  SStreamChildEpInfo* pInfo = streamTaskGetUpstreamTaskEpInfo(pTask, taskId);
  if (pInfo != NULL) {
    pInfo->dataAllowed = false;
  }
}

254 255 256
int32_t streamProcessDispatchMsg(SStreamTask* pTask, SStreamDispatchReq* pReq, SRpcMsg* pRsp, bool exec) {
  qDebug("s-task:%s receive dispatch msg from taskId:0x%x(vgId:%d), msgLen:%" PRId64, pTask->id.idStr,
         pReq->upstreamTaskId, pReq->upstreamNodeId, pReq->totalLen);
L
Liu Jicong 已提交
257

258
  int32_t status = 0;
L
Liu Jicong 已提交
259

260 261 262 263 264 265
  SStreamChildEpInfo* pInfo = streamTaskGetUpstreamTaskEpInfo(pTask, pReq->upstreamTaskId);
  ASSERT(pInfo != NULL);

  if (!pInfo->dataAllowed) {
    qWarn("s-task:%s data from task:0x%x is denied, since inputQ is closed for it", pTask->id.idStr, pReq->upstreamTaskId);
    status = TASK_INPUT_STATUS__BLOCKED;
L
Liu Jicong 已提交
266
  } else {
267 268 269 270 271 272 273
    // Current task has received the checkpoint req from the upstream task, from which the message should all be blocked
    if (pReq->type == STREAM_INPUT__CHECKPOINT_TRIGGER) {
      streamTaskCloseUpstreamInput(pTask, pReq->upstreamTaskId);
      qDebug("s-task:%s close inputQ for upstream:0x%x", pTask->id.idStr, pReq->upstreamTaskId);
    }

    status = streamTaskAppendInputBlocks(pTask, pReq);
274
  }
L
Liu Jicong 已提交
275

276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
  {
    // do send response with the input status
    int32_t code = buildDispatchRsp(pTask, pReq, status, &pRsp->pCont);
    if (code != TSDB_CODE_SUCCESS) {
      // todo handle failure
      return code;
    }

    pRsp->contLen = sizeof(SMsgHead) + sizeof(SStreamDispatchRsp);
    tmsgSendRsp(pRsp);
  }

  tDeleteStreamDispatchReq(pReq);
  streamSchedExec(pTask);

L
Liu Jicong 已提交
291 292 293
  return 0;
}

294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
//int32_t streamProcessDispatchMsg(SStreamTask* pTask, SStreamDispatchReq* pReq, SRpcMsg* pRsp, bool exec) {
//  qDebug("s-task:%s receive dispatch msg from taskId:0x%x(vgId:%d), msgLen:%" PRId64, pTask->id.idStr,
//         pReq->upstreamTaskId, pReq->upstreamNodeId, pReq->totalLen);
//
//  // todo add the input queue buffer limitation
//  streamTaskEnqueueBlocks(pTask, pReq, pRsp);
//  tDeleteStreamDispatchReq(pReq);
//
//  if (exec) {
//    if (streamTryExec(pTask) < 0) {
//      return -1;
//    }
//  } else {
//    streamSchedExec(pTask);
//  }
//
//  return 0;
//}

L
Liu Jicong 已提交
313
int32_t streamProcessRunReq(SStreamTask* pTask) {
L
Liu Jicong 已提交
314 315 316
  if (streamTryExec(pTask) < 0) {
    return -1;
  }
L
Liu Jicong 已提交
317

L
Liu Jicong 已提交
318 319 320
  return 0;
}

L
Liu Jicong 已提交
321 322
int32_t streamProcessRetrieveReq(SStreamTask* pTask, SStreamRetrieveReq* pReq, SRpcMsg* pRsp) {
  streamTaskEnqueueRetrieve(pTask, pReq, pRsp);
323
  ASSERT(pTask->info.taskLevel != TASK_LEVEL__SINK);
L
Liu Jicong 已提交
324
  streamSchedExec(pTask);
L
Liu Jicong 已提交
325 326 327
  return 0;
}

328
bool tInputQueueIsFull(const SStreamTask* pTask) {
329
  bool   isFull = taosQueueItemSize((pTask->inputQueue->queue)) >= STREAM_TASK_INPUT_QUEUE_CAPACITY;
330
  double size = QUEUE_MEM_SIZE_IN_MB(pTask->inputQueue->queue);
331
  return (isFull || size >= STREAM_TASK_INPUT_QUEUE_CAPACITY_IN_SIZE);
332 333
}

334
int32_t tAppendDataToInputQueue(SStreamTask* pTask, SStreamQueueItem* pItem) {
335 336 337
  int8_t  type = pItem->type;
  int32_t total = taosQueueItemSize(pTask->inputQueue->queue) + 1;
  double  size = QUEUE_MEM_SIZE_IN_MB(pTask->inputQueue->queue);
338 339

  if (type == STREAM_INPUT__DATA_SUBMIT) {
340
    SStreamDataSubmit* px = (SStreamDataSubmit*)pItem;
341
    if ((pTask->info.taskLevel == TASK_LEVEL__SOURCE) && tInputQueueIsFull(pTask)) {
342
      qError("s-task:%s input queue is full, capacity(size:%d num:%dMiB), current(blocks:%d, size:%.2fMiB) stop to push data",
343
             pTask->id.idStr, STREAM_TASK_INPUT_QUEUE_CAPACITY, STREAM_TASK_INPUT_QUEUE_CAPACITY_IN_SIZE, total,
dengyihao's avatar
dengyihao 已提交
344 345 346
             size);
      streamDataSubmitDestroy(px);
      taosFreeQitem(pItem);
347 348
      return -1;
    }
349

350 351 352
    int32_t msgLen = px->submit.msgLen;
    int64_t ver = px->submit.ver;

353 354 355 356 357 358
    int32_t code = taosWriteQitem(pTask->inputQueue->queue, pItem);
    if (code != TSDB_CODE_SUCCESS) {
      streamDataSubmitDestroy(px);
      taosFreeQitem(pItem);
      return code;
    }
359

360
    // use the local variable to avoid the pItem be freed by other threads, since it has been put into queue already.
361
    qDebug("s-task:%s submit enqueue msgLen:%d ver:%" PRId64 ", total in queue:%d, size:%.2fMiB", pTask->id.idStr,
362
           msgLen, ver, total, size + msgLen/1048576.0);
363 364
  } else if (type == STREAM_INPUT__DATA_BLOCK || type == STREAM_INPUT__DATA_RETRIEVE ||
             type == STREAM_INPUT__REF_DATA_BLOCK) {
365
    if ((pTask->info.taskLevel == TASK_LEVEL__SOURCE) && (tInputQueueIsFull(pTask))) {
H
Haojun Liao 已提交
366
      qError("s-task:%s input queue is full, capacity:%d size:%d MiB, current(blocks:%d, size:%.2fMiB) abort",
367
             pTask->id.idStr, STREAM_TASK_INPUT_QUEUE_CAPACITY, STREAM_TASK_INPUT_QUEUE_CAPACITY_IN_SIZE, total,
H
Haojun Liao 已提交
368
             size);
369
      destroyStreamDataBlock((SStreamDataBlock*) pItem);
370 371 372
      return -1;
    }

373
    qDebug("s-task:%s blockdata enqueue, total in queue:%d, size:%.2fMiB", pTask->id.idStr, total, size);
374 375 376 377 378
    int32_t code = taosWriteQitem(pTask->inputQueue->queue, pItem);
    if (code != TSDB_CODE_SUCCESS) {
      destroyStreamDataBlock((SStreamDataBlock*) pItem);
      return code;
    }
H
Haojun Liao 已提交
379
  } else if (type == STREAM_INPUT__CHECKPOINT || type == STREAM_INPUT__TRANS_STATE) {
380
    taosWriteQitem(pTask->inputQueue->queue, pItem);
H
Haojun Liao 已提交
381
    qDebug("s-task:%s trans-state blockdata enqueue, total in queue:%d, size:%.2fMiB", pTask->id.idStr, total, size);
382
  } else if (type == STREAM_INPUT__GET_RES) {
383
    // use the default memory limit, refactor later.
384
    taosWriteQitem(pTask->inputQueue->queue, pItem);
H
Haojun Liao 已提交
385
    qDebug("s-task:%s data res enqueue, current(blocks:%d, size:%.2fMiB)", pTask->id.idStr, total, size);
386 387
  } else {
    ASSERT(0);
388 389 390 391
  }

  if (type != STREAM_INPUT__GET_RES && type != STREAM_INPUT__CHECKPOINT && pTask->triggerParam != 0) {
    atomic_val_compare_exchange_8(&pTask->triggerStatus, TASK_TRIGGER_STATUS__INACTIVE, TASK_TRIGGER_STATUS__ACTIVE);
392
    qDebug("s-task:%s new data arrived, active the trigger, trigerStatus:%d", pTask->id.idStr, pTask->triggerStatus);
393 394 395
  }

  return 0;
396 397
}

398 399
static void* streamQueueCurItem(SStreamQueue* queue) { return queue->qItem; }

400 401 402 403 404 405
void* streamQueueNextItem(SStreamQueue* pQueue) {
  int8_t flag = atomic_exchange_8(&pQueue->status, STREAM_QUEUE__PROCESSING);

  if (flag == STREAM_QUEUE__FAILED) {
    ASSERT(pQueue->qItem != NULL);
    return streamQueueCurItem(pQueue);
406
  } else {
407 408 409 410 411
    pQueue->qItem = NULL;
    taosGetQitem(pQueue->qall, &pQueue->qItem);
    if (pQueue->qItem == NULL) {
      taosReadAllQitems(pQueue->queue, pQueue->qall);
      taosGetQitem(pQueue->qall, &pQueue->qItem);
412
    }
413 414

    return streamQueueCurItem(pQueue);
415
  }
416 417
}

418 419 420 421 422 423 424 425 426 427 428 429
void streamTaskInputFail(SStreamTask* pTask) { atomic_store_8(&pTask->inputStatus, TASK_INPUT_STATUS__FAILED); }

SStreamChildEpInfo * streamTaskGetUpstreamTaskEpInfo(SStreamTask* pTask, int32_t taskId) {
  int32_t num = taosArrayGetSize(pTask->pUpstreamEpInfoList);
  for(int32_t i = 0; i < num; ++i) {
    SStreamChildEpInfo* pInfo = taosArrayGetP(pTask->pUpstreamEpInfoList, i);
    if (pInfo->taskId == taskId) {
      return pInfo;
    }
  }

  return NULL;
430 431 432 433 434 435 436 437 438 439 440 441 442
}

void streamTaskOpenAllUpstreamInput(SStreamTask* pTask) {
  int32_t num = taosArrayGetSize(pTask->pUpstreamEpInfoList);
  if (num == 0) {
    return;
  }

  for(int32_t i = 0; i < num; ++i) {
    SStreamChildEpInfo* pInfo = taosArrayGetP(pTask->pUpstreamEpInfoList, i);
    pInfo->dataAllowed = true;
  }
}