From fc2d2672b9864286139a0fa0c703ca17075c6080 Mon Sep 17 00:00:00 2001 From: Jinqing Kuang Date: Wed, 12 Feb 2025 23:33:15 +0800 Subject: [PATCH 01/17] feat(stream)[TS-5469]: add more stream event notifications - Add event notifications for all window types - Implement window invalidation notifications - Add statistics for event notification delivery --- include/common/tdatablock.h | 2 + include/common/tmsg.h | 16 + include/libs/executor/executor.h | 2 +- include/libs/executor/storageapi.h | 1 + include/libs/nodes/cmdnodes.h | 1 + include/libs/stream/streamState.h | 3 +- include/libs/stream/tstream.h | 2 + include/libs/stream/tstreamFileState.h | 2 + include/util/tdef.h | 1 + source/common/src/msg/streamMsg.c | 30 +- source/common/src/systable.c | 1 + source/common/src/tdatablock.c | 49 ++ source/dnode/mnode/impl/src/mndStream.c | 2 +- source/dnode/mnode/impl/src/mndStreamUtil.c | 47 +- source/dnode/snode/src/snodeInitApi.c | 3 +- source/dnode/vnode/src/tq/tqStreamNotify.c | 140 ++-- source/dnode/vnode/src/tqCommon/tqCommon.c | 6 +- source/dnode/vnode/src/vnd/vnodeInitApi.c | 1 + source/libs/executor/inc/executorInt.h | 15 +- source/libs/executor/inc/querytask.h | 39 +- source/libs/executor/inc/streamexecutorInt.h | 60 +- source/libs/executor/src/executor.c | 3 +- .../executor/src/streamcountwindowoperator.c | 105 ++- .../executor/src/streameventwindowoperator.c | 78 +- source/libs/executor/src/streamexecutorInt.c | 706 +++++++++++++----- .../src/streamintervalsliceoperator.c | 40 +- .../executor/src/streamtimesliceoperator.c | 3 +- .../executor/src/streamtimewindowoperator.c | 366 +++++++-- source/libs/parser/src/parTranslater.c | 2 +- source/libs/stream/src/streamSessionState.c | 36 + source/libs/stream/src/streamState.c | 4 + source/libs/stream/src/streamTask.c | 4 + 32 files changed, 1340 insertions(+), 430 deletions(-) diff --git a/include/common/tdatablock.h b/include/common/tdatablock.h index 96478047ca..aa07183592 100644 --- a/include/common/tdatablock.h +++ b/include/common/tdatablock.h @@ -195,6 +195,8 @@ int32_t colDataSetVal(SColumnInfoData* pColumnInfoData, uint32_t rowIndex, const // For the VAR_DATA_TYPE type, if a row already has data before inserting it (judged by offset != -1), // it will be inserted at the original position and the old data will be overwritten. int32_t colDataSetValOrCover(SColumnInfoData* pColumnInfoData, uint32_t rowIndex, const char* pData, bool isNull); +int32_t varColSetVarData(SColumnInfoData* pColumnInfoData, uint32_t rowIndex, const char* pVarData, int32_t varDataLen, + bool isNull); int32_t colDataReassignVal(SColumnInfoData* pColumnInfoData, uint32_t dstRowIdx, uint32_t srcRowIdx, const char* pData); int32_t colDataSetNItems(SColumnInfoData* pColumnInfoData, uint32_t rowIndex, const char* pData, uint32_t numOfRows, bool trimValue); diff --git a/include/common/tmsg.h b/include/common/tmsg.h index c347c3f38a..6f78ec32a5 100644 --- a/include/common/tmsg.h +++ b/include/common/tmsg.h @@ -2966,6 +2966,22 @@ typedef struct { int8_t notifyHistory; } SCMCreateStreamReq; +typedef struct STaskNotifyEventStat { + int64_t notifyEventAddTimes; // call times of add function + int64_t notifyEventAddElems; // elements added by add function + double notifyEventAddCostSec; // time cost of add function + int64_t notifyEventPushTimes; // call times of push function + int64_t notifyEventPushElems; // elements pushed by push function + double notifyEventPushCostSec; // time cost of push function + int64_t notifyEventPackTimes; // call times of pack function + int64_t notifyEventPackElems; // elements packed by pack function + double notifyEventPackCostSec; // time cost of pack function + int64_t notifyEventSendTimes; // call times of send function + int64_t notifyEventSendElems; // elements sent by send function + double notifyEventSendCostSec; // time cost of send function + int64_t notifyEventHoldElems; // elements hold due to watermark +} STaskNotifyEventStat; + typedef struct { int64_t streamId; } SCMCreateStreamRsp; diff --git a/include/libs/executor/executor.h b/include/libs/executor/executor.h index 9a7c3912b0..f05234b82f 100644 --- a/include/libs/executor/executor.h +++ b/include/libs/executor/executor.h @@ -99,7 +99,7 @@ int32_t qSetTaskId(qTaskInfo_t tinfo, uint64_t taskId, uint64_t queryId); int32_t qSetStreamOpOpen(qTaskInfo_t tinfo); int32_t qSetStreamNotifyInfo(qTaskInfo_t tinfo, int32_t eventTypes, const SSchemaWrapper* pSchemaWrapper, - const char* stbFullName, bool newSubTableRule); + const char* stbFullName, bool newSubTableRule, STaskNotifyEventStat* pNotifyEventStat); /** * Set multiple input data blocks for the stream scan. diff --git a/include/libs/executor/storageapi.h b/include/libs/executor/storageapi.h index 3cc2acf30f..75252e8d9f 100644 --- a/include/libs/executor/storageapi.h +++ b/include/libs/executor/storageapi.h @@ -431,6 +431,7 @@ typedef struct SStateStore { int32_t (*updateInfoSerialize)(SEncoder* pEncoder, const SUpdateInfo* pInfo); int32_t (*updateInfoDeserialize)(SDecoder* pDeCoder, SUpdateInfo* pInfo); + SStreamStateCur* (*streamStateSessionSeekKeyPrev)(SStreamState* pState, const SSessionKey* key); SStreamStateCur* (*streamStateSessionSeekKeyNext)(SStreamState* pState, const SSessionKey* key); SStreamStateCur* (*streamStateCountSeekKeyPrev)(SStreamState* pState, const SSessionKey* pKey, COUNT_TYPE count); SStreamStateCur* (*streamStateSessionSeekKeyCurrentPrev)(SStreamState* pState, const SSessionKey* key); diff --git a/include/libs/nodes/cmdnodes.h b/include/libs/nodes/cmdnodes.h index 76db5e29a4..2cd743b37e 100644 --- a/include/libs/nodes/cmdnodes.h +++ b/include/libs/nodes/cmdnodes.h @@ -573,6 +573,7 @@ typedef enum EStreamNotifyOptionSetFlag { } EStreamNotifyOptionSetFlag; typedef enum EStreamNotifyEventType { + SNOTIFY_EVENT_WINDOW_INVALIDATION = 0, SNOTIFY_EVENT_WINDOW_OPEN = BIT_FLAG_MASK(0), SNOTIFY_EVENT_WINDOW_CLOSE = BIT_FLAG_MASK(1), } EStreamNotifyEventType; diff --git a/include/libs/stream/streamState.h b/include/libs/stream/streamState.h index b4e0087b1a..52a61e9452 100644 --- a/include/libs/stream/streamState.h +++ b/include/libs/stream/streamState.h @@ -65,6 +65,7 @@ int32_t streamStateCountGetKeyByRange(SStreamState* pState, const SSessionKey* r int32_t streamStateSessionAllocWinBuffByNextPosition(SStreamState* pState, SStreamStateCur* pCur, const SSessionKey* pKey, void** pVal, int32_t* pVLen); +SStreamStateCur *streamStateSessionSeekKeyPrev(SStreamState *pState, const SSessionKey *key); SStreamStateCur* streamStateSessionSeekKeyNext(SStreamState* pState, const SSessionKey* key); SStreamStateCur* streamStateCountSeekKeyPrev(SStreamState* pState, const SSessionKey* pKey, COUNT_TYPE count); SStreamStateCur* streamStateSessionSeekKeyCurrentPrev(SStreamState* pState, const SSessionKey* key); @@ -162,4 +163,4 @@ int stateKeyCmpr(const void* pKey1, int kLen1, const void* pKey2, int kLen2); } #endif -#endif /* ifndef _STREAM_STATE_H_ */ \ No newline at end of file +#endif /* ifndef _STREAM_STATE_H_ */ diff --git a/include/libs/stream/tstream.h b/include/libs/stream/tstream.h index 9cd6dd13ca..041d888d33 100644 --- a/include/libs/stream/tstream.h +++ b/include/libs/stream/tstream.h @@ -463,6 +463,7 @@ struct SStreamTask { SUpstreamInfo upstreamInfo; STaskCheckInfo taskCheckInfo; SNotifyInfo notifyInfo; + STaskNotifyEventStat notifyEventStat; // the followings attributes don't be serialized SScanhistorySchedInfo schedHistoryInfo; @@ -632,6 +633,7 @@ typedef struct STaskStatusEntry { int64_t startCheckpointVer; int64_t hTaskId; STaskCkptInfo checkpointInfo; + STaskNotifyEventStat notifyEventStat; } STaskStatusEntry; //typedef struct SNodeUpdateInfo { diff --git a/include/libs/stream/tstreamFileState.h b/include/libs/stream/tstreamFileState.h index f47c308e18..f07034adda 100644 --- a/include/libs/stream/tstreamFileState.h +++ b/include/libs/stream/tstreamFileState.h @@ -100,10 +100,12 @@ void sessionWinStateCleanup(void* pBuff); SStreamStateCur* createStateCursor(SStreamFileState* pFileState); SStreamStateCur* sessionWinStateSeekKeyCurrentPrev(SStreamFileState* pFileState, const SSessionKey* pWinKey); SStreamStateCur* sessionWinStateSeekKeyCurrentNext(SStreamFileState* pFileState, const SSessionKey* pWinKey); +SStreamStateCur* sessionWinStateSeekKeyPrev(SStreamFileState* pFileState, const SSessionKey* pWinKey); SStreamStateCur* sessionWinStateSeekKeyNext(SStreamFileState* pFileState, const SSessionKey* pWinKey); SStreamStateCur* countWinStateSeekKeyPrev(SStreamFileState* pFileState, const SSessionKey* pWinKey, COUNT_TYPE count); int32_t sessionWinStateGetKVByCur(SStreamStateCur* pCur, SSessionKey* pKey, void** pVal, int32_t* pVLen); void sessionWinStateMoveToNext(SStreamStateCur* pCur); +void sessionWinStateMoveToPrev(SStreamStateCur* pCur); int32_t sessionWinStateGetKeyByRange(SStreamFileState* pFileState, const SSessionKey* key, SSessionKey* curKey, range_cmpr_fn cmpFn); diff --git a/include/util/tdef.h b/include/util/tdef.h index f08697b0d4..0c15803e37 100644 --- a/include/util/tdef.h +++ b/include/util/tdef.h @@ -246,6 +246,7 @@ typedef enum ELogicConditionType { #define TSDB_USER_CGROUP_LEN (TSDB_USER_LEN + TSDB_CGROUP_LEN) // it is a null-terminated string #define TSDB_STREAM_NAME_LEN 193 // it is a null-terminated string #define TSDB_STREAM_NOTIFY_URL_LEN 128 // it includes the terminating '\0' +#define TSDB_STREAM_NOTIFY_STAT_LEN 350 // it includes the terminating '\0' #define TSDB_DB_NAME_LEN 65 #define TSDB_DB_FNAME_LEN (TSDB_ACCT_ID_LEN + TSDB_DB_NAME_LEN + TSDB_NAME_DELIMITER_LEN) #define TSDB_PRIVILEDGE_CONDITION_LEN 48 * 1024 diff --git a/source/common/src/msg/streamMsg.c b/source/common/src/msg/streamMsg.c index 54b17b14d1..7e7952eb60 100644 --- a/source/common/src/msg/streamMsg.c +++ b/source/common/src/msg/streamMsg.c @@ -56,6 +56,7 @@ typedef struct STaskStatusEntry { int64_t startCheckpointVer; int64_t hTaskId; STaskCkptInfo checkpointInfo; + STaskNotifyEventStat notifyEventStat; } STaskStatusEntry; int32_t tEncodeStreamEpInfo(SEncoder* pEncoder, const SStreamUpstreamEpInfo* pInfo) { @@ -523,6 +524,19 @@ int32_t tEncodeStreamHbMsg(SEncoder* pEncoder, const SStreamHbMsg* pReq) { TAOS_CHECK_EXIT(tEncodeI64(pEncoder, ps->startCheckpointId)); TAOS_CHECK_EXIT(tEncodeI64(pEncoder, ps->startCheckpointVer)); TAOS_CHECK_EXIT(tEncodeI64(pEncoder, ps->hTaskId)); + TAOS_CHECK_EXIT(tEncodeI64(pEncoder, ps->notifyEventStat.notifyEventAddTimes)); + TAOS_CHECK_EXIT(tEncodeI64(pEncoder, ps->notifyEventStat.notifyEventAddElems)); + TAOS_CHECK_EXIT(tEncodeDouble(pEncoder, ps->notifyEventStat.notifyEventAddCostSec)); + TAOS_CHECK_EXIT(tEncodeI64(pEncoder, ps->notifyEventStat.notifyEventPushTimes)); + TAOS_CHECK_EXIT(tEncodeI64(pEncoder, ps->notifyEventStat.notifyEventPushElems)); + TAOS_CHECK_EXIT(tEncodeDouble(pEncoder, ps->notifyEventStat.notifyEventPushCostSec)); + TAOS_CHECK_EXIT(tEncodeI64(pEncoder, ps->notifyEventStat.notifyEventPackTimes)); + TAOS_CHECK_EXIT(tEncodeI64(pEncoder, ps->notifyEventStat.notifyEventPackElems)); + TAOS_CHECK_EXIT(tEncodeDouble(pEncoder, ps->notifyEventStat.notifyEventPackCostSec)); + TAOS_CHECK_EXIT(tEncodeI64(pEncoder, ps->notifyEventStat.notifyEventSendTimes)); + TAOS_CHECK_EXIT(tEncodeI64(pEncoder, ps->notifyEventStat.notifyEventSendElems)); + TAOS_CHECK_EXIT(tEncodeDouble(pEncoder, ps->notifyEventStat.notifyEventSendCostSec)); + TAOS_CHECK_EXIT(tEncodeI64(pEncoder, ps->notifyEventStat.notifyEventHoldElems)); } int32_t numOfVgs = taosArrayGetSize(pReq->pUpdateNodes); @@ -596,6 +610,20 @@ int32_t tDecodeStreamHbMsg(SDecoder* pDecoder, SStreamHbMsg* pReq) { TAOS_CHECK_EXIT(tDecodeI64(pDecoder, &entry.startCheckpointVer)); TAOS_CHECK_EXIT(tDecodeI64(pDecoder, &entry.hTaskId)); + TAOS_CHECK_EXIT(tDecodeI64(pDecoder, &entry.notifyEventStat.notifyEventAddTimes)); + TAOS_CHECK_EXIT(tDecodeI64(pDecoder, &entry.notifyEventStat.notifyEventAddElems)); + TAOS_CHECK_EXIT(tDecodeDouble(pDecoder, &entry.notifyEventStat.notifyEventAddCostSec)); + TAOS_CHECK_EXIT(tDecodeI64(pDecoder, &entry.notifyEventStat.notifyEventPushTimes)); + TAOS_CHECK_EXIT(tDecodeI64(pDecoder, &entry.notifyEventStat.notifyEventPushElems)); + TAOS_CHECK_EXIT(tDecodeDouble(pDecoder, &entry.notifyEventStat.notifyEventPushCostSec)); + TAOS_CHECK_EXIT(tDecodeI64(pDecoder, &entry.notifyEventStat.notifyEventPackTimes)); + TAOS_CHECK_EXIT(tDecodeI64(pDecoder, &entry.notifyEventStat.notifyEventPackElems)); + TAOS_CHECK_EXIT(tDecodeDouble(pDecoder, &entry.notifyEventStat.notifyEventPackCostSec)); + TAOS_CHECK_EXIT(tDecodeI64(pDecoder, &entry.notifyEventStat.notifyEventSendTimes)); + TAOS_CHECK_EXIT(tDecodeI64(pDecoder, &entry.notifyEventStat.notifyEventSendElems)); + TAOS_CHECK_EXIT(tDecodeDouble(pDecoder, &entry.notifyEventStat.notifyEventSendCostSec)); + TAOS_CHECK_EXIT(tDecodeI64(pDecoder, &entry.notifyEventStat.notifyEventHoldElems)); + entry.id.taskId = taskId; if (taosArrayPush(pReq->pTaskStatus, &entry) == NULL) { TAOS_CHECK_EXIT(terrno); @@ -837,4 +865,4 @@ int32_t tDecodeStreamTaskRunReq(SDecoder* pDecoder, SStreamTaskRunReq* pReq) { _exit: return code; -} \ No newline at end of file +} diff --git a/source/common/src/systable.c b/source/common/src/systable.c index 4deb1bba24..3699c05713 100644 --- a/source/common/src/systable.c +++ b/source/common/src/systable.c @@ -212,6 +212,7 @@ static const SSysDbTableSchema streamTaskSchema[] = { {.name = "extra_info", .bytes = 25 + VARSTR_HEADER_SIZE, .type = TSDB_DATA_TYPE_VARCHAR, .sysInfo = false}, {.name = "history_task_id", .bytes = 16 + VARSTR_HEADER_SIZE, .type = TSDB_DATA_TYPE_VARCHAR, .sysInfo = false}, {.name = "history_task_status", .bytes = 12 + VARSTR_HEADER_SIZE, .type = TSDB_DATA_TYPE_VARCHAR, .sysInfo = false}, + {.name = "notify_event_stat", .bytes = TSDB_STREAM_NOTIFY_STAT_LEN + VARSTR_HEADER_SIZE, .type = TSDB_DATA_TYPE_VARCHAR, .sysInfo = false}, }; static const SSysDbTableSchema userTblsSchema[] = { diff --git a/source/common/src/tdatablock.c b/source/common/src/tdatablock.c index c3e0fff578..cb305cb6c7 100644 --- a/source/common/src/tdatablock.c +++ b/source/common/src/tdatablock.c @@ -165,6 +165,55 @@ int32_t colDataSetValOrCover(SColumnInfoData* pColumnInfoData, uint32_t rowIndex return colDataSetValHelp(pColumnInfoData, rowIndex, pData, isNull); } +int32_t varColSetVarData(SColumnInfoData* pColumnInfoData, uint32_t rowIndex, const char* pVarData, int32_t varDataLen, + bool isNull) { + if (!IS_VAR_DATA_TYPE(pColumnInfoData->info.type)) { + return TSDB_CODE_INVALID_PARA; + } + + if (isNull || pVarData == NULL) { + pColumnInfoData->varmeta.offset[rowIndex] = -1; // it is a null value of VAR type. + pColumnInfoData->hasNull = true; + return TSDB_CODE_SUCCESS; + } + + int32_t dataLen = VARSTR_HEADER_SIZE + varDataLen; + if (pColumnInfoData->varmeta.offset[rowIndex] > 0) { + pColumnInfoData->varmeta.length = pColumnInfoData->varmeta.offset[rowIndex]; + } + + SVarColAttr* pAttr = &pColumnInfoData->varmeta; + if (pAttr->allocLen < pAttr->length + dataLen) { + uint32_t newSize = pAttr->allocLen; + if (newSize <= 1) { + newSize = 8; + } + + while (newSize < pAttr->length + dataLen) { + newSize = newSize * 1.5; + if (newSize > UINT32_MAX) { + return TSDB_CODE_OUT_OF_MEMORY; + } + } + + char* buf = taosMemoryRealloc(pColumnInfoData->pData, newSize); + if (buf == NULL) { + return terrno; + } + + pColumnInfoData->pData = buf; + pAttr->allocLen = newSize; + } + + uint32_t len = pColumnInfoData->varmeta.length; + pColumnInfoData->varmeta.offset[rowIndex] = len; + + (void)memmove(varDataVal(pColumnInfoData->pData + len), pVarData, varDataLen); + varDataSetLen(pColumnInfoData->pData + len, varDataLen); + pColumnInfoData->varmeta.length += dataLen; + return TSDB_CODE_SUCCESS; +} + int32_t colDataReassignVal(SColumnInfoData* pColumnInfoData, uint32_t dstRowIdx, uint32_t srcRowIdx, const char* pData) { int32_t type = pColumnInfoData->info.type; diff --git a/source/dnode/mnode/impl/src/mndStream.c b/source/dnode/mnode/impl/src/mndStream.c index 65de7be704..b752d35b0a 100644 --- a/source/dnode/mnode/impl/src/mndStream.c +++ b/source/dnode/mnode/impl/src/mndStream.c @@ -765,7 +765,7 @@ static int32_t addStreamTaskNotifyInfo(const SCMCreateStreamReq *createReq, cons TSDB_CHECK_NULL(pTask->notifyInfo.pNotifyAddrUrls, code, lino, _end, terrno); pTask->notifyInfo.notifyEventTypes = createReq->notifyEventTypes; pTask->notifyInfo.notifyErrorHandle = createReq->notifyErrorHandle; - pTask->notifyInfo.streamName = taosStrdup(createReq->name); + pTask->notifyInfo.streamName = taosStrdup(mndGetDbStr(createReq->name)); TSDB_CHECK_NULL(pTask->notifyInfo.streamName, code, lino, _end, terrno); pTask->notifyInfo.stbFullName = taosStrdup(createReq->targetStbFullName); TSDB_CHECK_NULL(pTask->notifyInfo.stbFullName, code, lino, _end, terrno); diff --git a/source/dnode/mnode/impl/src/mndStreamUtil.c b/source/dnode/mnode/impl/src/mndStreamUtil.c index d896434f3b..8703ff96aa 100644 --- a/source/dnode/mnode/impl/src/mndStreamUtil.c +++ b/source/dnode/mnode/impl/src/mndStreamUtil.c @@ -1309,8 +1309,8 @@ int32_t setTaskAttrInResBlock(SStreamObj *pStream, SStreamTask *pTask, SSDataBlo TSDB_CHECK_CODE(code, lino, _end); // input queue - char vbuf[40] = {0}; - char buf[38] = {0}; + char vbuf[TSDB_STREAM_NOTIFY_STAT_LEN + 2] = {0}; + char buf[TSDB_STREAM_NOTIFY_STAT_LEN] = {0}; const char *queueInfoStr = "%4.2f MiB (%6.2f%)"; snprintf(buf, tListLen(buf), queueInfoStr, pe->inputQUsed, pe->inputRate); STR_TO_VARSTR(vbuf, buf); @@ -1503,6 +1503,47 @@ int32_t setTaskAttrInResBlock(SStreamObj *pStream, SStreamTask *pTask, SSDataBlo code = colDataSetVal(pColInfo, numOfRows, 0, true); TSDB_CHECK_CODE(code, lino, _end); + // notify_event_stat + int32_t offset =0; + if (pe->notifyEventStat.notifyEventAddTimes > 0) { + offset += tsnprintf(buf + offset, sizeof(buf) - offset, "Add %" PRId64 "x, %" PRId64 " elems in %lfs; ", + pe->notifyEventStat.notifyEventAddTimes, pe->notifyEventStat.notifyEventAddElems, + pe->notifyEventStat.notifyEventAddCostSec); + } + if (pe->notifyEventStat.notifyEventPushTimes > 0) { + offset += tsnprintf(buf + offset, sizeof(buf) - offset, "Push %" PRId64 "x, %" PRId64 " elems in %lfs; ", + pe->notifyEventStat.notifyEventPushTimes, pe->notifyEventStat.notifyEventPushElems, + pe->notifyEventStat.notifyEventPushCostSec); + } + if (pe->notifyEventStat.notifyEventPackTimes > 0) { + offset += tsnprintf(buf + offset, sizeof(buf) - offset, "Pack %" PRId64 "x, %" PRId64 " elems in %lfs; ", + pe->notifyEventStat.notifyEventPackTimes, pe->notifyEventStat.notifyEventPackElems, + pe->notifyEventStat.notifyEventPackCostSec); + } + if (pe->notifyEventStat.notifyEventSendTimes > 0) { + offset += tsnprintf(buf + offset, sizeof(buf) - offset, "Send %" PRId64 "x, %" PRId64 " elems in %lfs; ", + pe->notifyEventStat.notifyEventSendTimes, pe->notifyEventStat.notifyEventSendElems, + pe->notifyEventStat.notifyEventSendCostSec); + } + if (pe->notifyEventStat.notifyEventHoldElems > 0) { + offset += tsnprintf(buf + offset, sizeof(buf) - offset, "[Hold %" PRId64 " elems] ", + pe->notifyEventStat.notifyEventHoldElems); + } + TSDB_CHECK_CONDITION(offset < sizeof(buf), code, lino, _end, TSDB_CODE_INTERNAL_ERROR); + buf[offset] = '\0'; + + STR_TO_VARSTR(vbuf, buf); + + pColInfo = taosArrayGet(pBlock->pDataBlock, cols++); + TSDB_CHECK_NULL(pColInfo, code, lino, _end, terrno); + + if (offset == 0) { + colDataSetNULL(pColInfo, numOfRows); + } else { + code = colDataSetVal(pColInfo, numOfRows, (const char *)vbuf, false); + TSDB_CHECK_CODE(code, lino, _end); + } + _end: if (code) { mError("error happens during build task attr result blocks, lino:%d, code:%s", lino, tstrerror(code)); @@ -1689,4 +1730,4 @@ int32_t mndCheckForSnode(SMnode *pMnode, SDbObj *pSrcDb) { mError("snode not existed when trying to create stream in db with multiple replica"); return TSDB_CODE_SNODE_NOT_DEPLOYED; } -} \ No newline at end of file +} diff --git a/source/dnode/snode/src/snodeInitApi.c b/source/dnode/snode/src/snodeInitApi.c index 68dc981338..54ec15a558 100644 --- a/source/dnode/snode/src/snodeInitApi.c +++ b/source/dnode/snode/src/snodeInitApi.c @@ -97,6 +97,7 @@ void initStateStoreAPI(SStateStore* pStore) { pStore->updateInfoSerialize = updateInfoSerialize; pStore->updateInfoDeserialize = updateInfoDeserialize; + pStore->streamStateSessionSeekKeyPrev = streamStateSessionSeekKeyPrev; pStore->streamStateSessionSeekKeyNext = streamStateSessionSeekKeyNext; pStore->streamStateCountSeekKeyPrev = streamStateCountSeekKeyPrev; pStore->streamStateSessionSeekKeyCurrentPrev = streamStateSessionSeekKeyCurrentPrev; @@ -123,4 +124,4 @@ void initStateStoreAPI(SStateStore* pStore) { void initFunctionStateStore(SFunctionStateStore* pStore) { pStore->streamStateFuncPut = streamStateFuncPut; pStore->streamStateFuncGet = streamStateFuncGet; -} \ No newline at end of file +} diff --git a/source/dnode/vnode/src/tq/tqStreamNotify.c b/source/dnode/vnode/src/tq/tqStreamNotify.c index 46ee95d3b9..61200b189b 100644 --- a/source/dnode/vnode/src/tq/tqStreamNotify.c +++ b/source/dnode/vnode/src/tq/tqStreamNotify.c @@ -20,14 +20,16 @@ #include "curl/curl.h" #endif -#define STREAM_EVENT_NOTIFY_RETRY_MS 50 // 50ms +#define STREAM_EVENT_NOTIFY_RETRY_MS 50 // 50 ms +#define STREAM_EVENT_NOTIFY_MESSAAGE_SIZE_KB 8 * 1024 // 8 MB +#define STREAM_EVENT_NOTIFY_FRAME_SIZE 256 * 1024 // 256 KB typedef struct SStreamNotifyHandle { TdThreadMutex mutex; #ifndef WINDOWS - CURL* curl; + CURL* curl; #endif - char* url; + char* url; } SStreamNotifyHandle; struct SStreamNotifyHandleMap { @@ -49,6 +51,7 @@ static void stopStreamNotifyConn(SStreamNotifyHandle* pHandle) { } // TODO: add wait mechanism for peer connection close response curl_easy_cleanup(pHandle->curl); + pHandle->curl = NULL; #endif } @@ -258,7 +261,8 @@ _end: } static int32_t packupStreamNotifyEvent(const char* streamName, const SArray* pBlocks, char** pMsg, - int32_t* nNotifyEvents) { + int32_t* nNotifyEvents, STaskNotifyEventStat* pNotifyEventStat, + int32_t* pBlockIdx) { int32_t code = TSDB_CODE_SUCCESS; int32_t lino = 0; int32_t numOfBlocks = 0; @@ -268,15 +272,20 @@ static int32_t packupStreamNotifyEvent(const char* streamName, const SArray* pBl char* msgHeader = NULL; const char* msgTail = "]}]}"; char* msg = NULL; + int64_t startTime = 0; + int64_t endTime = 0; + int32_t nBlocks = 0; TSDB_CHECK_NULL(pMsg, code, lino, _end, TSDB_CODE_INVALID_PARA); + TSDB_CHECK_NULL(pNotifyEventStat, code, lino, _end, TSDB_CODE_INVALID_PARA); *pMsg = NULL; numOfBlocks = taosArrayGetSize(pBlocks); *nNotifyEvents = 0; - for (int32_t i = 0; i < numOfBlocks; ++i) { + for (int32_t i = *pBlockIdx; i < numOfBlocks; ++i) { SSDataBlock* pDataBlock = taosArrayGet(pBlocks, i); + nBlocks++; if (pDataBlock == NULL || pDataBlock->info.type != STREAM_NOTIFY_EVENT) { continue; } @@ -287,13 +296,19 @@ static int32_t packupStreamNotifyEvent(const char* streamName, const SArray* pBl msgLen += varDataLen(val) + 1; } *nNotifyEvents += pDataBlock->info.rows; + if (msgLen >= STREAM_EVENT_NOTIFY_MESSAAGE_SIZE_KB * 1024) { + break; + } } + *pBlockIdx += nBlocks; + if (msgLen == 0) { // skip since no notification events found goto _end; } + startTime = taosGetMonoTimestampMs(); code = getStreamNotifyEventHeader(streamName, &msgHeader); TSDB_CHECK_CODE(code, lino, _end); msgHeaderLen = strlen(msgHeader); @@ -306,7 +321,7 @@ static int32_t packupStreamNotifyEvent(const char* streamName, const SArray* pBl TAOS_STRNCPY(p, msgHeader, msgHeaderLen); p += msgHeaderLen - msgTailLen; - for (int32_t i = 0; i < numOfBlocks; ++i) { + for (int32_t i = *pBlockIdx - nBlocks; i < *pBlockIdx; ++i) { SSDataBlock* pDataBlock = taosArrayGet(pBlocks, i); if (pDataBlock == NULL || pDataBlock->info.type != STREAM_NOTIFY_EVENT) { continue; @@ -328,6 +343,11 @@ static int32_t packupStreamNotifyEvent(const char* streamName, const SArray* pBl *pMsg = msg; msg = NULL; + endTime = taosGetMonoTimestampMs(); + pNotifyEventStat->notifyEventPackTimes++; + pNotifyEventStat->notifyEventPackElems += *nNotifyEvents; + pNotifyEventStat->notifyEventPackCostSec += (endTime - startTime) / 1000.0; + _end: if (code != TSDB_CODE_SUCCESS) { tqError("%s failed at line %d since %s", __func__, lino, tstrerror(code)); @@ -354,9 +374,21 @@ static int32_t sendSingleStreamNotify(SStreamNotifyHandle* pHandle, char* msg) { TSDB_CHECK_NULL(pHandle->curl, code, lino, _end, TSDB_CODE_INVALID_PARA); totalLen = strlen(msg); - while (sentLen < totalLen) { - res = curl_ws_send(pHandle->curl, msg + sentLen, totalLen - sentLen, &nbytes, 0, CURLWS_TEXT); + if (totalLen > 0) { + // send PING frame to check if the connection is still alive + res = curl_ws_send(pHandle->curl, "", 0, (size_t*)&sentLen, 0, CURLWS_PING); TSDB_CHECK_CONDITION(res == CURLE_OK, code, lino, _end, TSDB_CODE_FAILED); + } + sentLen = 0; + while (sentLen < totalLen) { + size_t chunkSize = TMIN(totalLen - sentLen, STREAM_EVENT_NOTIFY_FRAME_SIZE); + if (sentLen == 0) { + res = curl_ws_send(pHandle->curl, msg, chunkSize, &nbytes, totalLen, CURLWS_TEXT | CURLWS_OFFSET); + TSDB_CHECK_CONDITION(res == CURLE_OK, code, lino, _end, TSDB_CODE_FAILED); + } else { + res = curl_ws_send(pHandle->curl, msg + sentLen, chunkSize, &nbytes, 0, CURLWS_TEXT | CURLWS_OFFSET); + TSDB_CHECK_CONDITION(res == CURLE_OK, code, lino, _end, TSDB_CODE_FAILED); + } sentLen += nbytes; } @@ -379,6 +411,9 @@ int32_t tqSendAllNotifyEvents(const SArray* pBlocks, SStreamTask* pTask, SVnode* int32_t nNotifyAddr = 0; int32_t nNotifyEvents = 0; SStreamNotifyHandle* pHandle = NULL; + int64_t startTime = 0; + int64_t endTime = 0; + int32_t blockIdx = 0; TSDB_CHECK_NULL(pTask, code, lino, _end, TSDB_CODE_INVALID_PARA); TSDB_CHECK_NULL(pVnode, code, lino, _end, TSDB_CODE_INVALID_PARA); @@ -388,50 +423,61 @@ int32_t tqSendAllNotifyEvents(const SArray* pBlocks, SStreamTask* pTask, SVnode* goto _end; } - code = packupStreamNotifyEvent(pTask->notifyInfo.streamName, pBlocks, &msg, &nNotifyEvents); - TSDB_CHECK_CODE(code, lino, _end); - if (msg == NULL) { - goto _end; - } + while (blockIdx < taosArrayGetSize(pBlocks)) { + code = packupStreamNotifyEvent(pTask->notifyInfo.streamName, pBlocks, &msg, &nNotifyEvents, &pTask->notifyEventStat, + &blockIdx); + TSDB_CHECK_CODE(code, lino, _end); + if (msg == NULL) { + continue; + } - tqDebug("stream task %s prepare to send %d notify events, total msg length: %" PRIu64, pTask->notifyInfo.streamName, - nNotifyEvents, (uint64_t)strlen(msg)); + tqDebug("stream task %s prepare to send %d notify events, total msg length: %" PRIu64, pTask->notifyInfo.streamName, + nNotifyEvents, (uint64_t)strlen(msg)); - for (int32_t i = 0; i < nNotifyAddr; ++i) { - if (streamTaskShouldStop(pTask)) { - break; - } - const char* url = taosArrayGetP(pTask->notifyInfo.pNotifyAddrUrls, i); - code = acquireStreamNotifyHandle(pVnode->pNotifyHandleMap, url, &pHandle); - if (code != TSDB_CODE_SUCCESS) { - tqError("failed to get stream notify handle of %s", url); - if (pTask->notifyInfo.notifyErrorHandle == SNOTIFY_ERROR_HANDLE_PAUSE) { - // retry for event message sending in PAUSE error handling mode - taosMsleep(STREAM_EVENT_NOTIFY_RETRY_MS); - --i; - continue; - } else { - // simply ignore the failure in DROP error handling mode - code = TSDB_CODE_SUCCESS; - continue; + startTime = taosGetMonoTimestampMs(); + for (int32_t i = 0; i < nNotifyAddr; ++i) { + if (streamTaskShouldStop(pTask)) { + break; } - } - code = sendSingleStreamNotify(pHandle, msg); - if (code != TSDB_CODE_SUCCESS) { - tqError("failed to send stream notify handle to %s since %s", url, tstrerror(code)); - if (pTask->notifyInfo.notifyErrorHandle == SNOTIFY_ERROR_HANDLE_PAUSE) { - // retry for event message sending in PAUSE error handling mode - taosMsleep(STREAM_EVENT_NOTIFY_RETRY_MS); - --i; - } else { - // simply ignore the failure in DROP error handling mode - code = TSDB_CODE_SUCCESS; + const char* url = taosArrayGetP(pTask->notifyInfo.pNotifyAddrUrls, i); + code = acquireStreamNotifyHandle(pVnode->pNotifyHandleMap, url, &pHandle); + if (code != TSDB_CODE_SUCCESS) { + tqError("failed to get stream notify handle of %s", url); + if (pTask->notifyInfo.notifyErrorHandle == SNOTIFY_ERROR_HANDLE_PAUSE) { + // retry for event message sending in PAUSE error handling mode + taosMsleep(STREAM_EVENT_NOTIFY_RETRY_MS); + --i; + continue; + } else { + // simply ignore the failure in DROP error handling mode + code = TSDB_CODE_SUCCESS; + continue; + } } - } else { - tqDebug("stream task %s send %d notify events to %s successfully", pTask->notifyInfo.streamName, nNotifyEvents, - url); + code = sendSingleStreamNotify(pHandle, msg); + if (code != TSDB_CODE_SUCCESS) { + tqError("failed to send stream notify handle to %s since %s", url, tstrerror(code)); + if (pTask->notifyInfo.notifyErrorHandle == SNOTIFY_ERROR_HANDLE_PAUSE) { + // retry for event message sending in PAUSE error handling mode + taosMsleep(STREAM_EVENT_NOTIFY_RETRY_MS); + --i; + } else { + // simply ignore the failure in DROP error handling mode + code = TSDB_CODE_SUCCESS; + } + } else { + tqDebug("stream task %s send %d notify events to %s successfully", pTask->notifyInfo.streamName, nNotifyEvents, + url); + } + releaseStreamNotifyHandle(&pHandle); } - releaseStreamNotifyHandle(&pHandle); + + endTime = taosGetMonoTimestampMs(); + pTask->notifyEventStat.notifyEventSendTimes++; + pTask->notifyEventStat.notifyEventSendElems += nNotifyEvents; + pTask->notifyEventStat.notifyEventSendCostSec += (endTime - startTime) / 1000.0; + + taosMemoryFreeClear(msg); } _end: diff --git a/source/dnode/vnode/src/tqCommon/tqCommon.c b/source/dnode/vnode/src/tqCommon/tqCommon.c index a73d7f849c..90381f4ad2 100644 --- a/source/dnode/vnode/src/tqCommon/tqCommon.c +++ b/source/dnode/vnode/src/tqCommon/tqCommon.c @@ -87,9 +87,9 @@ int32_t tqExpandStreamTask(SStreamTask* pTask) { return code; } - code = - qSetStreamNotifyInfo(pTask->exec.pExecutor, pTask->notifyInfo.notifyEventTypes, - pTask->notifyInfo.pSchemaWrapper, pTask->notifyInfo.stbFullName, IS_NEW_SUBTB_RULE(pTask)); + code = qSetStreamNotifyInfo(pTask->exec.pExecutor, pTask->notifyInfo.notifyEventTypes, + pTask->notifyInfo.pSchemaWrapper, pTask->notifyInfo.stbFullName, + IS_NEW_SUBTB_RULE(pTask), &pTask->notifyEventStat); if (code) { tqError("s-task:%s failed to set stream notify info, code:%s", pTask->id.idStr, tstrerror(code)); return code; diff --git a/source/dnode/vnode/src/vnd/vnodeInitApi.c b/source/dnode/vnode/src/vnd/vnodeInitApi.c index b8682028cf..b29d9add1b 100644 --- a/source/dnode/vnode/src/vnd/vnodeInitApi.c +++ b/source/dnode/vnode/src/vnd/vnodeInitApi.c @@ -223,6 +223,7 @@ void initStateStoreAPI(SStateStore* pStore) { pStore->updateInfoSerialize = updateInfoSerialize; pStore->updateInfoDeserialize = updateInfoDeserialize; + pStore->streamStateSessionSeekKeyPrev = streamStateSessionSeekKeyPrev; pStore->streamStateSessionSeekKeyNext = streamStateSessionSeekKeyNext; pStore->streamStateCountSeekKeyPrev = streamStateCountSeekKeyPrev; pStore->streamStateSessionSeekKeyCurrentPrev = streamStateSessionSeekKeyCurrentPrev; diff --git a/source/libs/executor/inc/executorInt.h b/source/libs/executor/inc/executorInt.h index 84eba69acb..e7bc1f67e1 100644 --- a/source/libs/executor/inc/executorInt.h +++ b/source/libs/executor/inc/executorInt.h @@ -450,16 +450,17 @@ typedef struct STimeWindowAggSupp { } STimeWindowAggSupp; typedef struct SStreamNotifyEventSupp { - SArray* pWindowEvents; // Array of SStreamNotifyEvent, storing window events and trigger values. - SHashObj* pTableNameHashMap; // Hash map from groupid to the dest child table name. - SHashObj* pResultHashMap; // Hash map from groupid+skey to the window agg result. - SSDataBlock* pEventBlock; // The datablock contains all window events and results. + SHashObj* pWindowEventHashMap; // Hash map from gorupid+skey+eventType to the list node of window event. + SHashObj* pTableNameHashMap; // Hash map from groupid to the dest child table name. + SSDataBlock* pEventBlock; // The datablock contains all window events and results. + SArray* pSessionKeys; + const char* windowType; } SStreamNotifyEventSupp; typedef struct SSteamOpBasicInfo { int32_t primaryPkIndex; bool updateOperatorInfo; - SStreamNotifyEventSupp windowEventSup; + SStreamNotifyEventSupp notifyEventSup; } SSteamOpBasicInfo; typedef struct SStreamFillSupporter { @@ -1053,7 +1054,7 @@ int32_t saveDeleteRes(SSHashObj* pStDelete, SSessionKey key); void removeSessionResult(SStreamAggSupporter* pAggSup, SSHashObj* pHashMap, SSHashObj* pResMap, SSessionKey* pKey); void doBuildDeleteDataBlock(struct SOperatorInfo* pOp, SSHashObj* pStDeleted, SSDataBlock* pBlock, void** Ite); void doBuildSessionResult(struct SOperatorInfo* pOperator, void* pState, SGroupResInfo* pGroupResInfo, - SSDataBlock* pBlock); + SSDataBlock* pBlock, SArray* pSessionKeys); int32_t getSessionWindowInfoByKey(SStreamAggSupporter* pAggSup, SSessionKey* pKey, SResultWindowInfo* pWinInfo); void getNextSessionWinInfo(SStreamAggSupporter* pAggSup, SSHashObj* pStUpdated, SResultWindowInfo* pCurWin, SResultWindowInfo* pNextWin); @@ -1090,7 +1091,7 @@ void freeResetOperatorParams(struct SOperatorInfo* pOperator, SOperatorParamT int32_t getNextBlockFromDownstreamImpl(struct SOperatorInfo* pOperator, int32_t idx, bool clearParam, SSDataBlock** pResBlock); void getCountWinRange(SStreamAggSupporter* pAggSup, const SSessionKey* pKey, EStreamType mode, SSessionKey* pDelRange); -void doDeleteSessionWindow(SStreamAggSupporter* pAggSup, SSessionKey* pKey); +void doDeleteSessionWindow(SStreamAggSupporter* pAggSup, SSessionKey* pKey); int32_t saveDeleteInfo(SArray* pWins, SSessionKey key); void removeSessionResults(SStreamAggSupporter* pAggSup, SSHashObj* pHashMap, SArray* pWins); diff --git a/source/libs/executor/inc/querytask.h b/source/libs/executor/inc/querytask.h index 86ee6f4124..7e621e3df5 100644 --- a/source/libs/executor/inc/querytask.h +++ b/source/libs/executor/inc/querytask.h @@ -18,9 +18,9 @@ #ifdef __cplusplus extern "C" { -#endif - -#include "executorInt.h" + #endif + + #include "executorInt.h" #define GET_TASKID(_t) (((SExecTaskInfo*)(_t))->id.str) @@ -59,22 +59,23 @@ typedef struct STaskStopInfo { } STaskStopInfo; typedef struct { - STqOffsetVal currentOffset; // for tmq - SMqBatchMetaRsp btMetaRsp; // for tmq fetching meta - int8_t sourceExcluded; - int64_t snapshotVer; - SSchemaWrapper* schema; - char tbName[TSDB_TABLE_NAME_LEN]; // this is the current scan table: todo refactor - int8_t recoverStep; - int8_t recoverScanFinished; - SQueryTableDataCond tableCond; - SVersionRange fillHistoryVer; - STimeWindow fillHistoryWindow; - SStreamState* pState; - int32_t eventTypes; // event types to notify - SSchemaWrapper* notifyResultSchema; // agg result to notify - char* stbFullName; // used to generate dest child table name - bool newSubTableRule; // used to generate dest child table name + STqOffsetVal currentOffset; // for tmq + SMqBatchMetaRsp btMetaRsp; // for tmq fetching meta + int8_t sourceExcluded; + int64_t snapshotVer; + SSchemaWrapper* schema; + char tbName[TSDB_TABLE_NAME_LEN]; // this is the current scan table: todo refactor + int8_t recoverStep; + int8_t recoverScanFinished; + SQueryTableDataCond tableCond; + SVersionRange fillHistoryVer; + STimeWindow fillHistoryWindow; + SStreamState* pState; + int32_t eventTypes; // event types to notify + SSchemaWrapper* notifyResultSchema; // agg result to notify + char* stbFullName; // used to generate dest child table name + bool newSubTableRule; // used to generate dest child table name + STaskNotifyEventStat* pNotifyEventStat; // used to store notify event statistics } SStreamTaskInfo; struct SExecTaskInfo { diff --git a/source/libs/executor/inc/streamexecutorInt.h b/source/libs/executor/inc/streamexecutorInt.h index 7b3c828351..3195b2b67d 100644 --- a/source/libs/executor/inc/streamexecutorInt.h +++ b/source/libs/executor/inc/streamexecutorInt.h @@ -30,20 +30,31 @@ extern "C" { #define FILL_POS_MID 2 #define FILL_POS_END 3 -#define HAS_NON_ROW_DATA(pRowData) (pRowData->key == INT64_MIN) -#define HAS_ROW_DATA(pRowData) (pRowData && pRowData->key != INT64_MIN) +#define HAS_NON_ROW_DATA(pRowData) (pRowData->key == INT64_MIN) +#define HAS_ROW_DATA(pRowData) (pRowData && pRowData->key != INT64_MIN) -#define IS_INVALID_WIN_KEY(ts) ((ts) == INT64_MIN) -#define IS_VALID_WIN_KEY(ts) ((ts) != INT64_MIN) -#define SET_WIN_KEY_INVALID(ts) ((ts) = INT64_MIN) +#define IS_INVALID_WIN_KEY(ts) ((ts) == INT64_MIN) +#define IS_VALID_WIN_KEY(ts) ((ts) != INT64_MIN) +#define SET_WIN_KEY_INVALID(ts) ((ts) = INT64_MIN) #define IS_NORMAL_INTERVAL_OP(op) \ ((op)->operatorType == QUERY_NODE_PHYSICAL_PLAN_STREAM_INTERVAL || \ (op)->operatorType == QUERY_NODE_PHYSICAL_PLAN_STREAM_FINAL_INTERVAL) +#define IS_NORMAL_SESSION_OP(op) \ + ((op)->operatorType == QUERY_NODE_PHYSICAL_PLAN_STREAM_SESSION || \ + (op)->operatorType == QUERY_NODE_PHYSICAL_PLAN_STREAM_FINAL_SESSION) + +#define IS_NORMAL_STATE_OP(op) ((op)->operatorType == QUERY_NODE_PHYSICAL_PLAN_STREAM_STATE) + +#define IS_NORMAL_EVENT_OP(op) ((op)->operatorType == QUERY_NODE_PHYSICAL_PLAN_STREAM_EVENT) + +#define IS_NORMAL_COUNT_OP(op) ((op)->operatorType == QUERY_NODE_PHYSICAL_PLAN_STREAM_COUNT) + #define IS_CONTINUE_INTERVAL_OP(op) ((op)->operatorType == QUERY_NODE_PHYSICAL_PLAN_STREAM_CONTINUE_INTERVAL) -#define IS_FILL_CONST_VALUE(type) ((type == TSDB_FILL_NULL || type == TSDB_FILL_NULL_F || type == TSDB_FILL_SET_VALUE || type == TSDB_FILL_SET_VALUE_F)) +#define IS_FILL_CONST_VALUE(type) \ + ((type == TSDB_FILL_NULL || type == TSDB_FILL_NULL_F || type == TSDB_FILL_SET_VALUE || type == TSDB_FILL_SET_VALUE_F)) typedef struct SSliceRowData { TSKEY key; @@ -57,11 +68,13 @@ typedef struct SSlicePoint { SRowBuffPos* pResPos; } SSlicePoint; -void setStreamOperatorState(SSteamOpBasicInfo* pBasicInfo, EStreamType type); -bool needSaveStreamOperatorInfo(SSteamOpBasicInfo* pBasicInfo); -void saveStreamOperatorStateComplete(SSteamOpBasicInfo* pBasicInfo); -int32_t initStreamBasicInfo(SSteamOpBasicInfo* pBasicInfo); -void destroyStreamBasicInfo(SSteamOpBasicInfo* pBasicInfo); +void setStreamOperatorState(SSteamOpBasicInfo* pBasicInfo, EStreamType type); +bool needSaveStreamOperatorInfo(SSteamOpBasicInfo* pBasicInfo); +void saveStreamOperatorStateComplete(SSteamOpBasicInfo* pBasicInfo); +int32_t initStreamBasicInfo(SSteamOpBasicInfo* pBasicInfo, const struct SOperatorInfo* pOperator); +void destroyStreamBasicInfo(SSteamOpBasicInfo* pBasicInfo); +int32_t encodeStreamBasicInfo(void** buf, const SSteamOpBasicInfo* pBasicInfo); +int32_t decodeStreamBasicInfo(void** buf, SSteamOpBasicInfo* pBasicInfo); int64_t getDeleteMarkFromOption(SStreamNodeOption* pOption); void removeDeleteResults(SSHashObj* pUpdatedMap, SArray* pDelWins); @@ -98,7 +111,7 @@ SResultCellData* getSliceResultCell(SResultCellData* pRowVal, int32_t index, int int32_t getDownstreamRes(struct SOperatorInfo* downstream, SSDataBlock** ppRes, SColumnInfo** ppPkCol); void destroyFlusedppPos(void* ppRes); void doBuildStreamIntervalResult(struct SOperatorInfo* pOperator, void* pState, SSDataBlock* pBlock, - SGroupResInfo* pGroupResInfo); + SGroupResInfo* pGroupResInfo, SArray* pSessionKeys); void transBlockToSliceResultRow(const SSDataBlock* pBlock, int32_t rowId, TSKEY ts, SSliceRowData* pRowVal, int32_t rowSize, void* pPkData, SColumnInfoData* pPkCol, int32_t* pCellOffsetInfo); int32_t getQualifiedRowNumDesc(SExprSupp* pExprSup, SSDataBlock* pBlock, TSKEY* tsCols, int32_t rowId, bool ignoreNull); @@ -112,10 +125,25 @@ TSKEY compareTs(void* pKey); int32_t addEventAggNotifyEvent(EStreamNotifyEventType eventType, const SSessionKey* pSessionKey, const SSDataBlock* pInputBlock, const SNodeList* pCondCols, int32_t ri, - SStreamNotifyEventSupp* sup); -int32_t addAggResultNotifyEvent(const SSDataBlock* pResultBlock, const SSchemaWrapper* pSchemaWrapper, - SStreamNotifyEventSupp* sup); -int32_t buildNotifyEventBlock(const SExecTaskInfo* pTaskInfo, SStreamNotifyEventSupp* sup); + SStreamNotifyEventSupp* sup, STaskNotifyEventStat* pNotifyEventStat); +int32_t addStateAggNotifyEvent(EStreamNotifyEventType eventType, const SSessionKey* pSessionKey, + const SStateKeys* pCurState, const SStateKeys* pAnotherState, bool onlyUpdate, + SStreamNotifyEventSupp* sup, STaskNotifyEventStat* pNotifyEventStat); +int32_t addIntervalAggNotifyEvent(EStreamNotifyEventType eventType, const SSessionKey* pSessionKey, + SStreamNotifyEventSupp* sup, STaskNotifyEventStat* pNotifyEventStat); +int32_t addSessionAggNotifyEvent(EStreamNotifyEventType eventType, const SSessionKey* pSessionKey, + SStreamNotifyEventSupp* sup, STaskNotifyEventStat* pNotifyEventStat); +int32_t addCountAggNotifyEvent(EStreamNotifyEventType eventType, const SSessionKey* pSessionKey, + SStreamNotifyEventSupp* sup, STaskNotifyEventStat* pNotifyEventStat); +int32_t addAggResultNotifyEvent(const SSDataBlock* pResultBlock, const SArray* pSessionKeys, + const SSchemaWrapper* pSchemaWrapper, SStreamNotifyEventSupp* sup, + STaskNotifyEventStat* pNotifyEventStat); +int32_t addAggDeleteNotifyEvent(const SSDataBlock* pDeleteBlock, SStreamNotifyEventSupp* sup, + STaskNotifyEventStat* pNotifyEventStat); +int32_t buildNotifyEventBlock(const SExecTaskInfo* pTaskInfo, SStreamNotifyEventSupp* sup, + STaskNotifyEventStat* pNotifyEventStat); +int32_t removeOutdatedNotifyEvents(STimeWindowAggSupp* pTwSup, SStreamNotifyEventSupp* sup, + STaskNotifyEventStat* pNotifyEventStat); #ifdef __cplusplus } diff --git a/source/libs/executor/src/executor.c b/source/libs/executor/src/executor.c index 39bef9c95f..2a6b77c53f 100644 --- a/source/libs/executor/src/executor.c +++ b/source/libs/executor/src/executor.c @@ -251,7 +251,7 @@ int32_t qSetStreamOpOpen(qTaskInfo_t tinfo) { } int32_t qSetStreamNotifyInfo(qTaskInfo_t tinfo, int32_t eventTypes, const SSchemaWrapper* pSchemaWrapper, - const char* stbFullName, bool newSubTableRule) { + const char* stbFullName, bool newSubTableRule, STaskNotifyEventStat* pNotifyEventStat) { int32_t code = TSDB_CODE_SUCCESS; SStreamTaskInfo *pStreamInfo = NULL; @@ -267,6 +267,7 @@ int32_t qSetStreamNotifyInfo(qTaskInfo_t tinfo, int32_t eventTypes, const SSchem } pStreamInfo->stbFullName = taosStrdup(stbFullName); pStreamInfo->newSubTableRule = newSubTableRule; + pStreamInfo->pNotifyEventStat = pNotifyEventStat; _end: return code; diff --git a/source/libs/executor/src/streamcountwindowoperator.c b/source/libs/executor/src/streamcountwindowoperator.c index b8c3ec90f9..c33abb3d89 100644 --- a/source/libs/executor/src/streamcountwindowoperator.c +++ b/source/libs/executor/src/streamcountwindowoperator.c @@ -25,7 +25,6 @@ #include "tlog.h" #include "ttime.h" -#define IS_NORMAL_COUNT_OP(op) ((op)->operatorType == QUERY_NODE_PHYSICAL_PLAN_STREAM_COUNT) #define STREAM_COUNT_OP_STATE_NAME "StreamCountHistoryState" #define STREAM_COUNT_OP_CHECKPOINT_NAME "StreamCountOperator_Checkpoint" @@ -56,6 +55,8 @@ void destroyStreamCountAggOperatorInfo(void* param) { &pInfo->groupResInfo); pInfo->pOperator = NULL; } + + destroyStreamBasicInfo(&pInfo->basic); destroyStreamAggSupporter(&pInfo->streamAggSup); cleanupExprSupp(&pInfo->scalarSupp); clearGroupResInfo(&pInfo->groupResInfo); @@ -79,10 +80,9 @@ void destroyStreamCountAggOperatorInfo(void* param) { bool isSlidingCountWindow(SStreamAggSupporter* pAggSup) { return pAggSup->windowCount != pAggSup->windowSliding; } int32_t setCountOutputBuf(SStreamAggSupporter* pAggSup, TSKEY ts, uint64_t groupId, SCountWindowInfo* pCurWin, - SBuffInfo* pBuffInfo) { + SBuffInfo* pBuffInfo, int32_t* pWinCode) { int32_t code = TSDB_CODE_SUCCESS; int32_t lino = 0; - int32_t winCode = TSDB_CODE_SUCCESS; int32_t size = pAggSup->resultRowSize; pCurWin->winInfo.sessionWin.groupId = groupId; pCurWin->winInfo.sessionWin.win.skey = ts; @@ -90,19 +90,21 @@ int32_t setCountOutputBuf(SStreamAggSupporter* pAggSup, TSKEY ts, uint64_t group if (isSlidingCountWindow(pAggSup)) { if (pBuffInfo->winBuffOp == CREATE_NEW_WINDOW) { - code = pAggSup->stateStore.streamStateCountWinAdd(pAggSup->pState, &pCurWin->winInfo.sessionWin, pAggSup->windowCount, - (void**)&pCurWin->winInfo.pStatePos, &size); + code = + pAggSup->stateStore.streamStateCountWinAdd(pAggSup->pState, &pCurWin->winInfo.sessionWin, + pAggSup->windowCount, (void**)&pCurWin->winInfo.pStatePos, &size); QUERY_CHECK_CODE(code, lino, _end); - winCode = TSDB_CODE_FAILED; + *pWinCode = TSDB_CODE_FAILED; } else if (pBuffInfo->winBuffOp == MOVE_NEXT_WINDOW) { QUERY_CHECK_NULL(pBuffInfo->pCur, code, lino, _end, terrno); pAggSup->stateStore.streamStateCurNext(pAggSup->pState, pBuffInfo->pCur); - winCode = pAggSup->stateStore.streamStateSessionGetKVByCur(pBuffInfo->pCur, &pCurWin->winInfo.sessionWin, - (void**)&pCurWin->winInfo.pStatePos, &size); - if (winCode == TSDB_CODE_FAILED) { - code = pAggSup->stateStore.streamStateCountWinAdd(pAggSup->pState, &pCurWin->winInfo.sessionWin, pAggSup->windowCount, - (void**)&pCurWin->winInfo.pStatePos, &size); + *pWinCode = pAggSup->stateStore.streamStateSessionGetKVByCur(pBuffInfo->pCur, &pCurWin->winInfo.sessionWin, + (void**)&pCurWin->winInfo.pStatePos, &size); + if (*pWinCode == TSDB_CODE_FAILED) { + code = pAggSup->stateStore.streamStateCountWinAdd(pAggSup->pState, &pCurWin->winInfo.sessionWin, + pAggSup->windowCount, (void**)&pCurWin->winInfo.pStatePos, + &size); QUERY_CHECK_CODE(code, lino, _end); } else { reuseOutputBuf(pAggSup->pState, pCurWin->winInfo.pStatePos, &pAggSup->stateStore); @@ -110,11 +112,12 @@ int32_t setCountOutputBuf(SStreamAggSupporter* pAggSup, TSKEY ts, uint64_t group } else { pBuffInfo->pCur = pAggSup->stateStore.streamStateCountSeekKeyPrev(pAggSup->pState, &pCurWin->winInfo.sessionWin, pAggSup->windowCount); - winCode = pAggSup->stateStore.streamStateSessionGetKVByCur(pBuffInfo->pCur, &pCurWin->winInfo.sessionWin, - (void**)&pCurWin->winInfo.pStatePos, &size); - if (winCode == TSDB_CODE_FAILED) { - code = pAggSup->stateStore.streamStateCountWinAdd(pAggSup->pState, &pCurWin->winInfo.sessionWin, pAggSup->windowCount, - (void**)&pCurWin->winInfo.pStatePos, &size); + *pWinCode = pAggSup->stateStore.streamStateSessionGetKVByCur(pBuffInfo->pCur, &pCurWin->winInfo.sessionWin, + (void**)&pCurWin->winInfo.pStatePos, &size); + if (*pWinCode == TSDB_CODE_FAILED) { + code = pAggSup->stateStore.streamStateCountWinAdd(pAggSup->pState, &pCurWin->winInfo.sessionWin, + pAggSup->windowCount, (void**)&pCurWin->winInfo.pStatePos, + &size); QUERY_CHECK_CODE(code, lino, _end); } else { reuseOutputBuf(pAggSup->pState, pCurWin->winInfo.pStatePos, &pAggSup->stateStore); @@ -126,11 +129,11 @@ int32_t setCountOutputBuf(SStreamAggSupporter* pAggSup, TSKEY ts, uint64_t group } else { code = pAggSup->stateStore.streamStateCountWinAddIfNotExist(pAggSup->pState, &pCurWin->winInfo.sessionWin, pAggSup->windowCount, - (void**)&pCurWin->winInfo.pStatePos, &size, &winCode); + (void**)&pCurWin->winInfo.pStatePos, &size, pWinCode); QUERY_CHECK_CODE(code, lino, _end); } - if (winCode == TSDB_CODE_SUCCESS) { + if (*pWinCode == TSDB_CODE_SUCCESS) { pCurWin->winInfo.isOutput = true; } pCurWin->pWindowCount = @@ -297,10 +300,18 @@ static void doStreamCountAggImpl(SOperatorInfo* pOperator, SSDataBlock* pSDataBl continue; } SCountWindowInfo curWin = {0}; + int32_t winCode = TSDB_CODE_SUCCESS; buffInfo.rebuildWindow = false; - code = setCountOutputBuf(pAggSup, startTsCols[i], groupId, &curWin, &buffInfo); + code = setCountOutputBuf(pAggSup, startTsCols[i], groupId, &curWin, &buffInfo, &winCode); QUERY_CHECK_CODE(code, lino, _end); + if (winCode != TSDB_CODE_SUCCESS && + BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_OPEN)) { + code = addCountAggNotifyEvent(SNOTIFY_EVENT_WINDOW_OPEN, &curWin.winInfo.sessionWin, &pInfo->basic.notifyEventSup, + pTaskInfo->streamInfo.pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } + if (!inCountSlidingWindow(pAggSup, &curWin.winInfo.sessionWin.win, &pSDataBlock->info)) { buffInfo.winBuffOp = MOVE_NEXT_WINDOW; continue; @@ -375,23 +386,54 @@ _end: static int32_t buildCountResult(SOperatorInfo* pOperator, SSDataBlock** ppRes) { int32_t code = TSDB_CODE_SUCCESS; + int32_t lino = 0; SStreamCountAggOperatorInfo* pInfo = pOperator->info; SStreamAggSupporter* pAggSup = &pInfo->streamAggSup; SOptrBasicInfo* pBInfo = &pInfo->binfo; SExecTaskInfo* pTaskInfo = pOperator->pTaskInfo; + SStreamNotifyEventSupp* pNotifySup = &pInfo->basic.notifyEventSup; + STaskNotifyEventStat* pNotifyEventStat = pTaskInfo->streamInfo.pNotifyEventStat; + bool addNotifyEvent = false; + addNotifyEvent = BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_CLOSE); doBuildDeleteDataBlock(pOperator, pInfo->pStDeleted, pInfo->pDelRes, &pInfo->pDelIterator); if (pInfo->pDelRes->info.rows > 0) { printDataBlock(pInfo->pDelRes, getStreamOpName(pOperator->operatorType), GET_TASKID(pTaskInfo)); + if (addNotifyEvent) { + code = addAggDeleteNotifyEvent(pInfo->pDelRes, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } (*ppRes) = pInfo->pDelRes; return code; } - doBuildSessionResult(pOperator, pAggSup->pState, &pInfo->groupResInfo, pBInfo->pRes); + doBuildSessionResult(pOperator, pAggSup->pState, &pInfo->groupResInfo, pBInfo->pRes, + addNotifyEvent ? pNotifySup->pSessionKeys : NULL); if (pBInfo->pRes->info.rows > 0) { printDataBlock(pBInfo->pRes, getStreamOpName(pOperator->operatorType), GET_TASKID(pTaskInfo)); + if (addNotifyEvent) { + code = addAggResultNotifyEvent(pBInfo->pRes, pNotifySup->pSessionKeys, pTaskInfo->streamInfo.notifyResultSchema, + pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } (*ppRes) = pBInfo->pRes; return code; } + + code = buildNotifyEventBlock(pTaskInfo, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + if (pNotifySup->pEventBlock && pNotifySup->pEventBlock->info.rows > 0) { + printDataBlock(pNotifySup->pEventBlock, getStreamOpName(pOperator->operatorType), GET_TASKID(pTaskInfo)); + (*ppRes) = pNotifySup->pEventBlock; + return code; + } + + code = removeOutdatedNotifyEvents(&pInfo->twAggSup, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + +_end: + if (code != TSDB_CODE_SUCCESS) { + qError("%s failed at line %d since %s. task:%s", __func__, lino, tstrerror(code), GET_TASKID(pTaskInfo)); + } (*ppRes) = NULL; return code; } @@ -423,7 +465,10 @@ int32_t doStreamCountEncodeOpState(void** buf, int32_t len, SOperatorInfo* pOper // 3.dataVersion tlen += taosEncodeFixedI32(buf, pInfo->dataVersion); - // 4.checksum + // 4.basicInfo + tlen += encodeStreamBasicInfo(buf, &pInfo->basic); + + // 5.checksum if (isParent) { if (buf) { uint32_t cksum = taosCalcChecksum(0, pData, len - sizeof(uint32_t)); @@ -441,12 +486,13 @@ int32_t doStreamCountDecodeOpState(void* buf, int32_t len, SOperatorInfo* pOpera int32_t lino = 0; SStreamCountAggOperatorInfo* pInfo = pOperator->info; SExecTaskInfo* pTaskInfo = pOperator->pTaskInfo; + void* pDataEnd = POINTER_SHIFT(buf, len); if (!pInfo) { code = TSDB_CODE_FAILED; QUERY_CHECK_CODE(code, lino, _end); } - // 4.checksum + // 5.checksum if (isParent) { int32_t dataLen = len - sizeof(uint32_t); void* pCksum = POINTER_SHIFT(buf, dataLen); @@ -454,6 +500,7 @@ int32_t doStreamCountDecodeOpState(void* buf, int32_t len, SOperatorInfo* pOpera code = TSDB_CODE_FAILED; QUERY_CHECK_CODE(code, lino, _end); } + pDataEnd = pCksum; } // 1.streamAggSup.pResultRows @@ -462,9 +509,10 @@ int32_t doStreamCountDecodeOpState(void* buf, int32_t len, SOperatorInfo* pOpera for (int32_t i = 0; i < mapSize; i++) { SSessionKey key = {0}; SCountWindowInfo curWin = {0}; + int32_t winCode = TSDB_CODE_SUCCESS; buf = decodeSSessionKey(buf, &key); SBuffInfo buffInfo = {.rebuildWindow = false, .winBuffOp = NONE_WINDOW, .pCur = NULL}; - code = setCountOutputBuf(&pInfo->streamAggSup, key.win.skey, key.groupId, &curWin, &buffInfo); + code = setCountOutputBuf(&pInfo->streamAggSup, key.win.skey, key.groupId, &curWin, &buffInfo, &winCode); QUERY_CHECK_CODE(code, lino, _end); buf = decodeSResultWindowInfo(buf, &curWin.winInfo, pInfo->streamAggSup.resultRowSize); @@ -479,6 +527,12 @@ int32_t doStreamCountDecodeOpState(void* buf, int32_t len, SOperatorInfo* pOpera // 3.dataVersion buf = taosDecodeFixedI64(buf, &pInfo->dataVersion); + // 4.basicInfo + if (buf < pDataEnd) { + code = decodeStreamBasicInfo(&buf, &pInfo->basic); + QUERY_CHECK_CODE(code, lino, _end); + } + _end: if (code != TSDB_CODE_SUCCESS) { qError("%s failed at line %d since %s. task:%s", __func__, lino, tstrerror(code), GET_TASKID(pTaskInfo)); @@ -851,7 +905,7 @@ int32_t createStreamCountAggOperatorInfo(SOperatorInfo* downstream, SPhysiNode* QUERY_CHECK_NULL(pResBlock, code, lino, _error, terrno); pInfo->binfo.pRes = pResBlock; - SExprInfo* pExprInfo = NULL; + SExprInfo* pExprInfo = NULL; code = createExprInfo(pCountNode->window.pFuncs, NULL, &pExprInfo, &numOfCols); QUERY_CHECK_CODE(code, lino, _error); @@ -925,6 +979,9 @@ int32_t createStreamCountAggOperatorInfo(SOperatorInfo* downstream, SPhysiNode* optrDefaultBufFn, NULL, optrDefaultGetNextExtFn, NULL); setOperatorStreamStateFn(pOperator, streamCountReleaseState, streamCountReloadState); + code = initStreamBasicInfo(&pInfo->basic, pOperator); + QUERY_CHECK_CODE(code, lino, _error); + if (downstream) { code = initDownStream(downstream, &pInfo->streamAggSup, pOperator->operatorType, pInfo->primaryTsIndex, &pInfo->twAggSup, &pInfo->basic); diff --git a/source/libs/executor/src/streameventwindowoperator.c b/source/libs/executor/src/streameventwindowoperator.c index 5f4d6b30fa..ab2aa600bb 100644 --- a/source/libs/executor/src/streameventwindowoperator.c +++ b/source/libs/executor/src/streameventwindowoperator.c @@ -30,7 +30,6 @@ #include "tlog.h" #include "ttime.h" -#define IS_NORMAL_EVENT_OP(op) ((op)->operatorType == QUERY_NODE_PHYSICAL_PLAN_STREAM_EVENT) #define STREAM_EVENT_OP_STATE_NAME "StreamEventHistoryState" #define STREAM_EVENT_OP_CHECKPOINT_NAME "StreamEventOperator_Checkpoint" @@ -135,7 +134,8 @@ void reuseOutputBuf(void* pState, SRowBuffPos* pPos, SStateStore* pAPI) { } int32_t setEventOutputBuf(SStreamAggSupporter* pAggSup, TSKEY* pTs, uint64_t groupId, bool* pStart, bool* pEnd, - int32_t index, int32_t rows, SEventWindowInfo* pCurWin, SSessionKey* pNextWinKey, int32_t* pWinCode) { + int32_t index, int32_t rows, SEventWindowInfo* pCurWin, SSessionKey* pNextWinKey, + int32_t* pWinCode) { int32_t code = TSDB_CODE_SUCCESS; int32_t lino = 0; int32_t winCode = TSDB_CODE_SUCCESS; @@ -179,7 +179,7 @@ int32_t setEventOutputBuf(SStreamAggSupporter* pAggSup, TSKEY* pTs, uint64_t gro SSessionKey winKey = {.win.skey = ts, .win.ekey = ts, .groupId = groupId}; code = pAggSup->stateStore.streamStateSessionAllocWinBuffByNextPosition(pAggSup->pState, pCur, &winKey, &pVal, &len); QUERY_CHECK_CODE(code, lino, _error); - (*pWinCode) = TSDB_CODE_FAILED; + (*pWinCode) = TSDB_CODE_FAILED; setEventWindowInfo(pAggSup, &winKey, pVal, pCurWin); pCurWin->pWinFlag->startFlag = start; @@ -335,6 +335,8 @@ static void doStreamEventAggImpl(SOperatorInfo* pOperator, SSDataBlock* pSDataBl SStreamAggSupporter* pAggSup = &pInfo->streamAggSup; SColumnInfoData* pColStart = NULL; SColumnInfoData* pColEnd = NULL; + SStreamNotifyEventSupp* pNotifySup = &pInfo->basic.notifyEventSup; + STaskNotifyEventStat* pNotifyEventStat = pTaskInfo->streamInfo.pNotifyEventStat; pInfo->dataVersion = TMAX(pInfo->dataVersion, pSDataBlock->info.version); pAggSup->winRange = pTaskInfo->streamInfo.fillHistoryWindow; @@ -395,17 +397,19 @@ static void doStreamEventAggImpl(SOperatorInfo* pOperator, SSDataBlock* pSDataBl &nextWinKey, &winCode); QUERY_CHECK_CODE(code, lino, _end); - if (BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_OPEN) && - *(bool*)colDataGetNumData(pColStart, i) && winCode != TSDB_CODE_SUCCESS) { + if (winCode != TSDB_CODE_SUCCESS && + BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_OPEN) && + *(bool*)colDataGetNumData(pColStart, i)) { code = addEventAggNotifyEvent(SNOTIFY_EVENT_WINDOW_OPEN, &curWin.winInfo.sessionWin, pSDataBlock, - pInfo->pStartCondCols, i, &pInfo->basic.windowEventSup); + pInfo->pStartCondCols, i, pNotifySup, pNotifyEventStat); QUERY_CHECK_CODE(code, lino, _end); } setSessionWinOutputInfo(pSeUpdated, &curWin.winInfo); bool rebuild = false; - code = updateEventWindowInfo(pAggSup, &curWin, &nextWinKey, tsCols, (bool*)pColStart->pData, (bool*)pColEnd->pData, - rows, i, pAggSup->pResultRows, pSeUpdated, pStDeleted, &rebuild, &winRows); + code = updateEventWindowInfo(pAggSup, &curWin, &nextWinKey, tsCols, (bool*)pColStart->pData, + (bool*)pColEnd->pData, rows, i, pAggSup->pResultRows, pSeUpdated, pStDeleted, &rebuild, + &winRows); QUERY_CHECK_CODE(code, lino, _end); if (rebuild) { @@ -471,7 +475,7 @@ static void doStreamEventAggImpl(SOperatorInfo* pOperator, SSDataBlock* pSDataBl if (BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_CLOSE)) { code = addEventAggNotifyEvent(SNOTIFY_EVENT_WINDOW_CLOSE, &curWin.winInfo.sessionWin, pSDataBlock, - pInfo->pEndCondCols, i + winRows - 1, &pInfo->basic.windowEventSup); + pInfo->pEndCondCols, i + winRows - 1, pNotifySup, pNotifyEventStat); QUERY_CHECK_CODE(code, lino, _end); } } @@ -513,7 +517,10 @@ int32_t doStreamEventEncodeOpState(void** buf, int32_t len, SOperatorInfo* pOper // 3.dataVersion tlen += taosEncodeFixedI32(buf, pInfo->dataVersion); - // 4.checksum + // 4.basicInfo + tlen += encodeStreamBasicInfo(buf, &pInfo->basic); + + // 5.checksum if (buf) { uint32_t cksum = taosCalcChecksum(0, pData, len - sizeof(uint32_t)); tlen += taosEncodeFixedU32(buf, cksum); @@ -529,13 +536,14 @@ int32_t doStreamEventDecodeOpState(void* buf, int32_t len, SOperatorInfo* pOpera int32_t lino = 0; SStreamEventAggOperatorInfo* pInfo = pOperator->info; SExecTaskInfo* pTaskInfo = pOperator->pTaskInfo; + void* pDataEnd = POINTER_SHIFT(buf, len); if (!pInfo) { code = TSDB_CODE_FAILED; QUERY_CHECK_CODE(code, lino, _end); } SStreamAggSupporter* pAggSup = &pInfo->streamAggSup; - // 4.checksum + // 5.checksum int32_t dataLen = len - sizeof(uint32_t); void* pCksum = POINTER_SHIFT(buf, dataLen); if (taosCheckChecksum(buf, dataLen, *(uint32_t*)pCksum) != TSDB_CODE_SUCCESS) { @@ -543,6 +551,7 @@ int32_t doStreamEventDecodeOpState(void* buf, int32_t len, SOperatorInfo* pOpera code = TSDB_CODE_FAILED; QUERY_CHECK_CODE(code, lino, _end); } + pDataEnd = pCksum; // 1.streamAggSup.pResultRows int32_t mapSize = 0; @@ -567,6 +576,12 @@ int32_t doStreamEventDecodeOpState(void* buf, int32_t len, SOperatorInfo* pOpera // 3.dataVersion buf = taosDecodeFixedI64(buf, &pInfo->dataVersion); + // 4.basicInfo + if (buf < pDataEnd) { + code = decodeStreamBasicInfo(&buf, &pInfo->basic); + QUERY_CHECK_CODE(code, lino, _end); + } + _end: if (code != TSDB_CODE_SUCCESS) { qError("%s failed at line %d since %s. task:%s", __func__, lino, tstrerror(code), GET_TASKID(pTaskInfo)); @@ -598,33 +613,45 @@ static int32_t buildEventResult(SOperatorInfo* pOperator, SSDataBlock** ppRes) { SStreamEventAggOperatorInfo* pInfo = pOperator->info; SOptrBasicInfo* pBInfo = &pInfo->binfo; SExecTaskInfo* pTaskInfo = pOperator->pTaskInfo; - + SStreamNotifyEventSupp* pNotifySup = &pInfo->basic.notifyEventSup; + STaskNotifyEventStat* pNotifyEventStat = pTaskInfo->streamInfo.pNotifyEventStat; + bool addNotifyEvent = false; + addNotifyEvent = BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_CLOSE); doBuildDeleteDataBlock(pOperator, pInfo->pSeDeleted, pInfo->pDelRes, &pInfo->pDelIterator); if (pInfo->pDelRes->info.rows > 0) { printDataBlock(pInfo->pDelRes, getStreamOpName(pOperator->operatorType), GET_TASKID(pTaskInfo)); + if (addNotifyEvent) { + code = addAggDeleteNotifyEvent(pInfo->pDelRes, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } (*ppRes) = pInfo->pDelRes; return code; } - doBuildSessionResult(pOperator, pInfo->streamAggSup.pState, &pInfo->groupResInfo, pBInfo->pRes); + doBuildSessionResult(pOperator, pInfo->streamAggSup.pState, &pInfo->groupResInfo, pBInfo->pRes, + addNotifyEvent ? pNotifySup->pSessionKeys : NULL); if (pBInfo->pRes->info.rows > 0) { printDataBlock(pBInfo->pRes, getStreamOpName(pOperator->operatorType), GET_TASKID(pTaskInfo)); - if (BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_CLOSE)) { - code = addAggResultNotifyEvent(pBInfo->pRes, pTaskInfo->streamInfo.notifyResultSchema, &pInfo->basic.windowEventSup); + if (addNotifyEvent) { + code = addAggResultNotifyEvent(pBInfo->pRes, pNotifySup->pSessionKeys, pTaskInfo->streamInfo.notifyResultSchema, + pNotifySup, pNotifyEventStat); QUERY_CHECK_CODE(code, lino, _end); } (*ppRes) = pBInfo->pRes; return code; } - code = buildNotifyEventBlock(pTaskInfo, &pInfo->basic.windowEventSup); + code = buildNotifyEventBlock(pTaskInfo, pNotifySup, pNotifyEventStat); QUERY_CHECK_CODE(code, lino, _end); - if (pInfo->basic.windowEventSup.pEventBlock->info.rows > 0) { - printDataBlock(pInfo->basic.windowEventSup.pEventBlock, getStreamOpName(pOperator->operatorType), GET_TASKID(pTaskInfo)); - (*ppRes) = pInfo->basic.windowEventSup.pEventBlock; + if (pNotifySup->pEventBlock && pNotifySup->pEventBlock->info.rows > 0) { + printDataBlock(pNotifySup->pEventBlock, getStreamOpName(pOperator->operatorType), GET_TASKID(pTaskInfo)); + (*ppRes) = pNotifySup->pEventBlock; return code; } + code = removeOutdatedNotifyEvents(&pInfo->twAggSup, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + _end: (*ppRes) = NULL; if (code != TSDB_CODE_SUCCESS) { @@ -800,7 +827,7 @@ void streamEventReleaseState(SOperatorInfo* pOperator) { int32_t resSize = winSize + sizeof(TSKEY); char* pBuff = taosMemoryCalloc(1, resSize); if (!pBuff) { - return ; + return; } memcpy(pBuff, pInfo->historyWins->pData, winSize); memcpy(pBuff + winSize, &pInfo->twAggSup.maxTs, sizeof(TSKEY)); @@ -954,9 +981,9 @@ int32_t createStreamEventAggOperatorInfo(SOperatorInfo* downstream, SPhysiNode* QUERY_CHECK_NULL(pResBlock, code, lino, _error, terrno); pInfo->binfo.pRes = pResBlock; - SExprSupp* pExpSup = &pOperator->exprSupp; - int32_t numOfCols = 0; - SExprInfo* pExprInfo = NULL; + SExprSupp* pExpSup = &pOperator->exprSupp; + int32_t numOfCols = 0; + SExprInfo* pExprInfo = NULL; code = createExprInfo(pEventNode->window.pFuncs, NULL, &pExprInfo, &numOfCols); QUERY_CHECK_CODE(code, lino, _error); @@ -1006,7 +1033,6 @@ int32_t createStreamEventAggOperatorInfo(SOperatorInfo* downstream, SPhysiNode* pInfo->pPkDeleted = tSimpleHashInit(64, hashFn); QUERY_CHECK_NULL(pInfo->pPkDeleted, code, lino, _error, terrno); pInfo->destHasPrimaryKey = pEventNode->window.destHasPrimaryKey; - initStreamBasicInfo(&pInfo->basic); pInfo->pOperator = pOperator; setOperatorInfo(pOperator, "StreamEventAggOperator", QUERY_NODE_PHYSICAL_PLAN_STREAM_EVENT, true, OP_NOT_OPENED, @@ -1026,6 +1052,10 @@ int32_t createStreamEventAggOperatorInfo(SOperatorInfo* downstream, SPhysiNode* pOperator->fpSet = createOperatorFpSet(optrDummyOpenFn, doStreamEventAggNext, NULL, destroyStreamEventOperatorInfo, optrDefaultBufFn, NULL, optrDefaultGetNextExtFn, NULL); setOperatorStreamStateFn(pOperator, streamEventReleaseState, streamEventReloadState); + + code = initStreamBasicInfo(&pInfo->basic, pOperator); + QUERY_CHECK_CODE(code, lino, _error); + code = initDownStream(downstream, &pInfo->streamAggSup, pOperator->operatorType, pInfo->primaryTsIndex, &pInfo->twAggSup, &pInfo->basic); QUERY_CHECK_CODE(code, lino, _error); diff --git a/source/libs/executor/src/streamexecutorInt.c b/source/libs/executor/src/streamexecutorInt.c index 9cafdfff0c..635de21b6e 100644 --- a/source/libs/executor/src/streamexecutorInt.c +++ b/source/libs/executor/src/streamexecutorInt.c @@ -16,53 +16,58 @@ #include "streamexecutorInt.h" #include "executorInt.h" +#include "operator.h" #include "tdatablock.h" #define NOTIFY_EVENT_NAME_CACHE_LIMIT_MB 16 typedef struct SStreamNotifyEvent { uint64_t gid; - TSKEY skey; - char* content; - bool isEnd; + int64_t eventType; + STimeWindow win; + cJSON* pJson; } SStreamNotifyEvent; +#define NOTIFY_EVENT_KEY_SIZE \ + ((sizeof(((struct SStreamNotifyEvent*)0)->gid) + sizeof(((struct SStreamNotifyEvent*)0)->eventType)) + \ + sizeof(((struct SStreamNotifyEvent*)0)->win.skey)) + void setStreamOperatorState(SSteamOpBasicInfo* pBasicInfo, EStreamType type) { if (type != STREAM_GET_ALL && type != STREAM_CHECKPOINT) { pBasicInfo->updateOperatorInfo = true; } } -bool needSaveStreamOperatorInfo(SSteamOpBasicInfo* pBasicInfo) { - return pBasicInfo->updateOperatorInfo; -} +bool needSaveStreamOperatorInfo(SSteamOpBasicInfo* pBasicInfo) { return pBasicInfo->updateOperatorInfo; } -void saveStreamOperatorStateComplete(SSteamOpBasicInfo* pBasicInfo) { - pBasicInfo->updateOperatorInfo = false; -} +void saveStreamOperatorStateComplete(SSteamOpBasicInfo* pBasicInfo) { pBasicInfo->updateOperatorInfo = false; } static void destroyStreamWindowEvent(void* ptr) { - SStreamNotifyEvent* pEvent = ptr; - if (pEvent == NULL || pEvent->content == NULL) return; - cJSON_free(pEvent->content); + SStreamNotifyEvent* pEvent = (SStreamNotifyEvent*)ptr; + if (pEvent) { + if (pEvent->pJson) { + cJSON_Delete(pEvent->pJson); + } + *pEvent = (SStreamNotifyEvent){0}; + } } static void destroyStreamNotifyEventSupp(SStreamNotifyEventSupp* sup) { if (sup == NULL) return; - taosArrayDestroyEx(sup->pWindowEvents, destroyStreamWindowEvent); + taosHashCleanup(sup->pWindowEventHashMap); taosHashCleanup(sup->pTableNameHashMap); - taosHashCleanup(sup->pResultHashMap); blockDataDestroy(sup->pEventBlock); + taosArrayDestroy(sup->pSessionKeys); *sup = (SStreamNotifyEventSupp){0}; } -static int32_t initStreamNotifyEventSupp(SStreamNotifyEventSupp *sup) { - int32_t code = TSDB_CODE_SUCCESS; - int32_t lino = 0; - SSDataBlock* pBlock = NULL; +static int32_t initStreamNotifyEventSupp(SStreamNotifyEventSupp* sup, const char* windowType, int32_t resCapacity) { + int32_t code = TSDB_CODE_SUCCESS; + int32_t lino = 0; + SSDataBlock* pBlock = NULL; SColumnInfoData infoData = {0}; - if (sup == NULL) { + if (sup == NULL || sup->pWindowEventHashMap != NULL) { goto _end; } @@ -77,15 +82,18 @@ static int32_t initStreamNotifyEventSupp(SStreamNotifyEventSupp *sup) { code = blockDataAppendColInfo(pBlock, &infoData); QUERY_CHECK_CODE(code, lino, _end); - sup->pWindowEvents = taosArrayInit(0, sizeof(SStreamNotifyEvent)); - QUERY_CHECK_NULL(sup->pWindowEvents, code, lino, _end, terrno); + sup->pWindowEventHashMap = taosHashInit(4096, taosGetDefaultHashFunction(TSDB_DATA_TYPE_BINARY), true, HASH_NO_LOCK); + QUERY_CHECK_NULL(sup->pWindowEventHashMap, code, lino, _end, terrno); + taosHashSetFreeFp(sup->pWindowEventHashMap, destroyStreamWindowEvent); sup->pTableNameHashMap = taosHashInit(1024, taosGetDefaultHashFunction(TSDB_DATA_TYPE_UBIGINT), false, HASH_NO_LOCK); QUERY_CHECK_NULL(sup->pTableNameHashMap, code, lino, _end, terrno); - sup->pResultHashMap = taosHashInit(4096, taosGetDefaultHashFunction(TSDB_DATA_TYPE_BINARY), false, HASH_NO_LOCK); - QUERY_CHECK_NULL(sup->pResultHashMap, code, lino, _end, terrno); - taosHashSetFreeFp(sup->pResultHashMap, destroyStreamWindowEvent); sup->pEventBlock = pBlock; pBlock = NULL; + code = blockDataEnsureCapacity(sup->pEventBlock, resCapacity); + QUERY_CHECK_CODE(code, lino, _end); + sup->windowType = windowType; + sup->pSessionKeys = taosArrayInit(resCapacity, sizeof(SSessionKey)); + QUERY_CHECK_NULL(sup->pSessionKeys, code, lino, _end, terrno); _end: if (code != TSDB_CODE_SUCCESS) { @@ -100,17 +108,99 @@ _end: return code; } -int32_t initStreamBasicInfo(SSteamOpBasicInfo* pBasicInfo) { +int32_t initStreamBasicInfo(SSteamOpBasicInfo* pBasicInfo, const struct SOperatorInfo* pOperator) { pBasicInfo->primaryPkIndex = -1; pBasicInfo->updateOperatorInfo = false; - return initStreamNotifyEventSupp(&pBasicInfo->windowEventSup); + const char* windowType = NULL; + if (IS_NORMAL_INTERVAL_OP(pOperator)) { + windowType = "Time"; + } else if (IS_NORMAL_SESSION_OP(pOperator)) { + windowType = "Session"; + } else if (IS_NORMAL_STATE_OP(pOperator)) { + windowType = "State"; + } else if (IS_NORMAL_EVENT_OP(pOperator)) { + windowType = "Event"; + } else if (IS_NORMAL_COUNT_OP(pOperator)) { + windowType = "Count"; + } else { + return TSDB_CODE_SUCCESS; + } + return initStreamNotifyEventSupp(&pBasicInfo->notifyEventSup, windowType, pOperator->resultInfo.capacity); } void destroyStreamBasicInfo(SSteamOpBasicInfo* pBasicInfo) { - destroyStreamNotifyEventSupp(&pBasicInfo->windowEventSup); + destroyStreamNotifyEventSupp(&pBasicInfo->notifyEventSup); } -static void streamNotifyGetEventWindowId(const SSessionKey* pSessionKey, char *buf) { +static int32_t encodeStreamNotifyEventSupp(void** buf, const SStreamNotifyEventSupp* sup) { + int32_t tlen = 0; + void* pIter = NULL; + char* str = NULL; + + if (sup == NULL) { + return tlen; + } + + tlen += taosEncodeFixedI32(buf, taosHashGetSize(sup->pWindowEventHashMap)); + pIter = taosHashIterate(sup->pWindowEventHashMap, NULL); + while (pIter) { + const SStreamNotifyEvent* pEvent = (const SStreamNotifyEvent*)pIter; + str = cJSON_PrintUnformatted(pEvent->pJson); + + tlen += taosEncodeFixedU64(buf, pEvent->gid); + tlen += taosEncodeFixedI64(buf, pEvent->eventType); + tlen += taosEncodeFixedI64(buf, pEvent->win.skey); + tlen += taosEncodeFixedI64(buf, pEvent->win.ekey); + tlen += taosEncodeString(buf, str); + cJSON_free(str); + pIter = taosHashIterate(sup->pWindowEventHashMap, pIter); + } + return tlen; +} + +static int32_t decodeStreamNotifyEventSupp(void** buf, SStreamNotifyEventSupp* sup) { + int32_t code = TSDB_CODE_SUCCESS; + int32_t lino = 0; + void* p = *buf; + int32_t size = 0; + uint64_t len = 0; + SStreamNotifyEvent item = {0}; + + p = taosDecodeFixedI32(p, &size); + for (int32_t i = 0; i < size; i++) { + p = taosDecodeFixedU64(p, &item.gid); + p = taosDecodeFixedI64(p, &item.eventType); + p = taosDecodeFixedI64(p, &item.win.skey); + p = taosDecodeFixedI64(p, &item.win.ekey); + p = taosDecodeVariantU64(p, &len); + item.pJson = cJSON_Parse(p); + if (item.pJson == NULL) { + qWarn("failed to parse the json content since %s", cJSON_GetErrorPtr()); + } + QUERY_CHECK_NULL(item.pJson, code, lino, _end, TSDB_CODE_INTERNAL_ERROR); + p = POINTER_SHIFT(p, len); + code = taosHashPut(sup->pWindowEventHashMap, &item, NOTIFY_EVENT_KEY_SIZE, &item, sizeof(SStreamNotifyEvent)); + QUERY_CHECK_CODE(code, lino, _end); + item.pJson = NULL; + } + *buf = p; +_end: + if (code != TSDB_CODE_SUCCESS) { + qError("%s failed at line %d since %s", __func__, lino, tstrerror(code)); + } + destroyStreamWindowEvent(&item); + return code; +} + +int32_t encodeStreamBasicInfo(void** buf, const SSteamOpBasicInfo* pBasicInfo) { + return encodeStreamNotifyEventSupp(buf, &pBasicInfo->notifyEventSup); +} + +int32_t decodeStreamBasicInfo(void** buf, SSteamOpBasicInfo* pBasicInfo) { + return decodeStreamNotifyEventSupp(buf, &pBasicInfo->notifyEventSup); +} + +static void streamNotifyGetEventWindowId(const SSessionKey* pSessionKey, char* buf) { uint64_t hash = 0; uint64_t ar[2]; @@ -123,60 +213,60 @@ static void streamNotifyGetEventWindowId(const SSessionKey* pSessionKey, char *b #define JSON_CHECK_ADD_ITEM(obj, str, item) \ QUERY_CHECK_CONDITION(cJSON_AddItemToObjectCS(obj, str, item), code, lino, _end, TSDB_CODE_OUT_OF_MEMORY) -static int32_t jsonAddColumnField(const char* colName, const SColumnInfoData* pColData, int32_t ri, cJSON* obj) { +static int32_t jsonAddColumnField(const char* colName, int16_t type, bool isNull, const char* pData, cJSON* obj) { int32_t code = TSDB_CODE_SUCCESS; int32_t lino = 0; char* temp = NULL; QUERY_CHECK_NULL(colName, code, lino, _end, TSDB_CODE_INVALID_PARA); - QUERY_CHECK_NULL(pColData, code, lino, _end, TSDB_CODE_INVALID_PARA); + QUERY_CHECK_CONDITION(isNull || (pData != NULL), code, lino, _end, TSDB_CODE_INVALID_PARA); QUERY_CHECK_NULL(obj, code, lino, _end, TSDB_CODE_INVALID_PARA); - if (colDataIsNull_s(pColData, ri)) { + if (isNull) { JSON_CHECK_ADD_ITEM(obj, colName, cJSON_CreateNull()); goto _end; } - switch (pColData->info.type) { + switch (type) { case TSDB_DATA_TYPE_BOOL: { - bool val = *(bool*)colDataGetNumData(pColData, ri); + bool val = *(const bool*)pData; JSON_CHECK_ADD_ITEM(obj, colName, cJSON_CreateBool(val)); break; } case TSDB_DATA_TYPE_TINYINT: { - int8_t val = *(int8_t*)colDataGetNumData(pColData, ri); + int8_t val = *(const int8_t*)pData; JSON_CHECK_ADD_ITEM(obj, colName, cJSON_CreateNumber(val)); break; } case TSDB_DATA_TYPE_SMALLINT: { - int16_t val = *(int16_t*)colDataGetNumData(pColData, ri); + int16_t val = *(const int16_t*)pData; JSON_CHECK_ADD_ITEM(obj, colName, cJSON_CreateNumber(val)); break; } case TSDB_DATA_TYPE_INT: { - int32_t val = *(int32_t*)colDataGetNumData(pColData, ri); + int32_t val = *(const int32_t*)pData; JSON_CHECK_ADD_ITEM(obj, colName, cJSON_CreateNumber(val)); break; } case TSDB_DATA_TYPE_BIGINT: case TSDB_DATA_TYPE_TIMESTAMP: { - int64_t val = *(int64_t*)colDataGetNumData(pColData, ri); + int64_t val = *(const int64_t*)pData; JSON_CHECK_ADD_ITEM(obj, colName, cJSON_CreateNumber(val)); break; } case TSDB_DATA_TYPE_FLOAT: { - float val = *(float*)colDataGetNumData(pColData, ri); + float val = *(const float*)pData; JSON_CHECK_ADD_ITEM(obj, colName, cJSON_CreateNumber(val)); break; } case TSDB_DATA_TYPE_DOUBLE: { - double val = *(double*)colDataGetNumData(pColData, ri); + double val = *(const double*)pData; JSON_CHECK_ADD_ITEM(obj, colName, cJSON_CreateNumber(val)); break; } @@ -185,8 +275,8 @@ static int32_t jsonAddColumnField(const char* colName, const SColumnInfoData* pC case TSDB_DATA_TYPE_NCHAR: { // cJSON requires null-terminated strings, but this data is not null-terminated, // so we need to manually copy the string and add null termination. - const char* src = varDataVal(colDataGetVarData(pColData, ri)); - int32_t len = varDataLen(colDataGetVarData(pColData, ri)); + const char* src = varDataVal(pData); + int32_t len = varDataLen(pData); temp = cJSON_malloc(len + 1); QUERY_CHECK_NULL(temp, code, lino, _end, TSDB_CODE_OUT_OF_MEMORY); memcpy(temp, src, len); @@ -202,25 +292,25 @@ static int32_t jsonAddColumnField(const char* colName, const SColumnInfoData* pC } case TSDB_DATA_TYPE_UTINYINT: { - uint8_t val = *(uint8_t*)colDataGetNumData(pColData, ri); + uint8_t val = *(const uint8_t*)pData; JSON_CHECK_ADD_ITEM(obj, colName, cJSON_CreateNumber(val)); break; } case TSDB_DATA_TYPE_USMALLINT: { - uint16_t val = *(uint16_t*)colDataGetNumData(pColData, ri); + uint16_t val = *(const uint16_t*)pData; JSON_CHECK_ADD_ITEM(obj, colName, cJSON_CreateNumber(val)); break; } case TSDB_DATA_TYPE_UINT: { - uint32_t val = *(uint32_t*)colDataGetNumData(pColData, ri); + uint32_t val = *(const uint32_t*)pData; JSON_CHECK_ADD_ITEM(obj, colName, cJSON_CreateNumber(val)); break; } case TSDB_DATA_TYPE_UBIGINT: { - uint64_t val = *(uint64_t*)colDataGetNumData(pColData, ri); + uint64_t val = *(const uint64_t*)pData; JSON_CHECK_ADD_ITEM(obj, colName, cJSON_CreateNumber(val)); break; } @@ -241,53 +331,91 @@ _end: return code; } -int32_t addEventAggNotifyEvent(EStreamNotifyEventType eventType, const SSessionKey* pSessionKey, - const SSDataBlock* pInputBlock, const SNodeList* pCondCols, int32_t ri, - SStreamNotifyEventSupp* sup) { - int32_t code = TSDB_CODE_SUCCESS; - int32_t lino = 0; - SNode* node = NULL; - cJSON* event = NULL; - cJSON* fields = NULL; - cJSON* cond = NULL; - SStreamNotifyEvent item = {0}; - char windowId[32]; +static cJSON* createBasicAggNotifyEvent(const char* windowType, EStreamNotifyEventType eventType, + const SSessionKey* pSessionKey) { + int32_t code = TSDB_CODE_SUCCESS; + int32_t lino = 0; + const char* eventTypeStr = NULL; + cJSON* event = NULL; + char windowId[32]; + QUERY_CHECK_NULL(windowType, code, lino, _end, TSDB_CODE_INVALID_PARA); QUERY_CHECK_NULL(pSessionKey, code, lino, _end, TSDB_CODE_INVALID_PARA); - QUERY_CHECK_NULL(pInputBlock, code, lino, _end, TSDB_CODE_INVALID_PARA); - QUERY_CHECK_NULL(pInputBlock->pDataBlock, code, lino, _end, TSDB_CODE_INVALID_PARA); - QUERY_CHECK_NULL(pCondCols, code, lino, _end, TSDB_CODE_INVALID_PARA); - QUERY_CHECK_NULL(sup, code, lino, _end, TSDB_CODE_INVALID_PARA); - qDebug("add stream notify event from event window, type: %s, start: %" PRId64 ", end: %" PRId64, - (eventType == SNOTIFY_EVENT_WINDOW_OPEN) ? "WINDOW_OPEN" : "WINDOW_CLOSE", pSessionKey->win.skey, - pSessionKey->win.ekey); + if (eventType == SNOTIFY_EVENT_WINDOW_OPEN) { + eventTypeStr = "WINDOW_OPEN"; + } else if (eventType == SNOTIFY_EVENT_WINDOW_CLOSE) { + eventTypeStr = "WINDOW_CLOSE"; + } else if (eventType == SNOTIFY_EVENT_WINDOW_INVALIDATION) { + eventTypeStr = "WINDOW_INVALIDATION"; + } else { + QUERY_CHECK_CONDITION(false, code, lino, _end, TSDB_CODE_INVALID_PARA); + } + + qDebug("add stream notify event from %s Window, type: %s, start: %" PRId64 ", end: %" PRId64, windowType, + eventTypeStr, pSessionKey->win.skey, pSessionKey->win.ekey); event = cJSON_CreateObject(); QUERY_CHECK_NULL(event, code, lino, _end, TSDB_CODE_OUT_OF_MEMORY); // add basic info streamNotifyGetEventWindowId(pSessionKey, windowId); - if (eventType == SNOTIFY_EVENT_WINDOW_OPEN) { - JSON_CHECK_ADD_ITEM(event, "eventType", cJSON_CreateStringReference("WINDOW_OPEN")); - } else if (eventType == SNOTIFY_EVENT_WINDOW_CLOSE) { - JSON_CHECK_ADD_ITEM(event, "eventType", cJSON_CreateStringReference("WINDOW_CLOSE")); - } + JSON_CHECK_ADD_ITEM(event, "eventType", cJSON_CreateStringReference(eventTypeStr)); JSON_CHECK_ADD_ITEM(event, "eventTime", cJSON_CreateNumber(taosGetTimestampMs())); - JSON_CHECK_ADD_ITEM(event, "windowId", cJSON_CreateStringReference(windowId)); - JSON_CHECK_ADD_ITEM(event, "windowType", cJSON_CreateStringReference("Event")); + JSON_CHECK_ADD_ITEM(event, "windowId", cJSON_CreateString(windowId)); + JSON_CHECK_ADD_ITEM(event, "windowType", cJSON_CreateStringReference(windowType)); JSON_CHECK_ADD_ITEM(event, "windowStart", cJSON_CreateNumber(pSessionKey->win.skey)); - if (eventType == SNOTIFY_EVENT_WINDOW_CLOSE) { - JSON_CHECK_ADD_ITEM(event, "windowEnd", cJSON_CreateNumber(pSessionKey->win.ekey)); + if (eventType != SNOTIFY_EVENT_WINDOW_OPEN) { + if (strcmp(windowType, "Time") == 0) { + JSON_CHECK_ADD_ITEM(event, "windowEnd", cJSON_CreateNumber(pSessionKey->win.ekey + 1)); + } else { + JSON_CHECK_ADD_ITEM(event, "windowEnd", cJSON_CreateNumber(pSessionKey->win.ekey)); + } } +_end: + if (code != TSDB_CODE_SUCCESS) { + qError("%s failed at line %d since %s", __func__, lino, tstrerror(code)); + terrno = code; + cJSON_Delete(event); + event = NULL; + } + return event; +} + +int32_t addEventAggNotifyEvent(EStreamNotifyEventType eventType, const SSessionKey* pSessionKey, + const SSDataBlock* pInputBlock, const SNodeList* pCondCols, int32_t ri, + SStreamNotifyEventSupp* sup, STaskNotifyEventStat* pNotifyEventStat) { + int32_t code = TSDB_CODE_SUCCESS; + int32_t lino = 0; + cJSON* event = NULL; + cJSON* fields = NULL; + cJSON* cond = NULL; + const SNode* pNode = NULL; + int32_t origSize = 0; + int64_t startTime = 0; + int64_t endTime = 0; + SStreamNotifyEvent item = {0}; + + QUERY_CHECK_NULL(pInputBlock, code, lino, _end, TSDB_CODE_INVALID_PARA); + QUERY_CHECK_NULL(pInputBlock->pDataBlock, code, lino, _end, TSDB_CODE_INVALID_PARA); + QUERY_CHECK_NULL(pCondCols, code, lino, _end, TSDB_CODE_INVALID_PARA); + QUERY_CHECK_NULL(sup, code, lino, _end, TSDB_CODE_INVALID_PARA); + QUERY_CHECK_NULL(sup->windowType, code, lino, _end, TSDB_CODE_INVALID_PARA); + QUERY_CHECK_NULL(pNotifyEventStat, code, lino, _end, TSDB_CODE_INVALID_PARA); + + startTime = taosGetMonoTimestampMs(); + event = createBasicAggNotifyEvent(sup->windowType, eventType, pSessionKey); + QUERY_CHECK_NULL(event, code, lino, _end, terrno); + // create fields object to store matched column values fields = cJSON_CreateObject(); QUERY_CHECK_NULL(fields, code, lino, _end, TSDB_CODE_OUT_OF_MEMORY); - FOREACH(node, pCondCols) { - SColumnNode* pColDef = (SColumnNode*)node; - SColumnInfoData* pColData = taosArrayGet(pInputBlock->pDataBlock, pColDef->slotId); - code = jsonAddColumnField(pColDef->colName, pColData, ri, fields); + FOREACH(pNode, pCondCols) { + const SColumnNode* pColDef = (const SColumnNode*)pNode; + const SColumnInfoData* pColData = taosArrayGet(pInputBlock->pDataBlock, pColDef->slotId); + code = jsonAddColumnField(pColDef->colName, pColData->info.type, colDataIsNull_s(pColData, ri), + colDataGetData(pColData, ri), fields); QUERY_CHECK_CODE(code, lino, _end); } @@ -297,16 +425,24 @@ int32_t addEventAggNotifyEvent(EStreamNotifyEventType eventType, const SSessionK JSON_CHECK_ADD_ITEM(cond, "conditionIndex", cJSON_CreateNumber(0)); JSON_CHECK_ADD_ITEM(cond, "fieldValues", fields); fields = NULL; - JSON_CHECK_ADD_ITEM(event, "triggerConditions", cond); + JSON_CHECK_ADD_ITEM(event, "triggerCondition", cond); cond = NULL; - // convert json object to string value item.gid = pSessionKey->groupId; - item.skey = pSessionKey->win.skey; - item.isEnd = (eventType == SNOTIFY_EVENT_WINDOW_CLOSE); - item.content = cJSON_PrintUnformatted(event); - QUERY_CHECK_NULL(taosArrayPush(sup->pWindowEvents, &item), code, lino, _end, terrno); - item.content = NULL; + item.win = pSessionKey->win; + item.eventType = eventType; + item.pJson = event; + event = NULL; + origSize = taosHashGetSize(sup->pWindowEventHashMap); + code = taosHashPut(sup->pWindowEventHashMap, &item, NOTIFY_EVENT_KEY_SIZE, &item, sizeof(SStreamNotifyEvent)); + QUERY_CHECK_CODE(code, lino, _end); + item.pJson = NULL; + + endTime = taosGetMonoTimestampMs(); + pNotifyEventStat->notifyEventAddTimes++; + pNotifyEventStat->notifyEventAddElems += taosHashGetSize(sup->pWindowEventHashMap) - origSize; + pNotifyEventStat->notifyEventAddCostSec += (endTime - startTime) / 1000.0; + pNotifyEventStat->notifyEventHoldElems = taosHashGetSize(sup->pWindowEventHashMap); _end: if (code != TSDB_CODE_SUCCESS) { @@ -325,50 +461,204 @@ _end: return code; } -int32_t addAggResultNotifyEvent(const SSDataBlock* pResultBlock, const SSchemaWrapper* pSchemaWrapper, - SStreamNotifyEventSupp* sup) { +int32_t addStateAggNotifyEvent(EStreamNotifyEventType eventType, const SSessionKey* pSessionKey, + const SStateKeys* pCurState, const SStateKeys* pAnotherState, bool onlyUpdate, + SStreamNotifyEventSupp* sup, STaskNotifyEventStat* pNotifyEventStat) { int32_t code = TSDB_CODE_SUCCESS; int32_t lino = 0; - SNode * node = NULL; cJSON* event = NULL; - cJSON* result = NULL; + int32_t origSize = 0; + int64_t startTime = 0; + int64_t endTime = 0; + SStreamNotifyEvent item = {0}; + + QUERY_CHECK_NULL(pCurState, code, lino, _end, TSDB_CODE_INVALID_PARA); + QUERY_CHECK_NULL(sup, code, lino, _end, TSDB_CODE_INVALID_PARA); + QUERY_CHECK_NULL(sup->windowType, code, lino, _end, TSDB_CODE_INVALID_PARA); + QUERY_CHECK_NULL(pNotifyEventStat, code, lino, _end, TSDB_CODE_INVALID_PARA); + + item.gid = pSessionKey->groupId; + item.win = pSessionKey->win; + item.eventType = eventType; + // Check if the notify event exists for update + if (onlyUpdate && taosHashGet(sup->pWindowEventHashMap, &item, NOTIFY_EVENT_KEY_SIZE) == NULL) { + goto _end; + } + + startTime = taosGetMonoTimestampMs(); + event = createBasicAggNotifyEvent(sup->windowType, eventType, pSessionKey); + QUERY_CHECK_NULL(event, code, lino, _end, terrno); + + // add state value + if (eventType == SNOTIFY_EVENT_WINDOW_OPEN) { + if (pAnotherState) { + code = jsonAddColumnField("prevState", pAnotherState->type, pAnotherState->isNull, pAnotherState->pData, event); + QUERY_CHECK_CODE(code, lino, _end); + } else { + code = jsonAddColumnField("prevState", pCurState->type, true, NULL, event); + QUERY_CHECK_CODE(code, lino, _end); + } + } + code = jsonAddColumnField("curState", pCurState->type, pCurState->isNull, pCurState->pData, event); + QUERY_CHECK_CODE(code, lino, _end); + if (eventType == SNOTIFY_EVENT_WINDOW_CLOSE) { + if (pAnotherState) { + code = jsonAddColumnField("nextState", pAnotherState->type, pAnotherState->isNull, pAnotherState->pData, event); + QUERY_CHECK_CODE(code, lino, _end); + } else { + code = jsonAddColumnField("nextState", pCurState->type, true, NULL, event); + QUERY_CHECK_CODE(code, lino, _end); + } + } + + item.pJson = event; + event = NULL; + origSize = taosHashGetSize(sup->pWindowEventHashMap); + code = taosHashPut(sup->pWindowEventHashMap, &item, NOTIFY_EVENT_KEY_SIZE, &item, sizeof(SStreamNotifyEvent)); + QUERY_CHECK_CODE(code, lino, _end); + item.pJson = NULL; + + endTime = taosGetMonoTimestampMs(); + pNotifyEventStat->notifyEventAddTimes++; + pNotifyEventStat->notifyEventAddElems += taosHashGetSize(sup->pWindowEventHashMap) - origSize; + pNotifyEventStat->notifyEventAddCostSec += (endTime - startTime) / 1000.0; + pNotifyEventStat->notifyEventHoldElems = taosHashGetSize(sup->pWindowEventHashMap); + +_end: + if (code != TSDB_CODE_SUCCESS) { + qError("%s failed at line %d since %s", __func__, lino, tstrerror(code)); + } + destroyStreamWindowEvent(&item); + if (event != NULL) { + cJSON_Delete(event); + } + return code; +} + +static int32_t addNormalAggNotifyEvent(EStreamNotifyEventType eventType, const SSessionKey* pSessionKey, + SStreamNotifyEventSupp* sup, STaskNotifyEventStat* pNotifyEventStat) { + int32_t code = TSDB_CODE_SUCCESS; + int32_t lino = 0; + cJSON* event = NULL; + int32_t origSize = 0; + int64_t startTime = 0; + int64_t endTime = 0; + SStreamNotifyEvent item = {0}; + + QUERY_CHECK_NULL(pSessionKey, code, lino, _end, TSDB_CODE_INVALID_PARA); + QUERY_CHECK_NULL(sup, code, lino, _end, TSDB_CODE_INVALID_PARA); + QUERY_CHECK_NULL(sup->windowType, code, lino, _end, TSDB_CODE_INVALID_PARA); + QUERY_CHECK_NULL(pNotifyEventStat, code, lino, _end, TSDB_CODE_INVALID_PARA); + + startTime = taosGetMonoTimestampMs(); + event = createBasicAggNotifyEvent(sup->windowType, eventType, pSessionKey); + QUERY_CHECK_NULL(event, code, lino, _end, terrno); + + item.gid = pSessionKey->groupId; + item.win = pSessionKey->win; + item.eventType = eventType; + item.pJson = event; + event = NULL; + origSize = taosHashGetSize(sup->pWindowEventHashMap); + code = taosHashPut(sup->pWindowEventHashMap, &item, NOTIFY_EVENT_KEY_SIZE, &item, sizeof(SStreamNotifyEvent)); + QUERY_CHECK_CODE(code, lino, _end); + item.pJson = NULL; + + endTime = taosGetMonoTimestampMs(); + pNotifyEventStat->notifyEventAddTimes++; + pNotifyEventStat->notifyEventAddElems += taosHashGetSize(sup->pWindowEventHashMap) - origSize; + pNotifyEventStat->notifyEventAddCostSec += (endTime - startTime) / 1000.0; + pNotifyEventStat->notifyEventHoldElems = taosHashGetSize(sup->pWindowEventHashMap); + +_end: + if (code != TSDB_CODE_SUCCESS) { + qError("%s failed at line %d since %s", __func__, lino, tstrerror(code)); + } + destroyStreamWindowEvent(&item); + if (event != NULL) { + cJSON_Delete(event); + } + return code; +} + +int32_t addIntervalAggNotifyEvent(EStreamNotifyEventType eventType, const SSessionKey* pSessionKey, + SStreamNotifyEventSupp* sup, STaskNotifyEventStat* pNotifyEventStat) { + return addNormalAggNotifyEvent(eventType, pSessionKey, sup, pNotifyEventStat); +} + +int32_t addSessionAggNotifyEvent(EStreamNotifyEventType eventType, const SSessionKey* pSessionKey, + SStreamNotifyEventSupp* sup, STaskNotifyEventStat* pNotifyEventStat) { + return addNormalAggNotifyEvent(eventType, pSessionKey, sup, pNotifyEventStat); +} + +int32_t addCountAggNotifyEvent(EStreamNotifyEventType eventType, const SSessionKey* pSessionKey, + SStreamNotifyEventSupp* sup, STaskNotifyEventStat* pNotifyEventStat) { + return addNormalAggNotifyEvent(eventType, pSessionKey, sup, pNotifyEventStat); +} + +int32_t addAggResultNotifyEvent(const SSDataBlock* pResultBlock, const SArray* pSessionKeys, + const SSchemaWrapper* pSchemaWrapper, SStreamNotifyEventSupp* sup, + STaskNotifyEventStat* pNotifyEventStat) { + int32_t code = TSDB_CODE_SUCCESS; + int32_t lino = 0; + cJSON* result = NULL; + int32_t origSize = 0; + int64_t startTime = 0; + int64_t endTime = 0; SStreamNotifyEvent item = {0}; - SColumnInfoData* pWstartCol = NULL; QUERY_CHECK_NULL(pResultBlock, code, lino, _end, TSDB_CODE_INVALID_PARA); + QUERY_CHECK_NULL(pSessionKeys, code, lino, _end, TSDB_CODE_INVALID_PARA); QUERY_CHECK_NULL(pSchemaWrapper, code, lino, _end, TSDB_CODE_INVALID_PARA); QUERY_CHECK_NULL(sup, code, lino, _end, TSDB_CODE_INVALID_PARA); + QUERY_CHECK_NULL(pNotifyEventStat, code, lino, _end, TSDB_CODE_INVALID_PARA); qDebug("add %" PRId64 " stream notify results from window agg", pResultBlock->info.rows); + startTime = taosGetMonoTimestampMs(); + origSize = taosHashGetSize(sup->pWindowEventHashMap); - pWstartCol = taosArrayGet(pResultBlock->pDataBlock, 0); - for (int32_t i = 0; i< pResultBlock->info.rows; ++i) { - event = cJSON_CreateObject(); - QUERY_CHECK_NULL(event, code, lino, _end, TSDB_CODE_OUT_OF_MEMORY); + for (int32_t i = 0; i < pResultBlock->info.rows; ++i) { + const SSessionKey* pSessionKey = taosArrayGet(pSessionKeys, i); + item.gid = pSessionKey->groupId; + item.win = pSessionKey->win; + item.eventType = SNOTIFY_EVENT_WINDOW_CLOSE; + SStreamNotifyEvent* pItem = taosHashGet(sup->pWindowEventHashMap, &item, NOTIFY_EVENT_KEY_SIZE); + if (pItem == NULL) { + item.pJson = createBasicAggNotifyEvent(sup->windowType, SNOTIFY_EVENT_WINDOW_CLOSE, pSessionKey); + QUERY_CHECK_NULL(item.pJson, code, lino, _end, terrno); + if (strcmp(sup->windowType, "Event") == 0) { + JSON_CHECK_ADD_ITEM(item.pJson, "triggerCondition", cJSON_CreateNull()); + } else if (strcmp(sup->windowType, "State") == 0) { + JSON_CHECK_ADD_ITEM(item.pJson, "curState", cJSON_CreateNull()); + JSON_CHECK_ADD_ITEM(item.pJson, "nextState", cJSON_CreateNull()); + } + code = taosHashPut(sup->pWindowEventHashMap, &item, NOTIFY_EVENT_KEY_SIZE, &item, sizeof(SStreamNotifyEvent)); + QUERY_CHECK_CODE(code, lino, _end); + item.pJson = NULL; + pItem = taosHashGet(sup->pWindowEventHashMap, &item, NOTIFY_EVENT_KEY_SIZE); + QUERY_CHECK_NULL(pItem, code, lino, _end, TSDB_CODE_INTERNAL_ERROR); + } // convert the result row into json result = cJSON_CreateObject(); QUERY_CHECK_NULL(result, code, lino, _end, TSDB_CODE_OUT_OF_MEMORY); for (int32_t j = 0; j < pSchemaWrapper->nCols; ++j) { - SSchema *pCol = pSchemaWrapper->pSchema + j; - SColumnInfoData *pColData = taosArrayGet(pResultBlock->pDataBlock, pCol->colId - 1); - code = jsonAddColumnField(pCol->name, pColData, i, result); + const SSchema* pCol = pSchemaWrapper->pSchema + j; + const SColumnInfoData* pColData = taosArrayGet(pResultBlock->pDataBlock, pCol->colId - 1); + code = jsonAddColumnField(pCol->name, pColData->info.type, colDataIsNull_s(pColData, i), + colDataGetData(pColData, i), result); QUERY_CHECK_CODE(code, lino, _end); } - JSON_CHECK_ADD_ITEM(event, "result", result); + JSON_CHECK_ADD_ITEM(pItem->pJson, "result", result); result = NULL; - - item.gid = pResultBlock->info.id.groupId; - item.skey = *(uint64_t*)colDataGetNumData(pWstartCol, i); - item.content = cJSON_PrintUnformatted(event); - code = taosHashPut(sup->pResultHashMap, &item.gid, sizeof(item.gid) + sizeof(item.skey), &item, sizeof(item)); - TSDB_CHECK_CODE(code, lino, _end); - item.content = NULL; - - cJSON_Delete(event); - event = NULL; } + endTime = taosGetMonoTimestampMs(); + pNotifyEventStat->notifyEventAddTimes++; + pNotifyEventStat->notifyEventAddElems += taosHashGetSize(sup->pWindowEventHashMap) - origSize; + pNotifyEventStat->notifyEventAddCostSec += (endTime - startTime) / 1000.0; + pNotifyEventStat->notifyEventHoldElems = taosHashGetSize(sup->pWindowEventHashMap); + _end: if (code != TSDB_CODE_SUCCESS) { qError("%s failed at line %d since %s", __func__, lino, tstrerror(code)); @@ -377,8 +667,38 @@ _end: if (result != NULL) { cJSON_Delete(result); } - if (event != NULL) { - cJSON_Delete(event); + return code; +} + +int32_t addAggDeleteNotifyEvent(const SSDataBlock* pDeleteBlock, SStreamNotifyEventSupp* sup, + STaskNotifyEventStat* pNotifyEventStat) { + int32_t code = TSDB_CODE_SUCCESS; + int32_t lino = 0; + SSessionKey sessionKey = {0}; + SColumnInfoData* pWstartCol = NULL; + SColumnInfoData* pWendCol = NULL; + SColumnInfoData* pGroupIdCol = NULL; + + QUERY_CHECK_NULL(pDeleteBlock, code, lino, _end, TSDB_CODE_INVALID_PARA); + QUERY_CHECK_NULL(sup, code, lino, _end, TSDB_CODE_INVALID_PARA); + QUERY_CHECK_NULL(pNotifyEventStat, code, lino, _end, TSDB_CODE_INVALID_PARA); + + qDebug("add %" PRId64 " stream notify delete events from window agg", pDeleteBlock->info.rows); + + pWstartCol = taosArrayGet(pDeleteBlock->pDataBlock, START_TS_COLUMN_INDEX); + pWendCol = taosArrayGet(pDeleteBlock->pDataBlock, END_TS_COLUMN_INDEX); + pGroupIdCol = taosArrayGet(pDeleteBlock->pDataBlock, GROUPID_COLUMN_INDEX); + for (int32_t i = 0; i < pDeleteBlock->info.rows; ++i) { + sessionKey.win.skey = *(int64_t*)colDataGetNumData(pWstartCol, i); + sessionKey.win.ekey = *(int64_t*)colDataGetNumData(pWendCol, i); + sessionKey.groupId = *(uint64_t*)colDataGetNumData(pGroupIdCol, i); + code = addNormalAggNotifyEvent(SNOTIFY_EVENT_WINDOW_INVALIDATION, &sessionKey, sup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } + +_end: + if (code != TSDB_CODE_SUCCESS) { + qError("%s failed at line %d since %s", __func__, lino, tstrerror(code)); } return code; } @@ -418,97 +738,42 @@ _end: return code; } -static int32_t streamNotifyFillTableName(const char* tableName, const SStreamNotifyEvent* pEvent, - const SStreamNotifyEvent* pResult, char** pVal) { - int32_t code = TSDB_CODE_SUCCESS; - int32_t lino = 0; - static const char* prefix = "{\"tableName\":\""; - uint64_t prefixLen = 0; - uint64_t nameLen = 0; - uint64_t eventLen = 0; - uint64_t resultLen = 0; - uint64_t valLen = 0; - char* val = NULL; - char* p = NULL; - - QUERY_CHECK_NULL(tableName, code, lino, _end, TSDB_CODE_INVALID_PARA); - QUERY_CHECK_NULL(pEvent, code, lino , _end, TSDB_CODE_INVALID_PARA); - QUERY_CHECK_NULL(pVal, code, lino , _end, TSDB_CODE_INVALID_PARA); - - *pVal = NULL; - prefixLen = strlen(prefix); - nameLen = strlen(tableName); - eventLen = strlen(pEvent->content); - - if (pResult != NULL) { - resultLen = strlen(pResult->content); - valLen = VARSTR_HEADER_SIZE + prefixLen + nameLen + eventLen + resultLen; - } else { - valLen = VARSTR_HEADER_SIZE + prefixLen + nameLen + eventLen + 1; - } - val = taosMemoryMalloc(valLen); - QUERY_CHECK_NULL(val, code, lino, _end, terrno); - varDataSetLen(val, valLen - VARSTR_HEADER_SIZE); - - p = varDataVal(val); - TAOS_STRNCPY(p, prefix, prefixLen); - p += prefixLen; - TAOS_STRNCPY(p, tableName, nameLen); - p += nameLen; - *(p++) = '\"'; - TAOS_STRNCPY(p, pEvent->content, eventLen); - *p = ','; - - if (pResult != NULL) { - p += eventLen - 1; - TAOS_STRNCPY(p, pResult->content, resultLen); - *p = ','; - } - *pVal = val; - val = NULL; - -_end: - if (code != TSDB_CODE_SUCCESS) { - qError("%s failed at line %d since %s", __func__, lino, tstrerror(code)); - } - if (val != NULL) { - taosMemoryFreeClear(val); - } - return code; -} - -int32_t buildNotifyEventBlock(const SExecTaskInfo* pTaskInfo, SStreamNotifyEventSupp* sup) { +int32_t buildNotifyEventBlock(const SExecTaskInfo* pTaskInfo, SStreamNotifyEventSupp* sup, + STaskNotifyEventStat* pNotifyEventStat) { int32_t code = TSDB_CODE_SUCCESS; int32_t lino = 0; - SColumnInfoData* pEventStrCol = NULL; int32_t nWindowEvents = 0; - int32_t nWindowResults = 0; - char* val = NULL; + SColumnInfoData* pEventStrCol = NULL; + int64_t startTime = 0; + int64_t endTime = 0; + void* pIter = NULL; - if (pTaskInfo == NULL || sup == NULL) { + if (pTaskInfo == NULL || sup == NULL || sup->pEventBlock == NULL || pNotifyEventStat == NULL) { goto _end; } QUERY_CHECK_NULL(sup->pEventBlock, code, lino, _end, TSDB_CODE_INVALID_PARA); - blockDataCleanup(sup->pEventBlock); - nWindowEvents = taosArrayGetSize(sup->pWindowEvents); - nWindowResults = taosHashGetSize(sup->pResultHashMap); - qDebug("start to build stream notify event block, nWindowEvents: %d, nWindowResults: %d", nWindowEvents, - nWindowResults); - if (nWindowEvents == 0) { - goto _end; - } - code = blockDataEnsureCapacity(sup->pEventBlock, nWindowEvents); - QUERY_CHECK_CODE(code, lino, _end); + startTime = taosGetMonoTimestampMs(); + blockDataCleanup(sup->pEventBlock); + nWindowEvents = taosHashGetSize(sup->pWindowEventHashMap); + qDebug("start to build stream notify event block, nWindowEvents: %d", nWindowEvents); pEventStrCol = taosArrayGet(sup->pEventBlock->pDataBlock, NOTIFY_EVENT_STR_COLUMN_INDEX); QUERY_CHECK_NULL(pEventStrCol, code, lino, _end, terrno); - for (int32_t i = 0; i < nWindowEvents; ++i) { - SStreamNotifyEvent* pResult = NULL; - SStreamNotifyEvent* pEvent = taosArrayGet(sup->pWindowEvents, i); - char* tableName = taosHashGet(sup->pTableNameHashMap, &pEvent->gid, sizeof(pEvent->gid)); + // Append all events content into data block. + pIter = taosHashIterate(sup->pWindowEventHashMap, NULL); + while (pIter) { + const SStreamNotifyEvent* pEvent = (const SStreamNotifyEvent*)pIter; + pIter = taosHashIterate(sup->pWindowEventHashMap, pIter); + if (pEvent->eventType == SNOTIFY_EVENT_WINDOW_CLOSE && !cJSON_HasObjectItem(pEvent->pJson, "result")) { + // current WINDOW_CLOSE event cannot be pushed yet due to watermark + continue; + } + + // get name of the dest child table + char* tableName = taosHashGet(sup->pTableNameHashMap, &pEvent->gid, sizeof(&pEvent->gid)); if (tableName == NULL) { code = streamNotifyGetDestTableName(pTaskInfo, pEvent->gid, &tableName); QUERY_CHECK_CODE(code, lino, _end); @@ -518,32 +783,73 @@ int32_t buildNotifyEventBlock(const SExecTaskInfo* pTaskInfo, SStreamNotifyEvent tableName = taosHashGet(sup->pTableNameHashMap, &pEvent->gid, sizeof(pEvent->gid)); QUERY_CHECK_NULL(tableName, code, lino, _end, TSDB_CODE_INTERNAL_ERROR); } - if (pEvent->isEnd) { - pResult = taosHashGet(sup->pResultHashMap, &pEvent->gid, sizeof(pEvent->gid) + sizeof(pEvent->skey)); - QUERY_CHECK_NULL(pResult, code, lino, _end, TSDB_CODE_INTERNAL_ERROR); - } - code = streamNotifyFillTableName(tableName, pEvent, pResult, &val); + JSON_CHECK_ADD_ITEM(pEvent->pJson, "tableName", cJSON_CreateStringReference(tableName)); + + // convert the json object into string and append it into the block + char* str = cJSON_PrintUnformatted(pEvent->pJson); + QUERY_CHECK_NULL(str, code, lino, _end, TSDB_CODE_OUT_OF_MEMORY); + int32_t len = strlen(str); + code = varColSetVarData(pEventStrCol, sup->pEventBlock->info.rows, str, len, false); + cJSON_free(str); QUERY_CHECK_CODE(code, lino, _end); - code = colDataSetVal(pEventStrCol, i, val, false); - QUERY_CHECK_CODE(code, lino, _end); - taosMemoryFreeClear(val); sup->pEventBlock->info.rows++; + code = taosHashRemove(sup->pWindowEventHashMap, pEvent, NOTIFY_EVENT_KEY_SIZE); + if (code == TSDB_CODE_NOT_FOUND) { + code = TSDB_CODE_SUCCESS; + } + QUERY_CHECK_CODE(code, lino, _end); + if (sup->pEventBlock->info.rows >= sup->pEventBlock->info.capacity) { + break; + } } if (taosHashGetMemSize(sup->pTableNameHashMap) >= NOTIFY_EVENT_NAME_CACHE_LIMIT_MB * 1024 * 1024) { taosHashClear(sup->pTableNameHashMap); } + endTime = taosGetMonoTimestampMs(); + if (sup->pEventBlock->info.rows > 0) { + pNotifyEventStat->notifyEventPushTimes++; + pNotifyEventStat->notifyEventPushElems += sup->pEventBlock->info.rows; + pNotifyEventStat->notifyEventPushCostSec += (endTime - startTime) / 1000.0; + } + pNotifyEventStat->notifyEventHoldElems = taosHashGetSize(sup->pWindowEventHashMap); + _end: if (code != TSDB_CODE_SUCCESS) { qError("%s failed at line %d since %s", __func__, lino, tstrerror(code)); } - if (val != NULL) { - taosMemoryFreeClear(val); - } - if (sup != NULL) { - taosArrayClearEx(sup->pWindowEvents, destroyStreamWindowEvent); - taosHashClear(sup->pResultHashMap); + if (pIter) { + taosHashCancelIterate(sup->pWindowEventHashMap, pIter); + } + return code; +} + +int32_t removeOutdatedNotifyEvents(STimeWindowAggSupp* pTwSup, SStreamNotifyEventSupp* sup, + STaskNotifyEventStat* pNotifyEventStat) { + int32_t code = TSDB_CODE_SUCCESS; + int32_t lino = 0; + void* pIter = NULL; + + if (pTwSup || sup == NULL || pNotifyEventStat == NULL) { + goto _end; + } + + pIter = taosHashIterate(sup->pWindowEventHashMap, NULL); + while (pIter) { + const SStreamNotifyEvent* pEvent = (const SStreamNotifyEvent*)pIter; + pIter = taosHashIterate(sup->pWindowEventHashMap, pIter); + if (isOverdue(pEvent->win.ekey, pTwSup)) { + code = taosHashRemove(sup->pWindowEventHashMap, pEvent, NOTIFY_EVENT_KEY_SIZE); + QUERY_CHECK_CODE(code, lino, _end); + } + } + + pNotifyEventStat->notifyEventHoldElems = taosHashGetSize(sup->pWindowEventHashMap); + +_end: + if (code != TSDB_CODE_SUCCESS) { + qError("%s failed at line %d since %s", __func__, lino, tstrerror(code)); } return code; } diff --git a/source/libs/executor/src/streamintervalsliceoperator.c b/source/libs/executor/src/streamintervalsliceoperator.c index 44799f193b..cc06f5b693 100644 --- a/source/libs/executor/src/streamintervalsliceoperator.c +++ b/source/libs/executor/src/streamintervalsliceoperator.c @@ -87,23 +87,48 @@ static int32_t buildIntervalSliceResult(SOperatorInfo* pOperator, SSDataBlock** SExecTaskInfo* pTaskInfo = pOperator->pTaskInfo; uint16_t opType = pOperator->operatorType; SStreamAggSupporter* pAggSup = &pInfo->streamAggSup; - + SStreamNotifyEventSupp* pNotifySup = &pInfo->basic.notifyEventSup; + STaskNotifyEventStat* pNotifyEventStat = pTaskInfo->streamInfo.pNotifyEventStat; + bool addNotifyEvent = false; + addNotifyEvent = IS_NORMAL_INTERVAL_OP(pOperator) && + BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_CLOSE); doBuildDeleteResultImpl(&pInfo->streamAggSup.stateStore, pInfo->streamAggSup.pState, pInfo->pDelWins, &pInfo->delIndex, pInfo->pDelRes); if (pInfo->pDelRes->info.rows != 0) { // process the rest of the data printDataBlock(pInfo->pDelRes, getStreamOpName(opType), GET_TASKID(pTaskInfo)); + if (addNotifyEvent) { + code = addAggDeleteNotifyEvent(pInfo->pDelRes, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } (*ppRes) = pInfo->pDelRes; return code; } - doBuildStreamIntervalResult(pOperator, pInfo->streamAggSup.pState, pInfo->binfo.pRes, &pInfo->groupResInfo); + doBuildStreamIntervalResult(pOperator, pInfo->streamAggSup.pState, pInfo->binfo.pRes, &pInfo->groupResInfo, + addNotifyEvent ? pNotifySup->pSessionKeys : NULL); if (pInfo->binfo.pRes->info.rows != 0) { printDataBlock(pInfo->binfo.pRes, getStreamOpName(opType), GET_TASKID(pTaskInfo)); + if (addNotifyEvent) { + code = addAggResultNotifyEvent(pInfo->binfo.pRes, pNotifySup->pSessionKeys, + pTaskInfo->streamInfo.notifyResultSchema, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } (*ppRes) = pInfo->binfo.pRes; goto _end; } + code = buildNotifyEventBlock(pTaskInfo, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + if (pNotifySup->pEventBlock && pNotifySup->pEventBlock->info.rows > 0) { + printDataBlock(pNotifySup->pEventBlock, getStreamOpName(opType), GET_TASKID(pTaskInfo)); + (*ppRes) = pNotifySup->pEventBlock; + return code; + } + + code = removeOutdatedNotifyEvents(&pInfo->twAggSup, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + _end: if (code != TSDB_CODE_SUCCESS) { qError("%s failed at line %d since %s", __func__, lino, tstrerror(code)); @@ -316,6 +341,14 @@ static int32_t doStreamIntervalSliceAggImpl(SOperatorInfo* pOperator, SSDataBloc code = setIntervalSliceOutputBuf(&curPoint, pSup->pCtx, numOfOutput, pSup->rowEntryInfoOffset); QUERY_CHECK_CODE(code, lino, _end); + if (winCode != TSDB_CODE_SUCCESS && IS_NORMAL_INTERVAL_OP(pOperator) && + BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_OPEN)) { + SSessionKey key = {.win = curWin, .groupId = groupId}; + code = addIntervalAggNotifyEvent(SNOTIFY_EVENT_WINDOW_OPEN, &key, &pInfo->basic.notifyEventSup, + pTaskInfo->streamInfo.pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } + resetIntervalSliceFunctionKey(pSup->pCtx, numOfOutput); if (pInfo->hasInterpoFunc && IS_VALID_WIN_KEY(prevPoint.winKey.win.skey) && curPoint.winKey.win.skey != curTs) { doStreamSliceInterpolation(prevPoint.pLastRow, curPoint.winKey.win.skey, curTs, pBlock, startPos, &pOperator->exprSupp, INTERVAL_SLICE_START, pInfo->pOffsetInfo); @@ -652,8 +685,9 @@ int32_t createStreamIntervalSliceOperatorInfo(SOperatorInfo* downstream, SPhysiN optrDefaultBufFn, NULL, optrDefaultGetNextExtFn, NULL); setOperatorStreamStateFn(pOperator, streamIntervalSliceReleaseState, streamIntervalSliceReloadState); - code = initStreamBasicInfo(&pInfo->basic); + code = initStreamBasicInfo(&pInfo->basic, pOperator); QUERY_CHECK_CODE(code, lino, _error); + if (downstream) { code = initIntervalSliceDownStream(downstream, &pInfo->streamAggSup, pPhyNode->type, pInfo->primaryTsIndex, &pInfo->twAggSup, &pInfo->basic, &pInfo->interval, pInfo->hasInterpoFunc); diff --git a/source/libs/executor/src/streamtimesliceoperator.c b/source/libs/executor/src/streamtimesliceoperator.c index 4fe8efe397..681e07f452 100644 --- a/source/libs/executor/src/streamtimesliceoperator.c +++ b/source/libs/executor/src/streamtimesliceoperator.c @@ -150,7 +150,6 @@ void destroyStreamTimeSliceOperatorInfo(void* param) { &pInfo->groupResInfo); pInfo->pOperator = NULL; } - destroyStreamBasicInfo(&pInfo->basic); colDataDestroy(&pInfo->twAggSup.timeWindowData); destroyStreamAggSupporter(&pInfo->streamAggSup); resetPrevAndNextWindow(pInfo->pFillSup); @@ -2202,7 +2201,7 @@ int32_t createStreamTimeSliceOperatorInfo(SOperatorInfo* downstream, SPhysiNode* optrDefaultBufFn, NULL, optrDefaultGetNextExtFn, NULL); setOperatorStreamStateFn(pOperator, streamTimeSliceReleaseState, streamTimeSliceReloadState); - code = initStreamBasicInfo(&pInfo->basic); + code = initStreamBasicInfo(&pInfo->basic, pOperator); QUERY_CHECK_CODE(code, lino, _error); if (downstream) { code = initTimeSliceDownStream(downstream, &pInfo->streamAggSup, pOperator->operatorType, pInfo->primaryTsIndex, diff --git a/source/libs/executor/src/streamtimewindowoperator.c b/source/libs/executor/src/streamtimewindowoperator.c index 031d2e8bdc..3b799eea23 100644 --- a/source/libs/executor/src/streamtimewindowoperator.c +++ b/source/libs/executor/src/streamtimewindowoperator.c @@ -32,11 +32,6 @@ #define IS_MID_INTERVAL_OP(op) ((op)->operatorType == QUERY_NODE_PHYSICAL_PLAN_STREAM_MID_INTERVAL) #define IS_FINAL_SESSION_OP(op) ((op)->operatorType == QUERY_NODE_PHYSICAL_PLAN_STREAM_FINAL_SESSION) -#define IS_NORMAL_SESSION_OP(op) \ - ((op)->operatorType == QUERY_NODE_PHYSICAL_PLAN_STREAM_SESSION || \ - (op)->operatorType == QUERY_NODE_PHYSICAL_PLAN_STREAM_FINAL_SESSION) - -#define IS_NORMAL_STATE_OP(op) ((op)->operatorType == QUERY_NODE_PHYSICAL_PLAN_STREAM_STATE) #define DEAULT_DELETE_MARK INT64_MAX #define STREAM_INTERVAL_OP_STATE_NAME "StreamIntervalHistoryState" @@ -480,6 +475,8 @@ void destroyStreamFinalIntervalOperatorInfo(void* param) { false); pInfo->pOperator = NULL; } + + destroyStreamBasicInfo(&pInfo->basic); cleanupAggSup(&pInfo->aggSup); clearGroupResInfo(&pInfo->groupResInfo); taosArrayDestroyP(pInfo->pUpdated, destroyFlusedPos); @@ -917,7 +914,7 @@ int32_t getOutputBuf(void* pState, SRowBuffPos* pPos, SResultRow** pResult, SSta } void buildDataBlockFromGroupRes(SOperatorInfo* pOperator, void* pState, SSDataBlock* pBlock, SExprSupp* pSup, - SGroupResInfo* pGroupResInfo) { + SGroupResInfo* pGroupResInfo, SArray* pSessionKeys) { int32_t code = TSDB_CODE_SUCCESS; int32_t lino = 0; SExecTaskInfo* pTaskInfo = pOperator->pTaskInfo; @@ -991,6 +988,14 @@ void buildDataBlockFromGroupRes(SOperatorInfo* pOperator, void* pState, SSDataBl } } + if (pSessionKeys) { + SSessionKey key = {.groupId = groupId, .win = pRow->win}; + for (int32_t j = 0; j < pRow->numOfRows; ++j) { + const void* px = taosArrayPush(pSessionKeys, &key); + QUERY_CHECK_NULL(px, code, lino, _end, terrno); + } + } + pBlock->info.rows += pRow->numOfRows; } @@ -1005,19 +1010,20 @@ _end: } void doBuildStreamIntervalResult(SOperatorInfo* pOperator, void* pState, SSDataBlock* pBlock, - SGroupResInfo* pGroupResInfo) { + SGroupResInfo* pGroupResInfo, SArray* pSessionKeys) { SExecTaskInfo* pTaskInfo = pOperator->pTaskInfo; // set output datablock version pBlock->info.version = pTaskInfo->version; blockDataCleanup(pBlock); + taosArrayClear(pSessionKeys); if (!hasRemainResults(pGroupResInfo)) { return; } // clear the existed group id pBlock->info.id.groupId = 0; - buildDataBlockFromGroupRes(pOperator, pState, pBlock, &pOperator->exprSupp, pGroupResInfo); + buildDataBlockFromGroupRes(pOperator, pState, pBlock, &pOperator->exprSupp, pGroupResInfo, pSessionKeys); } static int32_t getNextQualifiedFinalWindow(SInterval* pInterval, STimeWindow* pNext, SDataBlockInfo* pDataBlockInfo, @@ -1150,6 +1156,14 @@ static int32_t doStreamIntervalAggImpl(SOperatorInfo* pOperator, SSDataBlock* pS pSup->rowEntryInfoOffset, &pInfo->aggSup, &pInfo->stateStore, &winCode); QUERY_CHECK_CODE(code, lino, _end); + if (winCode != TSDB_CODE_SUCCESS && IS_NORMAL_INTERVAL_OP(pOperator) && + BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_OPEN)) { + SSessionKey key = {.win = nextWin, .groupId = groupId}; + code = addIntervalAggNotifyEvent(SNOTIFY_EVENT_WINDOW_OPEN, &key, &pInfo->basic.notifyEventSup, + pTaskInfo->streamInfo.pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } + pResult = (SResultRow*)pResPos->pRowBuff; if (IS_FINAL_INTERVAL_OP(pOperator)) { @@ -1371,7 +1385,10 @@ int32_t doStreamIntervalEncodeOpState(void** buf, int32_t len, SOperatorInfo* pO // 5.dataVersion tlen += taosEncodeFixedI64(buf, pInfo->dataVersion); - // 6.checksum + // 6.basicInfo + tlen += encodeStreamBasicInfo(buf, &pInfo->basic); + + // 7.checksum if (buf) { uint32_t cksum = taosCalcChecksum(0, pData, len - sizeof(uint32_t)); tlen += taosEncodeFixedU32(buf, cksum); @@ -1387,18 +1404,20 @@ void doStreamIntervalDecodeOpState(void* buf, int32_t len, SOperatorInfo* pOpera int32_t lino = 0; SStreamIntervalOperatorInfo* pInfo = pOperator->info; SExecTaskInfo* pTaskInfo = pOperator->pTaskInfo; + void* pDataEnd = POINTER_SHIFT(buf, len); if (!pInfo) { code = TSDB_CODE_FAILED; QUERY_CHECK_CODE(code, lino, _end); } - // 6.checksum + // 7.checksum int32_t dataLen = len - sizeof(uint32_t); void* pCksum = POINTER_SHIFT(buf, dataLen); if (taosCheckChecksum(buf, dataLen, *(uint32_t*)pCksum) != TSDB_CODE_SUCCESS) { code = TSDB_CODE_FAILED; QUERY_CHECK_CODE(code, lino, _end); } + pDataEnd = pCksum; // 1.pResultRowHashTable int32_t mapSize = 0; @@ -1454,6 +1473,12 @@ void doStreamIntervalDecodeOpState(void* buf, int32_t len, SOperatorInfo* pOpera // 5.dataVersion buf = taosDecodeFixedI64(buf, &pInfo->dataVersion); + // 6.basicInfo + if (buf < pDataEnd) { + code = decodeStreamBasicInfo(&buf, &pInfo->basic); + QUERY_CHECK_CODE(code, lino, _end); + } + _end: if (code != TSDB_CODE_SUCCESS) { qError("%s failed at line %d since %s. task:%s", __func__, lino, tstrerror(code), GET_TASKID(pTaskInfo)); @@ -1503,8 +1528,12 @@ _end: static int32_t buildIntervalResult(SOperatorInfo* pOperator, SSDataBlock** ppRes) { SStreamIntervalOperatorInfo* pInfo = pOperator->info; int32_t code = TSDB_CODE_SUCCESS; + int32_t lino = 0; SExecTaskInfo* pTaskInfo = pOperator->pTaskInfo; uint16_t opType = pOperator->operatorType; + SStreamNotifyEventSupp* pNotifySup = &pInfo->basic.notifyEventSup; + STaskNotifyEventStat* pNotifyEventStat = pTaskInfo->streamInfo.pNotifyEventStat; + bool addNotifyEvent = false; // check if query task is closed or not if (isTaskKilled(pTaskInfo)) { @@ -1512,6 +1541,8 @@ static int32_t buildIntervalResult(SOperatorInfo* pOperator, SSDataBlock** ppRes return code; } + addNotifyEvent = IS_NORMAL_INTERVAL_OP(pOperator) && + BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_CLOSE); if (IS_FINAL_INTERVAL_OP(pOperator)) { doBuildPullDataBlock(pInfo->pPullWins, &pInfo->pullIndex, pInfo->pPullDataRes); if (pInfo->pPullDataRes->info.rows != 0) { @@ -1526,17 +1557,42 @@ static int32_t buildIntervalResult(SOperatorInfo* pOperator, SSDataBlock** ppRes if (pInfo->pDelRes->info.rows != 0) { // process the rest of the data printDataBlock(pInfo->pDelRes, getStreamOpName(opType), GET_TASKID(pTaskInfo)); + if (addNotifyEvent) { + code = addAggDeleteNotifyEvent(pInfo->pDelRes, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } (*ppRes) = pInfo->pDelRes; return code; } - doBuildStreamIntervalResult(pOperator, pInfo->pState, pInfo->binfo.pRes, &pInfo->groupResInfo); + doBuildStreamIntervalResult(pOperator, pInfo->pState, pInfo->binfo.pRes, &pInfo->groupResInfo, + addNotifyEvent ? pNotifySup->pSessionKeys : NULL); if (pInfo->binfo.pRes->info.rows != 0) { printDataBlock(pInfo->binfo.pRes, getStreamOpName(opType), GET_TASKID(pTaskInfo)); + if (addNotifyEvent) { + code = addAggResultNotifyEvent(pInfo->binfo.pRes, pNotifySup->pSessionKeys, + pTaskInfo->streamInfo.notifyResultSchema, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } (*ppRes) = pInfo->binfo.pRes; return code; } + code = buildNotifyEventBlock(pTaskInfo, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + if (pNotifySup->pEventBlock && pNotifySup->pEventBlock->info.rows > 0) { + printDataBlock(pNotifySup->pEventBlock, getStreamOpName(pOperator->operatorType), GET_TASKID(pTaskInfo)); + (*ppRes) = pNotifySup->pEventBlock; + return code; + } + + code = removeOutdatedNotifyEvents(&pInfo->twAggSup, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + +_end: + if (code != TSDB_CODE_SUCCESS) { + qError("%s failed at line %d since %s. task:%s", __func__, lino, tstrerror(code), GET_TASKID(pTaskInfo)); + } (*ppRes) = NULL; return code; } @@ -2023,7 +2079,7 @@ int32_t createStreamFinalIntervalOperatorInfo(SOperatorInfo* downstream, SPhysiN pInfo->pState->pResultRowStore.resultRowGet = getResultRowFromBuf; pInfo->pState->pResultRowStore.resultRowPut = putResultRowToBuf; pInfo->pState->pExprSupp = &pOperator->exprSupp; - + code = pAPI->stateStore.streamFileStateInit(tsStreamBufferSize, sizeof(SWinKey), pInfo->aggSup.resultRowSize, funResSize, compareTs, pInfo->pState, pInfo->twAggSup.deleteMark, GET_TASKID(pTaskInfo), @@ -2069,6 +2125,10 @@ int32_t createStreamFinalIntervalOperatorInfo(SOperatorInfo* downstream, SPhysiN optrDefaultBufFn, NULL, optrDefaultGetNextExtFn, NULL); } setOperatorStreamStateFn(pOperator, streamIntervalReleaseState, streamIntervalReloadState); + + code = initStreamBasicInfo(&pInfo->basic, pOperator); + QUERY_CHECK_CODE(code, lino, _error); + if (pPhyNode->type == QUERY_NODE_PHYSICAL_PLAN_STREAM_SEMI_INTERVAL || pPhyNode->type == QUERY_NODE_PHYSICAL_PLAN_STREAM_MID_INTERVAL) { pInfo->basic.primaryPkIndex = -1; @@ -2120,6 +2180,8 @@ void destroyStreamSessionAggOperatorInfo(void* param) { &pInfo->groupResInfo); pInfo->pOperator = NULL; } + + destroyStreamBasicInfo(&pInfo->basic); destroyStreamAggSupporter(&pInfo->streamAggSup); cleanupExprSupp(&pInfo->scalarSupp); clearGroupResInfo(&pInfo->groupResInfo); @@ -2248,8 +2310,8 @@ int32_t initStreamAggSupporter(SStreamAggSupporter* pSup, SExprSupp* pExpSup, in } if (stateType == STREAM_STATE_BUFF_SORT) { - pSup->pState->pFileState = NULL; - code = pSup->stateStore.streamFileStateInit(tsStreamBufferSize, sizeof(SSessionKey), pSup->resultRowSize, + pSup->pState->pFileState = NULL; + code = pSup->stateStore.streamFileStateInit(tsStreamBufferSize, sizeof(SSessionKey), pSup->resultRowSize, funResSize, sesionTs, pSup->pState, pTwAggSup->deleteMark, taskIdStr, pHandle->checkpointId, stateType, &pSup->pState->pFileState); } else if (stateType == STREAM_STATE_BUFF_HASH_SORT || stateType == STREAM_STATE_BUFF_HASH_SEARCH) { @@ -2309,24 +2371,23 @@ bool inWinRange(STimeWindow* range, STimeWindow* cur) { void clearOutputBuf(void* pState, SRowBuffPos* pPos, SStateStore* pAPI) { pAPI->streamStateClearBuff(pState, pPos); } int32_t setSessionOutputBuf(SStreamAggSupporter* pAggSup, TSKEY startTs, TSKEY endTs, uint64_t groupId, - SResultWindowInfo* pCurWin) { + SResultWindowInfo* pCurWin, int32_t* pWinCode) { int32_t code = TSDB_CODE_SUCCESS; int32_t lino = 0; pCurWin->sessionWin.groupId = groupId; pCurWin->sessionWin.win.skey = startTs; pCurWin->sessionWin.win.ekey = endTs; int32_t size = pAggSup->resultRowSize; - int32_t winCode = TSDB_CODE_SUCCESS; code = pAggSup->stateStore.streamStateSessionAddIfNotExist(pAggSup->pState, &pCurWin->sessionWin, pAggSup->gap, - (void**)&pCurWin->pStatePos, &size, &winCode); + (void**)&pCurWin->pStatePos, &size, pWinCode); QUERY_CHECK_CODE(code, lino, _end); - if (winCode == TSDB_CODE_SUCCESS && !inWinRange(&pAggSup->winRange, &pCurWin->sessionWin.win)) { - winCode = TSDB_CODE_FAILED; + if (*pWinCode == TSDB_CODE_SUCCESS && !inWinRange(&pAggSup->winRange, &pCurWin->sessionWin.win)) { + *pWinCode = TSDB_CODE_FAILED; clearOutputBuf(pAggSup->pState, pCurWin->pStatePos, &pAggSup->pSessionAPI->stateStore); } - if (winCode == TSDB_CODE_SUCCESS) { + if (*pWinCode == TSDB_CODE_SUCCESS) { pCurWin->isOutput = true; if (pCurWin->pStatePos->needFree) { pAggSup->stateStore.streamStateSessionDel(pAggSup->pState, &pCurWin->sessionWin); @@ -2692,9 +2753,17 @@ static void doStreamSessionAggImpl(SOperatorInfo* pOperator, SSDataBlock* pSData continue; } SResultWindowInfo winInfo = {0}; - code = setSessionOutputBuf(pAggSup, startTsCols[i], endTsCols[i], groupId, &winInfo); + int32_t winCode = TSDB_CODE_SUCCESS; + code = setSessionOutputBuf(pAggSup, startTsCols[i], endTsCols[i], groupId, &winInfo, &winCode); QUERY_CHECK_CODE(code, lino, _end); + if (winCode != TSDB_CODE_SUCCESS && IS_NORMAL_SESSION_OP(pOperator) && + BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_OPEN)) { + code = addSessionAggNotifyEvent(SNOTIFY_EVENT_WINDOW_OPEN, &winInfo.sessionWin, &pInfo->basic.notifyEventSup, + pTaskInfo->streamInfo.pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } + // coverity scan error if (!winInfo.pStatePos) { continue; @@ -2908,7 +2977,9 @@ static int32_t rebuildSessionWindow(SOperatorInfo* pOperator, SArray* pWinArray, if (winCode == TSDB_CODE_SUCCESS && inWinRange(&pWinKey->win, &childWin.sessionWin.win)) { if (num == 0) { - code = setSessionOutputBuf(pAggSup, pWinKey->win.skey, pWinKey->win.ekey, pWinKey->groupId, &parentWin); + int32_t winCode = TSDB_CODE_SUCCESS; + code = setSessionOutputBuf(pAggSup, pWinKey->win.skey, pWinKey->win.ekey, pWinKey->groupId, &parentWin, + &winCode); QUERY_CHECK_CODE(code, lino, _end); parentWin.sessionWin = childWin.sessionWin; @@ -3051,7 +3122,7 @@ void initGroupResInfoFromArrayList(SGroupResInfo* pGroupResInfo, SArray* pArrayL } int32_t buildSessionResultDataBlock(SOperatorInfo* pOperator, void* pState, SSDataBlock* pBlock, SExprSupp* pSup, - SGroupResInfo* pGroupResInfo) { + SGroupResInfo* pGroupResInfo, SArray* pSessionKeys) { int32_t code = TSDB_CODE_SUCCESS; int32_t lino = 0; SExecTaskInfo* pTaskInfo = pOperator->pTaskInfo; @@ -3131,6 +3202,13 @@ int32_t buildSessionResultDataBlock(SOperatorInfo* pOperator, void* pState, SSDa } } + if (pSessionKeys) { + for (int32_t j = 0; j < pRow->numOfRows; ++j) { + const void* px = taosArrayPush(pSessionKeys, pKey); + QUERY_CHECK_NULL(px, code, lino, _end, terrno); + } + } + pBlock->info.dataLoad = 1; pBlock->info.rows += pRow->numOfRows; } @@ -3144,7 +3222,8 @@ _end: return code; } -void doBuildSessionResult(SOperatorInfo* pOperator, void* pState, SGroupResInfo* pGroupResInfo, SSDataBlock* pBlock) { +void doBuildSessionResult(SOperatorInfo* pOperator, void* pState, SGroupResInfo* pGroupResInfo, SSDataBlock* pBlock, + SArray* pSessionKeys) { int32_t code = TSDB_CODE_SUCCESS; int32_t lino = 0; SExecTaskInfo* pTaskInfo = pOperator->pTaskInfo; @@ -3152,6 +3231,7 @@ void doBuildSessionResult(SOperatorInfo* pOperator, void* pState, SGroupResInfo* pBlock->info.version = pTaskInfo->version; blockDataCleanup(pBlock); + taosArrayClear(pSessionKeys); if (!hasRemainResults(pGroupResInfo)) { cleanupGroupResInfo(pGroupResInfo); goto _end; @@ -3159,7 +3239,7 @@ void doBuildSessionResult(SOperatorInfo* pOperator, void* pState, SGroupResInfo* // clear the existed group id pBlock->info.id.groupId = 0; - code = buildSessionResultDataBlock(pOperator, pState, pBlock, &pOperator->exprSupp, pGroupResInfo); + code = buildSessionResultDataBlock(pOperator, pState, pBlock, &pOperator->exprSupp, pGroupResInfo, pSessionKeys); QUERY_CHECK_CODE(code, lino, _end); if (pBlock->info.rows == 0) { @@ -3174,23 +3254,60 @@ _end: static int32_t buildSessionResult(SOperatorInfo* pOperator, SSDataBlock** ppRes) { int32_t code = TSDB_CODE_SUCCESS; + int32_t lino = 0; SStreamSessionAggOperatorInfo* pInfo = pOperator->info; SStreamAggSupporter* pAggSup = &pInfo->streamAggSup; SOptrBasicInfo* pBInfo = &pInfo->binfo; SExecTaskInfo* pTaskInfo = pOperator->pTaskInfo; + SStreamNotifyEventSupp* pNotifySup = &pInfo->basic.notifyEventSup; + STaskNotifyEventStat* pNotifyEventStat = pTaskInfo->streamInfo.pNotifyEventStat; + bool addNotifyEvent = false; + addNotifyEvent = IS_NORMAL_SESSION_OP(pOperator) && + BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_CLOSE); doBuildDeleteDataBlock(pOperator, pInfo->pStDeleted, pInfo->pDelRes, &pInfo->pDelIterator); if (pInfo->pDelRes->info.rows > 0) { printDataBlock(pInfo->pDelRes, getStreamOpName(pOperator->operatorType), GET_TASKID(pTaskInfo)); + if (addNotifyEvent) { + code = addAggDeleteNotifyEvent(pInfo->pDelRes, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } (*ppRes) = pInfo->pDelRes; return code; } - doBuildSessionResult(pOperator, pAggSup->pState, &pInfo->groupResInfo, pBInfo->pRes); + doBuildSessionResult(pOperator, pAggSup->pState, &pInfo->groupResInfo, pBInfo->pRes, + addNotifyEvent ? pNotifySup->pSessionKeys : NULL); if (pBInfo->pRes->info.rows > 0) { printDataBlock(pBInfo->pRes, getStreamOpName(pOperator->operatorType), GET_TASKID(pTaskInfo)); + if (addNotifyEvent) { + // Adjust the window end time based on the Session Window gap + for (int32_t i = 0; i < taosArrayGetSize(pNotifySup->pSessionKeys); ++i) { + SSessionKey* pKey = taosArrayGet(pNotifySup->pSessionKeys, i); + pKey->win.ekey += pAggSup->gap; + } + code = addAggResultNotifyEvent(pBInfo->pRes, pNotifySup->pSessionKeys, pTaskInfo->streamInfo.notifyResultSchema, + pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } (*ppRes) = pBInfo->pRes; return code; } + + code = buildNotifyEventBlock(pTaskInfo, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + if (pNotifySup->pEventBlock && pNotifySup->pEventBlock->info.rows > 0) { + printDataBlock(pNotifySup->pEventBlock, getStreamOpName(pOperator->operatorType), GET_TASKID(pTaskInfo)); + (*ppRes) = pNotifySup->pEventBlock; + return code; + } + + code = removeOutdatedNotifyEvents(&pInfo->twAggSup, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + +_end: + if (code != TSDB_CODE_SUCCESS) { + qError("%s failed at line %d since %s. task:%s", __func__, lino, tstrerror(code), GET_TASKID(pTaskInfo)); + } (*ppRes) = NULL; return code; } @@ -3295,7 +3412,10 @@ int32_t doStreamSessionEncodeOpState(void** buf, int32_t len, SOperatorInfo* pOp // 4.dataVersion tlen += taosEncodeFixedI64(buf, pInfo->dataVersion); - // 5.checksum + // 5.basicInfo + tlen += encodeStreamBasicInfo(buf, &pInfo->basic); + + // 6.checksum if (isParent) { if (buf) { uint32_t cksum = taosCalcChecksum(0, pData, len - sizeof(uint32_t)); @@ -3313,13 +3433,14 @@ int32_t doStreamSessionDecodeOpState(void* buf, int32_t len, SOperatorInfo* pOpe int32_t lino = 0; SStreamSessionAggOperatorInfo* pInfo = pOperator->info; SExecTaskInfo* pTaskInfo = pOperator->pTaskInfo; + void* pDataEnd = POINTER_SHIFT(buf, len); if (!pInfo) { code = TSDB_CODE_FAILED; QUERY_CHECK_CODE(code, lino, _end); } SStreamAggSupporter* pAggSup = &pInfo->streamAggSup; - // 5.checksum + // 6.checksum if (isParent) { int32_t dataLen = len - sizeof(uint32_t); void* pCksum = POINTER_SHIFT(buf, dataLen); @@ -3328,6 +3449,7 @@ int32_t doStreamSessionDecodeOpState(void* buf, int32_t len, SOperatorInfo* pOpe code = TSDB_CODE_FAILED; QUERY_CHECK_CODE(code, lino, _end); } + pDataEnd = pCksum; } // 1.streamAggSup.pResultRows @@ -3366,6 +3488,12 @@ int32_t doStreamSessionDecodeOpState(void* buf, int32_t len, SOperatorInfo* pOpe (*ppBuf) = buf; } + // 5.basicInfo + if (buf < pDataEnd) { + code = decodeStreamBasicInfo(&buf, &pInfo->basic); + QUERY_CHECK_CODE(code, lino, _end); + } + _end: if (code != TSDB_CODE_SUCCESS) { qError("%s failed at line %d since %s. task:%s", __func__, lino, tstrerror(code), GET_TASKID(pTaskInfo)); @@ -3926,6 +4054,9 @@ int32_t createStreamSessionAggOperatorInfo(SOperatorInfo* downstream, SPhysiNode optrDefaultBufFn, NULL, optrDefaultGetNextExtFn, NULL); setOperatorStreamStateFn(pOperator, streamSessionReleaseState, streamSessionReloadState); + code = initStreamBasicInfo(&pInfo->basic, pOperator); + QUERY_CHECK_CODE(code, lino, _error); + if (downstream) { pInfo->basic.primaryPkIndex = -1; code = initDownStream(downstream, &pInfo->streamAggSup, pOperator->operatorType, pInfo->primaryTsIndex, @@ -4162,6 +4293,10 @@ int32_t createStreamFinalSessionAggOperatorInfo(SOperatorInfo* downstream, SPhys optrDefaultBufFn, NULL, optrDefaultGetNextExtFn, NULL); setOperatorStreamStateFn(pOperator, streamSessionReleaseState, streamSessionSemiReloadState); } + + code = initStreamBasicInfo(&pInfo->basic, pOperator); + QUERY_CHECK_CODE(code, lino, _error); + setOperatorInfo(pOperator, getStreamOpName(pOperator->operatorType), pPhyNode->type, false, OP_NOT_OPENED, pInfo, pTaskInfo); @@ -4230,6 +4365,8 @@ void destroyStreamStateOperatorInfo(void* param) { &pInfo->groupResInfo); pInfo->pOperator = NULL; } + + destroyStreamBasicInfo(&pInfo->basic); destroyStreamAggSupporter(&pInfo->streamAggSup); clearGroupResInfo(&pInfo->groupResInfo); taosArrayDestroyP(pInfo->pUpdated, destroyFlusedPos); @@ -4284,12 +4421,39 @@ bool compareWinStateKey(SStateKeys* left, SStateKeys* right) { return compareVal(left->pData, right); } +static void getNextStateWin(const SStreamAggSupporter* pAggSup, SStateWindowInfo* pNextWin, bool asc) { + SStreamStateCur* pCur = NULL; + + if (pAggSup == NULL || pNextWin == NULL) { + return; + } + + if (asc) + pCur = pAggSup->stateStore.streamStateSessionSeekKeyNext(pAggSup->pState, &pNextWin->winInfo.sessionWin); + else + pCur = pAggSup->stateStore.streamStateSessionSeekKeyPrev(pAggSup->pState, &pNextWin->winInfo.sessionWin); + int32_t nextSize = pAggSup->resultRowSize; + int32_t winCode = pAggSup->stateStore.streamStateSessionGetKVByCur(pCur, &pNextWin->winInfo.sessionWin, + (void**)&pNextWin->winInfo.pStatePos, &nextSize); + if (winCode != TSDB_CODE_SUCCESS) { + SET_SESSION_WIN_INVALID(pNextWin->winInfo); + } else { + pNextWin->pStateKey = + (SStateKeys*)((char*)pNextWin->winInfo.pStatePos->pRowBuff + (pAggSup->resultRowSize - pAggSup->stateKeySize)); + pNextWin->pStateKey->bytes = pAggSup->stateKeySize - sizeof(SStateKeys); + pNextWin->pStateKey->type = pAggSup->stateKeyType; + pNextWin->pStateKey->pData = (char*)pNextWin->pStateKey + sizeof(SStateKeys); + pNextWin->pStateKey->isNull = false; + pNextWin->winInfo.isOutput = true; + } + pAggSup->stateStore.streamStateFreeCur(pCur); +} + int32_t getStateWindowInfoByKey(SStreamAggSupporter* pAggSup, SSessionKey* pKey, SStateWindowInfo* pCurWin, SStateWindowInfo* pNextWin) { - int32_t code = TSDB_CODE_SUCCESS; - int32_t lino = 0; - SStreamStateCur* pCur = NULL; - int32_t size = pAggSup->resultRowSize; + int32_t code = TSDB_CODE_SUCCESS; + int32_t lino = 0; + int32_t size = pAggSup->resultRowSize; pCurWin->winInfo.sessionWin.groupId = pKey->groupId; pCurWin->winInfo.sessionWin.win.skey = pKey->win.skey; pCurWin->winInfo.sessionWin.win.ekey = pKey->win.ekey; @@ -4313,24 +4477,9 @@ int32_t getStateWindowInfoByKey(SStreamAggSupporter* pAggSup, SSessionKey* pKey, pCurWin->winInfo.sessionWin.win.ekey); pNextWin->winInfo.sessionWin = pCurWin->winInfo.sessionWin; - pCur = pAggSup->stateStore.streamStateSessionSeekKeyNext(pAggSup->pState, &pNextWin->winInfo.sessionWin); - int32_t nextSize = pAggSup->resultRowSize; - int32_t winCode = pAggSup->stateStore.streamStateSessionGetKVByCur(pCur, &pNextWin->winInfo.sessionWin, - (void**)&pNextWin->winInfo.pStatePos, &nextSize); - if (winCode != TSDB_CODE_SUCCESS) { - SET_SESSION_WIN_INVALID(pNextWin->winInfo); - } else { - pNextWin->pStateKey = - (SStateKeys*)((char*)pNextWin->winInfo.pStatePos->pRowBuff + (pAggSup->resultRowSize - pAggSup->stateKeySize)); - pNextWin->pStateKey->bytes = pAggSup->stateKeySize - sizeof(SStateKeys); - pNextWin->pStateKey->type = pAggSup->stateKeyType; - pNextWin->pStateKey->pData = (char*)pNextWin->pStateKey + sizeof(SStateKeys); - pNextWin->pStateKey->isNull = false; - pNextWin->winInfo.isOutput = true; - } + getNextStateWin(pAggSup, pNextWin, true); _end: - pAggSup->stateStore.streamStateFreeCur(pCur); qDebug("===stream===get state next win buff. skey:%" PRId64 ", endkey:%" PRId64, pNextWin->winInfo.sessionWin.win.skey, pNextWin->winInfo.sessionWin.win.ekey); if (code != TSDB_CODE_SUCCESS) { @@ -4340,9 +4489,8 @@ _end: } int32_t setStateOutputBuf(SStreamAggSupporter* pAggSup, TSKEY ts, uint64_t groupId, char* pKeyData, - SStateWindowInfo* pCurWin, SStateWindowInfo* pNextWin) { - int32_t size = pAggSup->resultRowSize; - SStreamStateCur* pCur = NULL; + SStateWindowInfo* pCurWin, SStateWindowInfo* pNextWin, int32_t* pWinCode) { + int32_t size = pAggSup->resultRowSize; pCurWin->winInfo.sessionWin.groupId = groupId; pCurWin->winInfo.sessionWin.win.skey = ts; pCurWin->winInfo.sessionWin.win.ekey = ts; @@ -4390,29 +4538,16 @@ int32_t setStateOutputBuf(SStreamAggSupporter* pAggSup, TSKEY ts, uint64_t group } } + *pWinCode = winCode; + qDebug("===stream===set state cur win buff. skey:%" PRId64 ", endkey:%" PRId64, pCurWin->winInfo.sessionWin.win.skey, pCurWin->winInfo.sessionWin.win.ekey); pNextWin->winInfo.sessionWin = pCurWin->winInfo.sessionWin; - pCur = pAggSup->stateStore.streamStateSessionSeekKeyNext(pAggSup->pState, &pNextWin->winInfo.sessionWin); - int32_t nextSize = pAggSup->resultRowSize; - winCode = pAggSup->stateStore.streamStateSessionGetKVByCur(pCur, &pNextWin->winInfo.sessionWin, - (void**)&pNextWin->winInfo.pStatePos, &nextSize); - if (winCode != TSDB_CODE_SUCCESS) { - SET_SESSION_WIN_INVALID(pNextWin->winInfo); - } else { - pNextWin->pStateKey = - (SStateKeys*)((char*)pNextWin->winInfo.pStatePos->pRowBuff + (pAggSup->resultRowSize - pAggSup->stateKeySize)); - pNextWin->pStateKey->bytes = pAggSup->stateKeySize - sizeof(SStateKeys); - pNextWin->pStateKey->type = pAggSup->stateKeyType; - pNextWin->pStateKey->pData = (char*)pNextWin->pStateKey + sizeof(SStateKeys); - pNextWin->pStateKey->isNull = false; - pNextWin->winInfo.isOutput = true; - } + getNextStateWin(pAggSup, pNextWin, true); qDebug("===stream===set state next win buff. skey:%" PRId64 ", endkey:%" PRId64, pNextWin->winInfo.sessionWin.win.skey, pNextWin->winInfo.sessionWin.win.ekey); _end: - pAggSup->stateStore.streamStateFreeCur(pCur); if (code != TSDB_CODE_SUCCESS) { qError("%s failed at line %d since %s", __func__, lino, tstrerror(code)); } @@ -4472,7 +4607,7 @@ static bool isWinResult(SSessionKey* pKey, SSHashObj* pSeUpdate, SSHashObj* pRes if (tSimpleHashGet(pSeUpdate, &checkKey, sizeof(SSessionKey)) != NULL) { return true; } - + if (tSimpleHashGet(pResults, &checkKey, sizeof(SSessionKey)) != NULL) { return true; } @@ -4493,6 +4628,8 @@ static void doStreamStateAggImpl(SOperatorInfo* pOperator, SSDataBlock* pSDataBl SResultRow* pResult = NULL; int32_t winRows = 0; SStreamAggSupporter* pAggSup = &pInfo->streamAggSup; + SStreamNotifyEventSupp* pNotifySup = &pInfo->basic.notifyEventSup; + STaskNotifyEventStat* pNotifyEventStat = pTaskInfo->streamInfo.pNotifyEventStat; pInfo->dataVersion = TMAX(pInfo->dataVersion, pSDataBlock->info.version); pAggSup->winRange = pTaskInfo->streamInfo.fillHistoryWindow; @@ -4528,9 +4665,31 @@ static void doStreamStateAggImpl(SOperatorInfo* pOperator, SSDataBlock* pSDataBl bool allEqual = true; SStateWindowInfo curWin = {0}; SStateWindowInfo nextWin = {0}; - code = setStateOutputBuf(pAggSup, tsCols[i], groupId, pKeyData, &curWin, &nextWin); + int32_t winCode = TSDB_CODE_SUCCESS; + code = setStateOutputBuf(pAggSup, tsCols[i], groupId, pKeyData, &curWin, &nextWin, &winCode); QUERY_CHECK_CODE(code, lino, _end); + if (winCode != TSDB_CODE_SUCCESS && pTaskInfo->streamInfo.eventTypes) { + SStateWindowInfo prevWin = {.winInfo.sessionWin = curWin.winInfo.sessionWin}; + getNextStateWin(pAggSup, &prevWin, false); + qDebug("===stream===get state prev win buff. skey:%" PRId64 ", endkey:%" PRId64, + prevWin.winInfo.sessionWin.win.skey, prevWin.winInfo.sessionWin.win.ekey); + releaseOutputBuf(pAggSup->pState, prevWin.winInfo.pStatePos, &pAPI->stateStore); + // For ordered data, the previous window's closure did not record the corresponding state values, so they need to + // be added here. + if (BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_CLOSE) && + IS_VALID_SESSION_WIN(prevWin.winInfo)) { + code = addStateAggNotifyEvent(SNOTIFY_EVENT_WINDOW_CLOSE, &prevWin.winInfo.sessionWin, prevWin.pStateKey, + curWin.pStateKey, true, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } + if (BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_OPEN)) { + code = addStateAggNotifyEvent(SNOTIFY_EVENT_WINDOW_OPEN, &curWin.winInfo.sessionWin, curWin.pStateKey, + prevWin.pStateKey, false, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } + } + if (isWinResult(&nextWin.winInfo.sessionWin, pSeUpdated, pAggSup->pResultRows) == false) { releaseOutputBuf(pAggSup->pState, nextWin.winInfo.pStatePos, &pAPI->stateStore); } @@ -4578,6 +4737,14 @@ static void doStreamStateAggImpl(SOperatorInfo* pOperator, SSDataBlock* pSDataBl tSimpleHashPut(pAggSup->pResultRows, &key, sizeof(SSessionKey), &curWin.winInfo, sizeof(SResultWindowInfo)); QUERY_CHECK_CODE(code, lino, _end); } + + // If this is a windown recalculation, add the corresponding state values here since the next window may not require + // recalculation. + if (BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_CLOSE)) { + code = addStateAggNotifyEvent(SNOTIFY_EVENT_WINDOW_CLOSE, &curWin.winInfo.sessionWin, curWin.pStateKey, + nextWin.pStateKey, false, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } } _end: @@ -4621,7 +4788,10 @@ int32_t doStreamStateEncodeOpState(void** buf, int32_t len, SOperatorInfo* pOper // 4.dataVersion tlen += taosEncodeFixedI64(buf, pInfo->dataVersion); - // 5.checksum + // 5.basicInfo + tlen += encodeStreamBasicInfo(buf, &pInfo->basic); + + // 6.checksum if (isParent) { if (buf) { uint32_t cksum = taosCalcChecksum(0, pData, len - sizeof(uint32_t)); @@ -4640,12 +4810,13 @@ int32_t doStreamStateDecodeOpState(void* buf, int32_t len, SOperatorInfo* pOpera int32_t lino = 0; SStreamAggSupporter* pAggSup = &pInfo->streamAggSup; SExecTaskInfo* pTaskInfo = pOperator->pTaskInfo; + void* pDataEnd = POINTER_SHIFT(buf, len); if (!pInfo) { code = TSDB_CODE_FAILED; QUERY_CHECK_CODE(code, lino, _end); } - // 5.checksum + // 6.checksum if (isParent) { int32_t dataLen = len - sizeof(uint32_t); void* pCksum = POINTER_SHIFT(buf, dataLen); @@ -4654,6 +4825,7 @@ int32_t doStreamStateDecodeOpState(void* buf, int32_t len, SOperatorInfo* pOpera code = TSDB_CODE_FAILED; QUERY_CHECK_CODE(code, lino, _end); } + pDataEnd = pCksum; } // 1.streamAggSup.pResultRows @@ -4693,6 +4865,12 @@ int32_t doStreamStateDecodeOpState(void* buf, int32_t len, SOperatorInfo* pOpera (*ppBuf) = buf; } + // 5.basicInfo + if (buf < pDataEnd) { + code = decodeStreamBasicInfo(&buf, &pInfo->basic); + QUERY_CHECK_CODE(code, lino, _end); + } + _end: if (code != TSDB_CODE_SUCCESS) { qError("%s failed at line %d since %s. task:%s", __func__, lino, tstrerror(code), GET_TASKID(pTaskInfo)); @@ -4720,23 +4898,53 @@ void doStreamStateSaveCheckpoint(SOperatorInfo* pOperator) { static int32_t buildStateResult(SOperatorInfo* pOperator, SSDataBlock** ppRes) { int32_t code = TSDB_CODE_SUCCESS; + int32_t lino = 0; SStreamStateAggOperatorInfo* pInfo = pOperator->info; SOptrBasicInfo* pBInfo = &pInfo->binfo; SExecTaskInfo* pTaskInfo = pOperator->pTaskInfo; - + SStreamNotifyEventSupp* pNotifySup = &pInfo->basic.notifyEventSup; + STaskNotifyEventStat* pNotifyEventStat = pTaskInfo->streamInfo.pNotifyEventStat; + bool addNotifyEvent = false; + addNotifyEvent = BIT_FLAG_TEST_MASK(pTaskInfo->streamInfo.eventTypes, SNOTIFY_EVENT_WINDOW_CLOSE); doBuildDeleteDataBlock(pOperator, pInfo->pSeDeleted, pInfo->pDelRes, &pInfo->pDelIterator); if (pInfo->pDelRes->info.rows > 0) { printDataBlock(pInfo->pDelRes, getStreamOpName(pOperator->operatorType), GET_TASKID(pTaskInfo)); + if (addNotifyEvent) { + code = addAggDeleteNotifyEvent(pInfo->pDelRes, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } (*ppRes) = pInfo->pDelRes; return code; } - doBuildSessionResult(pOperator, pInfo->streamAggSup.pState, &pInfo->groupResInfo, pBInfo->pRes); + doBuildSessionResult(pOperator, pInfo->streamAggSup.pState, &pInfo->groupResInfo, pBInfo->pRes, + addNotifyEvent ? pNotifySup->pSessionKeys : NULL); if (pBInfo->pRes->info.rows > 0) { printDataBlock(pBInfo->pRes, getStreamOpName(pOperator->operatorType), GET_TASKID(pTaskInfo)); + if (addNotifyEvent) { + code = addAggResultNotifyEvent(pBInfo->pRes, pNotifySup->pSessionKeys, pTaskInfo->streamInfo.notifyResultSchema, + pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + } (*ppRes) = pBInfo->pRes; return code; } + + code = buildNotifyEventBlock(pTaskInfo, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + if (pNotifySup->pEventBlock && pNotifySup->pEventBlock->info.rows > 0) { + printDataBlock(pNotifySup->pEventBlock, getStreamOpName(pOperator->operatorType), GET_TASKID(pTaskInfo)); + (*ppRes) = pNotifySup->pEventBlock; + return code; + } + + code = removeOutdatedNotifyEvents(&pInfo->twAggSup, pNotifySup, pNotifyEventStat); + QUERY_CHECK_CODE(code, lino, _end); + +_end: + if (code != TSDB_CODE_SUCCESS) { + qError("%s failed at line %d since %s. task:%s", __func__, lino, tstrerror(code), GET_TASKID(pTaskInfo)); + } (*ppRes) = NULL; return code; } @@ -5122,6 +5330,10 @@ int32_t createStreamStateAggOperatorInfo(SOperatorInfo* downstream, SPhysiNode* pOperator->fpSet = createOperatorFpSet(optrDummyOpenFn, doStreamStateAggNext, NULL, destroyStreamStateOperatorInfo, optrDefaultBufFn, NULL, optrDefaultGetNextExtFn, NULL); setOperatorStreamStateFn(pOperator, streamStateReleaseState, streamStateReloadState); + + code = initStreamBasicInfo(&pInfo->basic, pOperator); + QUERY_CHECK_CODE(code, lino, _error); + code = initDownStream(downstream, &pInfo->streamAggSup, pOperator->operatorType, pInfo->primaryTsIndex, &pInfo->twAggSup, &pInfo->basic); QUERY_CHECK_CODE(code, lino, _error); @@ -5331,8 +5543,9 @@ _end: return code; } -static int32_t createStreamSingleIntervalOperatorInfo(SOperatorInfo* downstream, SPhysiNode* pPhyNode, SExecTaskInfo* pTaskInfo, - SReadHandle* pHandle, SOperatorInfo** pOptrInfo) { +static int32_t createStreamSingleIntervalOperatorInfo(SOperatorInfo* downstream, SPhysiNode* pPhyNode, + SExecTaskInfo* pTaskInfo, SReadHandle* pHandle, + SOperatorInfo** pOptrInfo) { QRY_PARAM_CHECK(pOptrInfo); int32_t code = TSDB_CODE_SUCCESS; @@ -5455,6 +5668,9 @@ static int32_t createStreamSingleIntervalOperatorInfo(SOperatorInfo* downstream, optrDefaultBufFn, NULL, optrDefaultGetNextExtFn, NULL); setOperatorStreamStateFn(pOperator, streamIntervalReleaseState, streamIntervalReloadState); + code = initStreamBasicInfo(&pInfo->basic, pOperator); + QUERY_CHECK_CODE(code, lino, _error); + pInfo->recvGetAll = false; code = createSpecialDataBlock(STREAM_CHECKPOINT, &pInfo->pCheckpointRes); diff --git a/source/libs/parser/src/parTranslater.c b/source/libs/parser/src/parTranslater.c index b657272b91..142529830a 100755 --- a/source/libs/parser/src/parTranslater.c +++ b/source/libs/parser/src/parTranslater.c @@ -11420,7 +11420,7 @@ static int32_t checkStreamQuery(STranslateContext* pCxt, SCreateStreamStmt* pStm !hasTbnameFunction(pSelect->pPartitionByList) && pSelect->pWindow != NULL && pSelect->pWindow->type == QUERY_NODE_EVENT_WINDOW) { return generateSyntaxErrMsgExt(&pCxt->msgBuf, TSDB_CODE_PAR_INVALID_STREAM_QUERY, - "Event window for stream on super table must patitioned by table name"); + "Event window for stream on super table must partitioned by table name"); } if (pSelect->pWindow != NULL && pSelect->pWindow->type == QUERY_NODE_EVENT_WINDOW && diff --git a/source/libs/stream/src/streamSessionState.c b/source/libs/stream/src/streamSessionState.c index d2d7c7b11b..9aabb30baa 100644 --- a/source/libs/stream/src/streamSessionState.c +++ b/source/libs/stream/src/streamSessionState.c @@ -562,6 +562,33 @@ SStreamStateCur* sessionWinStateSeekKeyCurrentPrev(SStreamFileState* pFileState, pCur->pStreamFileState = pFileState; return pCur; } + +SStreamStateCur* sessionWinStateSeekKeyPrev(SStreamFileState *pFileState, const SSessionKey *pWinKey) { + SArray* pWinStates = NULL; + int32_t index = -1; + SStreamStateCur *pCur = seekKeyCurrentPrev_buff(pFileState, pWinKey, &pWinStates, &index); + if (pCur) { + int32_t cmpRes= sessionStateRangeKeyCompare(pWinKey, pWinStates, index); + if (cmpRes > 0) { + return pCur; + } else if (cmpRes == 0 && index > 0) { + sessionWinStateMoveToPrev(pCur); + return pCur; + } + streamStateFreeCur(pCur); + pCur = NULL; + } + + void* pFileStore = getStateFileStore(pFileState); + pCur = streamStateSessionSeekKeyPrev_rocksdb(pFileStore, pWinKey); + if (!pCur) { + return NULL; + } + pCur->buffIndex = -1; + pCur->pStreamFileState = pFileState; + return pCur; +} + static void transformCursor(SStreamFileState* pFileState, SStreamStateCur* pCur) { if (!pCur) { return; @@ -747,6 +774,15 @@ void sessionWinStateMoveToNext(SStreamStateCur* pCur) { } } +void sessionWinStateMoveToPrev(SStreamStateCur* pCur) { + qTrace("move cursor to prev"); + if (pCur && pCur->buffIndex >= 1) { + pCur->buffIndex--; + } else { + streamStateCurPrev_rocksdb(pCur); + } +} + int32_t sessionWinStateGetKeyByRange(SStreamFileState* pFileState, const SSessionKey* key, SSessionKey* curKey, range_cmpr_fn cmpFn) { SStreamStateCur* pCur = sessionWinStateSeekKeyCurrentPrev(pFileState, key); diff --git a/source/libs/stream/src/streamState.c b/source/libs/stream/src/streamState.c index 7259c0e49a..dba02015ed 100644 --- a/source/libs/stream/src/streamState.c +++ b/source/libs/stream/src/streamState.c @@ -440,6 +440,10 @@ SStreamStateCur* streamStateSessionSeekKeyCurrentNext(SStreamState* pState, cons return sessionWinStateSeekKeyCurrentNext(pState->pFileState, key); } +SStreamStateCur *streamStateSessionSeekKeyPrev(SStreamState *pState, const SSessionKey *key) { + return sessionWinStateSeekKeyPrev(pState->pFileState, key); +} + SStreamStateCur* streamStateSessionSeekKeyNext(SStreamState* pState, const SSessionKey* key) { return sessionWinStateSeekKeyNext(pState->pFileState, key); } diff --git a/source/libs/stream/src/streamTask.c b/source/libs/stream/src/streamTask.c index 5ee8bd43f5..7209b6434f 100644 --- a/source/libs/stream/src/streamTask.c +++ b/source/libs/stream/src/streamTask.c @@ -331,6 +331,8 @@ void tFreeStreamTask(void* pParam) { taosMemoryFreeClear(pTask->notifyInfo.stbFullName); tDeleteSchemaWrapper(pTask->notifyInfo.pSchemaWrapper); + pTask->notifyEventStat = (STaskNotifyEventStat){0}; + taosMemoryFree(pTask); stDebug("s-task:0x%x free task completed", taskId); } @@ -988,6 +990,7 @@ void streamTaskStatusCopy(STaskStatusEntry* pDst, const STaskStatusEntry* pSrc) pDst->startTime = pSrc->startTime; pDst->hTaskId = pSrc->hTaskId; + pDst->notifyEventStat = pSrc->notifyEventStat; } STaskStatusEntry streamTaskGetStatusEntry(SStreamTask* pTask) { @@ -1016,6 +1019,7 @@ STaskStatusEntry streamTaskGetStatusEntry(SStreamTask* pTask) { .outputThroughput = SIZE_IN_KiB(pExecInfo->outputThroughput), .startCheckpointId = pExecInfo->startCheckpointId, .startCheckpointVer = pExecInfo->startCheckpointVer, + .notifyEventStat = pTask->notifyEventStat, }; return entry; } From 8ad3a5802d209648bd2d0c9f1bfc57641cfc520b Mon Sep 17 00:00:00 2001 From: Jinqing Kuang Date: Wed, 12 Feb 2025 23:33:31 +0800 Subject: [PATCH 02/17] feat(stream)[TS-5469]: add tests for stream event notifications Add functional tests for different stream processing scenarios and window types. Also include tests for network failure cases to ensure robustness. --- tests/army/stream/stream_notify.json | 71 +++ tests/army/stream/stream_notify_disorder.json | 72 +++ tests/army/stream/stream_notify_server.py | 55 ++ tests/army/stream/test_stream_notify.py | 473 ++++++++++++++++++ tests/parallel_test/longtimeruning_cases.task | 1 + .../0-others/information_schema.py | 2 +- tools/taos-tools/src/benchUtil.c | 8 + 7 files changed, 681 insertions(+), 1 deletion(-) create mode 100644 tests/army/stream/stream_notify.json create mode 100644 tests/army/stream/stream_notify_disorder.json create mode 100644 tests/army/stream/stream_notify_server.py create mode 100644 tests/army/stream/test_stream_notify.py diff --git a/tests/army/stream/stream_notify.json b/tests/army/stream/stream_notify.json new file mode 100644 index 0000000000..9dcbe4efb2 --- /dev/null +++ b/tests/army/stream/stream_notify.json @@ -0,0 +1,71 @@ +{ + "filetype": "insert", + "cfgdir": "/etc/taos", + "host": "127.0.0.1", + "port": 6030, + "user": "root", + "password": "taosdata", + "thread_count": 4, + "create_table_thread_count": 4, + "result_file": "./insert_res.txt", + "confirm_parameter_prompt": "no", + "num_of_records_per_req": 10000, + "prepared_rand": 10000, + "chinese": "no", + "escape_character": "yes", + "continue_if_fail": "no", + "databases": [ + { + "dbinfo": { + "name": "test", + "drop": "no", + "vgroups": 4, + "precision": "ms" + }, + "super_tables": [ + { + "name": "st", + "child_table_exists": "no", + "childtable_count": 5, + "childtable_prefix": "ct", + "auto_create_table": "yes", + "batch_create_tbl_num": 5, + "data_source": "rand", + "insert_mode": "taosc", + "non_stop_mode": "no", + "line_protocol": "line", + "insert_rows": 10000, + "childtable_limit": 0, + "childtable_offset": 0, + "interlace_rows": 50, + "insert_interval": 10, + "partial_col_num": 0, + "timestamp_step": 500, + "start_timestamp": "2025-01-13 17:30:00.000", + "sample_format": "csv", + "sample_file": "./sample.csv", + "use_sample_ts": "no", + "tags_file": "", + "columns": [ + {"type": "TINYINT", "name": "c0"}, + {"type": "SMALLINT", "name": "c1"}, + {"type": "INT", "name": "c2"}, + {"type": "BIGINT", "name": "c3"}, + {"type": "DOUBLE", "name": "c4"}, + {"type": "FLOAT", "name": "c5"}, + {"type": "BOOL", "name": "c6"}, + {"type": "VARCHAR", "name": "c7", "len": 10}, + {"type": "NCHAR", "name": "c8", "len": 10}, + {"type": "UTINYINT", "name": "c9"}, + {"type": "USMALLINT", "name": "c10"}, + {"type": "UINT", "name": "c11"}, + {"type": "UBIGINT", "name": "c12"} + ], + "tags": [ + {"type": "INT", "name": "groupid", "max": 100, "min": 1} + ] + } + ] + } + ] +} diff --git a/tests/army/stream/stream_notify_disorder.json b/tests/army/stream/stream_notify_disorder.json new file mode 100644 index 0000000000..2f9f9bea69 --- /dev/null +++ b/tests/army/stream/stream_notify_disorder.json @@ -0,0 +1,72 @@ +{ + "filetype": "insert", + "cfgdir": "/etc/taos", + "host": "127.0.0.1", + "port": 6030, + "user": "root", + "password": "taosdata", + "thread_count": 4, + "create_table_thread_count": 4, + "result_file": "./insert_res.txt", + "confirm_parameter_prompt": "no", + "num_of_records_per_req": 10000, + "prepared_rand": 10000, + "chinese": "no", + "escape_character": "yes", + "continue_if_fail": "no", + "databases": [ + { + "dbinfo": { + "name": "test", + "drop": "no", + "vgroups": 4, + "precision": "ms" + }, + "super_tables": [ + { + "name": "st", + "child_table_exists": "no", + "childtable_count": 5, + "childtable_prefix": "ct", + "auto_create_table": "yes", + "batch_create_tbl_num": 5, + "data_source": "rand", + "insert_mode": "taosc", + "non_stop_mode": "no", + "line_protocol": "line", + "insert_rows": 10000, + "disorder_ratio": 10, + "childtable_limit": 0, + "childtable_offset": 0, + "interlace_rows": 50, + "insert_interval": 10, + "partial_col_num": 0, + "timestamp_step": 500, + "start_timestamp": "2025-01-13 17:30:00.000", + "sample_format": "csv", + "sample_file": "./sample.csv", + "use_sample_ts": "no", + "tags_file": "", + "columns": [ + {"type": "TINYINT", "name": "c0"}, + {"type": "SMALLINT", "name": "c1"}, + {"type": "INT", "name": "c2"}, + {"type": "BIGINT", "name": "c3"}, + {"type": "DOUBLE", "name": "c4"}, + {"type": "FLOAT", "name": "c5"}, + {"type": "BOOL", "name": "c6"}, + {"type": "VARCHAR", "name": "c7", "len": 10}, + {"type": "NCHAR", "name": "c8", "len": 10}, + {"type": "UTINYINT", "name": "c9"}, + {"type": "USMALLINT", "name": "c10"}, + {"type": "UINT", "name": "c11"}, + {"type": "UBIGINT", "name": "c12"} + ], + "tags": [ + {"type": "INT", "name": "groupid", "max": 100, "min": 1} + ] + } + ] + } + ] +} diff --git a/tests/army/stream/stream_notify_server.py b/tests/army/stream/stream_notify_server.py new file mode 100644 index 0000000000..a105d55971 --- /dev/null +++ b/tests/army/stream/stream_notify_server.py @@ -0,0 +1,55 @@ +################################################################### +# Copyright (c) 2016 by TAOS Technologies, Inc. +# All rights reserved. +# +# This file is proprietary and confidential to TAOS Technologies. +# No part of this file may be reproduced, stored, transmitted, +# disclosed or used in any form or by any means other than as +# expressly provided by the written permission from Jianhui Tao +# +################################################################### + +# -*- coding: utf-8 -*- + +import asyncio +import signal +import websockets +import argparse + +stop_event = asyncio.Event() + +async def handle_websocket(websocket, log_file): + try: + # Write the message to the specified log file + if log_file != "": + with open(log_file, "a", encoding="utf-8") as f: + async for message in websocket: + f.write(message + "\n") + if stop_event.is_set(): + break + except Exception as e: + print(f"Connection closed with error: {e}") + +async def listen(port, log_file): + async with websockets.serve( + lambda ws: handle_websocket(ws, log_file), + "0.0.0.0", + port, + ping_timeout = None, + max_size= 10 * 1024 * 1024 # 10MB, + ): + print(f"WebSocket server listening on port {port}...") + await stop_event.wait() # Run forever (until canceled) + +def signal_handler(sig, frame): + stop_event.set() + +if __name__ == '__main__': + signal.signal(signal.SIGINT, signal_handler) + + parser = argparse.ArgumentParser() + parser.add_argument('-d', '--log_file', type=str, default='stream_notify_server.log', help='log file') + parser.add_argument('-p', '--port', type=int, default=12345, help='port number') + args = parser.parse_args() + + asyncio.run(listen(args.port, args.log_file)) diff --git a/tests/army/stream/test_stream_notify.py b/tests/army/stream/test_stream_notify.py new file mode 100644 index 0000000000..0e6bbd5c01 --- /dev/null +++ b/tests/army/stream/test_stream_notify.py @@ -0,0 +1,473 @@ +################################################################### +# Copyright (c) 2016 by TAOS Technologies, Inc. +# All rights reserved. +# +# This file is proprietary and confidential to TAOS Technologies. +# No part of this file may be reproduced, stored, transmitted, +# disclosed or used in any form or by any means other than as +# expressly provided by the written permission from Jianhui Tao +# +################################################################### + +# -*- coding: utf-8 -*- + +from frame import etool +from frame.etool import * +from frame.log import * +from frame.cases import * +from frame.sql import * +from frame.caseBase import * +from frame.common import * +import signal +import subprocess + +class StreamNotifyServer: + def __init__(self): + self.log_file = "" + self.sub_process = None + + def __del__(self): + self.stop() + + def run(self, port, log_file): + tdLog.info(f"Start notify server: python3 {etool.curFile(__file__, 'stream_notify_server.py')} -p {port} -d {log_file}") + self.sub_process = subprocess.Popen(['python3', etool.curFile(__file__, 'stream_notify_server.py'), '-p', str(port), '-d', log_file]) + self.log_file = log_file + + def stop(self): + if self.sub_process is not None: + self.sub_process.send_signal(signal.SIGINT) + try: + self.sub_process.wait(60) + except subprocess.TimeoutExpired: + self.sub_process.kill() + +class TestStreamNotifySinglePass(): + def __init__(self, num_addr_per_stream, trigger_mode, notify_event, disorder): + self.current_dir = os.path.dirname(os.path.abspath(__file__)) + self.num_addr_per_stream = num_addr_per_stream + self.trigger_mode = trigger_mode + self.notify_event = notify_event + self.disorder = disorder + self.streams = [] + self.id = ''.join(random.choices(string.ascii_letters + string.digits, k=8)) + + def is_port_in_use(self, port): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + return s.connect_ex(("127.0.0.1", port)) == 0 + + def gen_streams(self): + self.streams = [ + { + "stream_name": "s_time_par", + "dest_table": "dst_time_par", + "window_clause": "interval(5s)", + "partitioned": True, + }, + { + "stream_name": "s_time", + "dest_table": "dst_time", + "window_clause": "interval(5s)", + "partitioned": False, + }, + { + "stream_name": "s_state_par", + "dest_table": "dst_state_par", + "window_clause": "state_window(c6)", + "partitioned": True, + }, + { + "stream_name": "s_session_par", + "dest_table": "dst_session_par", + "window_clause": "session(ts, 50a)", + "partitioned": True, + }, + { + "stream_name": "s_session", + "dest_table": "dst_session", + "window_clause": "session(ts, 50a)", + "partitioned": False, + }, + { + "stream_name": "s_event_par", + "dest_table": "dst_event_par", + "window_clause": "event_window start with c6 = true end with c6 = false", + "partitioned": True, + }, + { + "stream_name": "s_count_par", + "dest_table": "dst_count_par", + "window_clause": "count_window(10)", + "partitioned": True, + }, + ] + # set port to random number between 10000 and 20000 + port = random.randint(10000, 20000) + for stream in self.streams: + stream["notify_address"] = "" + stream["notify_server"] = [] + if self.trigger_mode == "FORCE_WINDOW_CLOSE": + if stream["stream_name"] == "s_time" or stream["stream_name"] == "s_session": + continue + elif "MAX_DELAY" in self.trigger_mode or "AT_ONCE" in self.trigger_mode or self.disorder: + if stream["stream_name"] == "s_session_par" or stream["stream_name"] == "s_state_par": + continue + for i in range(self.num_addr_per_stream): + # Find an available port + while self.is_port_in_use(port): + port += 1 + # Start the stream notify server and add the address to the stream + log_file = f"{self.current_dir}/{self.id}_{stream['stream_name']}_{i}.log" + if os.path.exists(log_file): + os.remove(log_file) + server = StreamNotifyServer() + server.run(port, log_file) + stream["notify_address"] += f"'ws://127.0.0.1:{port}'," + stream["notify_server"].append(server) + port += 1 + stream["notify_address"] = stream["notify_address"][:-1] + + def create_streams(self): + tdLog.info("==========step1:create table") + tdSql.execute("drop database if exists test;") + tdSql.execute("create database test keep 3650;") + tdSql.execute("use test;") + tdSql.execute( + f"""create stable if not exists test.st + (ts timestamp, c0 tinyint, c1 smallint, c2 int, c3 bigint, c4 double, c5 float, c6 bool, c7 varchar(10), c8 nchar(10), c9 tinyint unsigned, c10 smallint unsigned, c11 int unsigned, c12 bigint unsigned) + tags(groupid int); + """ + ) + for stream in self.streams: + if len(stream["notify_server"]) == 0: + continue + stream_option = f"TRIGGER {self.trigger_mode}" + if self.trigger_mode != "FORCE_WINDOW_CLOSE": + stream_option += " IGNORE UPDATE 0" + if not stream["stream_name"].startswith("s_count"): + stream_option += " IGNORE EXPIRED 0" + if stream["stream_name"].startswith("s_count"): + stream_option += " WATERMARK 1a" + stream_sql = f"""create stream {stream["stream_name"]} {stream_option} into {stream["dest_table"]} as + select _wstart, _wend, min(c0), max(c1), min(c2), max(c3), min(c4), max(c5), first(c6), first(c7), last(c8), min(c9), max(c10), min(c11), max(c12) + from test.st {stream["partitioned"] and "partition by tbname" or ""} + {stream["window_clause"]} + notify ({stream["notify_address"]}) on ({self.notify_event}); + """ + tdSql.execute(stream_sql, show=True) + # Wait for the stream tasks to be ready + for i in range(50): + tdLog.info(f"i={i} wait for stream tasks ready ...") + time.sleep(1) + rows = tdSql.query("select * from information_schema.ins_stream_tasks where status <> 'ready';") + if rows == 0: + break + + def insert_data(self): + tdLog.info("insert stream notify test data.") + # taosBenchmark run + json_file = self.disorder and "stream_notify_disorder.json" or "stream_notify.json" + json = etool.curFile(__file__, json_file) + etool.benchMark(json=json) + + def wait_all_streams_done(self): + while True: + tdLog.info("wait for all streams done ...") + time.sleep(10) + rows = tdSql.query("select stream_name, level, notify_event_stat from information_schema.ins_stream_tasks where notify_event_stat is not null;") + num_pushed = 0 + num_sent = 0 + for i in range(rows): + tdLog.printNoPrefix(f"{tdSql.getData(i, 0)}, {tdSql.getData(i, 1)}, {tdSql.getData(i, 2)}") + notify_event_stat = tdSql.getData(i, 2) + match = re.search(r"Push (\d+)x, (\d+) elems", notify_event_stat) + if match: + num_pushed += int(match.group(2)) + match = re.search(r"Send (\d+)x, (\d+) elems", notify_event_stat) + if match: + num_sent += int(match.group(2)) + if num_pushed == num_sent: + break + tdLog.info("wait for all notify servers stop ...") + for stream in self.streams: + for server in stream["notify_server"]: + server.stop() + + def parse(self, log_file, out_file, stream_name): + message_ids = set() + events_map = {} + has_open = "window_open" in self.notify_event + has_close = "window_close" in self.notify_event + with open(log_file, "r", encoding="utf-8") as f: + for line in f: + data = json.loads(line) + + # Check if the data has the required fields: messageId, timestamp, streams + if "messageId" not in data: + print(f"Error: Missing 'messageId' in data {data}") + return False + if "timestamp" not in data: + print(f"Error: Missing 'timestamp' in data {data}") + return False + if "streams" not in data: + print(f"Error: Missing 'streams' in data {data}") + return False + + # Check if the message id is duplicated + if message_ids.__contains__(data["messageId"]): + print(f"Error: Duplicate message id {data['messageId']}") + return False + message_ids.add(data["messageId"]) + + # Check if the streams is correct + for stream in data["streams"]: + # Check if the stream has the required fields: streamName, events + if "streamName" not in stream: + print(f"Error: Missing 'streamName' in stream {stream}") + return False + if "events" not in stream: + print(f"Error: Missing 'events' in stream {stream}") + return False + + # Check if the stream name is correct + if stream["streamName"] != stream_name: + print(f"Error: Incorrect stream name {stream['streamName']}") + return False + + # Check if the events are correct + for event in stream["events"]: + # Check if the event has the required fields: tableName, eventType, eventTime, windowId, windowType + if "tableName" not in event: + print(f"Error: Missing 'tableName' in event {event}") + return False + if "eventType" not in event: + print(f"Error: Missing 'eventType' in event {event}") + return False + if "eventTime" not in event: + print(f"Error: Missing 'eventTime' in event {event}") + return False + if "windowId" not in event: + print(f"Error: Missing 'windowId' in event {event}") + return False + if "windowType" not in event: + print(f"Error: Missing 'windowType' in event {event}") + return False + if event["eventType"] not in [ + "WINDOW_OPEN", + "WINDOW_CLOSE", + "WINDOW_INVALIDATION", + ]: + print(f"Error: Invalid event type {event['eventType']}") + return False + if event["windowType"] not in [ + "Time", + "State", + "Session", + "Event", + "Count", + ]: + print(f"Error: Invalid window type {event['windowType']}") + return False + + if event["eventType"] == "WINDOW_INVALIDATION": + if not has_close: + print(f"Error: WINDOW_INVALIDATION event is not allowed") + return False + # WINDOW_INVALIDATION must have fields: windowStart, windowEnd + if "windowStart" not in event: + print(f"Error: Missing 'windowStart' in event {event}") + return False + if "windowEnd" not in event: + print(f"Error: Missing 'windowEnd' in event {event}") + return False + events_map.pop( + (event["tableName"], event["windowId"]), None + ) + continue + + # Get the event from the event map; if it doesn't exist, create a new one + e = events_map.get((event["tableName"], event["windowId"])) + if e is None: + events_map[(event["tableName"], event["windowId"])] = { + "opened": False, + "closed": False, + "wstart": 0, + "wend": 0, + } + e = events_map.get((event["tableName"], event["windowId"])) + + if event["eventType"] == "WINDOW_OPEN": + if not has_open: + print(f"Error: WINDOW_OPEN event is not allowed") + return False + # WINDOW_OPEN for all windows must have field: windowStart + if "windowStart" not in event: + print(f"Error: Missing 'windowStart' in event {event}") + return False + if event["windowType"] == "State": + # WINDOW_OPEN for State window must also have fields: prevState, curState + if "prevState" not in event: + print( + f"Error: Missing 'prevState' in event {event}" + ) + return False + if "curState" not in event: + print(f"Error: Missing 'curState' in event {event}") + return False + elif event["windowType"] == "Event": + # WINDOW_OPEN for Event window must also have fields: triggerCondition + if "triggerCondition" not in event: + print( + f"Error: Missing 'triggerCondition' in event {event}" + ) + return False + e["opened"] = True + e["wstart"] = event["windowStart"] + elif event["eventType"] == "WINDOW_CLOSE": + if not has_close: + print(f"Error: WINDOW_CLOSE event is not allowed") + return False + # WINDOW_CLOSE for all windows must have fields: windowStart, windowEnd, result + if "windowStart" not in event: + print(f"Error: Missing 'windowStart' in event {event}") + return False + if "windowEnd" not in event: + print(f"Error: Missing 'windowEnd' in event {event}") + return False + if "result" not in event: + print(f"Error: Missing 'result' in event {event}") + return False + if event["windowType"] == "State": + # WINDOW_CLOSE for State window must also have fields: curState, nextState + if "curState" not in event: + print(f"Error: Missing 'curState' in event {event}") + return False + if "nextState" not in event: + print( + f"Error: Missing 'nextState' in event {event}" + ) + return False + elif event["windowType"] == "Event": + # WINDOW_CLOSE for Event window must also have fields: triggerCondition + if "triggerCondition" not in event: + print( + f"Error: Missing 'triggerCondition' in event {event}" + ) + return False + e["closed"] = True + e["wstart"] = event["windowStart"] + e["wend"] = event["windowEnd"] + + # Collect all the windows that closed + windows_map = {} + for k, v in events_map.items(): + if not v["closed"]: + continue + e = windows_map.get(k[0]) + if e is None: + windows_map[k[0]] = [] + e = windows_map.get(k[0]) + e.append((v["wstart"], v["wend"])) + + # Sort the windows by start time + for k, v in windows_map.items(): + v.sort(key=lambda x: x[0]) + + # Write all collected window info to the specified output file in sorted order as csv format + with open(out_file, "w", encoding="utf-8") as f: + f.write("wstart,wend,tbname\n") + for k, v in sorted(windows_map.items()): + for w in v: + f.write(f"{w[0]},{w[1]},\"{k}\"\n") + return True + + def check_notify_result(self): + all_right = True + for stream in self.streams: + if len(stream["notify_server"]) == 0 or not "window_close" in self.notify_event: + continue + query_file = f"{self.current_dir}/{self.id}_{stream['stream_name']}.csv" + query_sql = f"select cast(_wstart as bigint) as wstart, cast(_wend as bigint) as wend, tbname from test.{stream['dest_table']} order by tbname, wstart >> {query_file};" + tdLog.info("query_sql: " + query_sql) + os.system(f"taos -c {tdCom.getClientCfgPath()} -s '{query_sql}'") + for i in range(self.num_addr_per_stream): + server = stream["notify_server"][i] + parse_file = f"{self.current_dir}/{self.id}_{stream['stream_name']}_{i}.csv" + if os.path.exists(parse_file): + os.remove(parse_file) + if not self.parse(f"{self.current_dir}/{self.id}_{stream['stream_name']}_{i}.log", parse_file, stream["stream_name"]): + tdLog.exit(f"Error: {stream['stream_name']}_{i} parse notify result failed") + # Compare the result using diff command + diff_file = f"{self.current_dir}/{self.id}_{stream['stream_name']}_{i}.diff" + if os.path.exists(diff_file): + os.remove(diff_file) + os.system(f"diff --strip-trailing-cr {query_file} {parse_file} > {diff_file}") + if os.path.getsize(diff_file) != 0: + tdLog.info(f"Error: {stream['stream_name']}_{i} notify result is not correct") + all_right = False + if not all_right: + raise Exception("Error: notify result is not correct") + + def drop_all_streams(self): + for stream in self.streams: + tdSql.execute(f"drop stream if exists {stream['stream_name']};") + # Also remove all generaetd files + query_file = f"{self.current_dir}/{self.id}_{stream['stream_name']}.csv" + if os.path.exists(query_file): + os.remove(query_file) + for i in range(self.num_addr_per_stream): + log_file = f"{self.current_dir}/{self.id}_{stream['stream_name']}_{i}.log" + parse_file = f"{self.current_dir}/{self.id}_{stream['stream_name']}_{i}.csv" + diff_file = f"{self.current_dir}/{self.id}_{stream['stream_name']}_{i}.diff" + if os.path.exists(log_file): + os.remove(log_file) + if os.path.exists(parse_file): + os.remove(parse_file) + if os.path.exists(diff_file): + os.remove(diff_file) + + def run(self): + tdLog.info(f"Start to execute TestStreamNotifySinglePass({self.num_addr_per_stream}, {self.trigger_mode}, {self.notify_event}, {self.disorder})") + self.gen_streams() + self.create_streams() + self.insert_data() + self.wait_all_streams_done() + self.check_notify_result() + self.drop_all_streams() + tdLog.info(f"TestStreamNotifySinglePass({self.num_addr_per_stream}, {self.trigger_mode}, {self.notify_event}, {self.disorder}) successfully executed") + +class TDTestCase(TBase): + def init(self, conn, logSql, replicaVar=1): + self.replicaVar = int(replicaVar) + tdLog.debug(f"start to excute {__file__}") + tdSql.init(conn.cursor(), True) + + def run(self): + # Disable many tests due to long execution time + + # TestStreamNotifySinglePass(num_addr_per_stream=1, trigger_mode="AT_ONCE", notify_event="'window_open', 'window_close'", disorder=False).run() + # TestStreamNotifySinglePass(num_addr_per_stream=1, trigger_mode="MAX_DELAY 10s", notify_event="'window_open', 'window_close'", disorder=False).run() + TestStreamNotifySinglePass(num_addr_per_stream=1, trigger_mode="WINDOW_CLOSE", notify_event="'window_open', 'window_close'", disorder=False).run() + # TestStreamNotifySinglePass(num_addr_per_stream=1, trigger_mode="FORCE_WINDOW_CLOSE", notify_event="'window_open', 'window_close'", disorder=False).run() + + # TestStreamNotifySinglePass(num_addr_per_stream=1, trigger_mode="AT_ONCE", notify_event="'window_open', 'window_close'", disorder=True).run() + # TestStreamNotifySinglePass(num_addr_per_stream=1, trigger_mode="MAX_DELAY 10s", notify_event="'window_open', 'window_close'", disorder=True).run() + TestStreamNotifySinglePass(num_addr_per_stream=1, trigger_mode="WINDOW_CLOSE", notify_event="'window_open', 'window_close'", disorder=True).run() + # TestStreamNotifySinglePass(num_addr_per_stream=1, trigger_mode="FORCE_WINDOW_CLOSE", notify_event="'window_open', 'window_close'", disorder=True).run() + + # TestStreamNotifySinglePass(num_addr_per_stream=1, trigger_mode="AT_ONCE", notify_event="'window_close'", disorder=False).run() + TestStreamNotifySinglePass(num_addr_per_stream=1, trigger_mode="MAX_DELAY 10s", notify_event="'window_close'", disorder=False).run() + # TestStreamNotifySinglePass(num_addr_per_stream=1, trigger_mode="WINDOW_CLOSE", notify_event="'window_close'", disorder=False).run() + # TestStreamNotifySinglePass(num_addr_per_stream=1, trigger_mode="FORCE_WINDOW_CLOSE", notify_event="'window_close'", disorder=False).run() + + TestStreamNotifySinglePass(num_addr_per_stream=3, trigger_mode="AT_ONCE", notify_event="'window_open'", disorder=False).run() + # TestStreamNotifySinglePass(num_addr_per_stream=3, trigger_mode="MAX_DELAY 10s", notify_event="'window_open'", disorder=False).run() + # TestStreamNotifySinglePass(num_addr_per_stream=3, trigger_mode="WINDOW_CLOSE", notify_event="'window_open'", disorder=False).run() + # TestStreamNotifySinglePass(num_addr_per_stream=3, trigger_mode="FORCE_WINDOW_CLOSE", notify_event="'window_open'", disorder=False).run() + + def stop(self): + tdLog.success(f"{__file__} successfully executed") + + +tdCases.addLinux(__file__, TDTestCase()) +tdCases.addWindows(__file__, TDTestCase()) diff --git a/tests/parallel_test/longtimeruning_cases.task b/tests/parallel_test/longtimeruning_cases.task index 1034771159..a4230882a9 100644 --- a/tests/parallel_test/longtimeruning_cases.task +++ b/tests/parallel_test/longtimeruning_cases.task @@ -10,6 +10,7 @@ # army-test #,,y,army,./pytest.sh python3 ./test.py -f multi-level/mlevel_basic.py -N 3 -L 3 -D 2 +,,y,army,./pytest.sh python3 ./test.py -f stream/test_stream_notify.py #tsim test #,,y,script,./test.sh -f tsim/query/timeline.sim diff --git a/tests/system-test/0-others/information_schema.py b/tests/system-test/0-others/information_schema.py index 5bdb744f6c..4874174f38 100644 --- a/tests/system-test/0-others/information_schema.py +++ b/tests/system-test/0-others/information_schema.py @@ -222,7 +222,7 @@ class TDTestCase: tdSql.query("select * from information_schema.ins_columns where db_name ='information_schema'") tdLog.info(len(tdSql.queryResult)) - tdSql.checkEqual(True, len(tdSql.queryResult) in range(313, 314)) + tdSql.checkEqual(True, len(tdSql.queryResult) in range(314, 315)) tdSql.query("select * from information_schema.ins_columns where db_name ='performance_schema'") tdSql.checkEqual(61, len(tdSql.queryResult)) diff --git a/tools/taos-tools/src/benchUtil.c b/tools/taos-tools/src/benchUtil.c index ad59c4e37e..8dcca30f8c 100644 --- a/tools/taos-tools/src/benchUtil.c +++ b/tools/taos-tools/src/benchUtil.c @@ -1033,18 +1033,26 @@ int convertStringToDatatype(char *type, int length) { return TSDB_DATA_TYPE_BOOL; } else if (0 == strCompareN(type, "tinyint", length)) { return TSDB_DATA_TYPE_TINYINT; + } else if (0 == strCompareN(type, "tinyint unsigned", length)) { + return TSDB_DATA_TYPE_UTINYINT; } else if (0 == strCompareN(type, "utinyint", length)) { return TSDB_DATA_TYPE_UTINYINT; } else if (0 == strCompareN(type, "smallint", length)) { return TSDB_DATA_TYPE_SMALLINT; + } else if (0 == strCompareN(type, "smallint unsigned", length)) { + return TSDB_DATA_TYPE_USMALLINT; } else if (0 == strCompareN(type, "usmallint", length)) { return TSDB_DATA_TYPE_USMALLINT; } else if (0 == strCompareN(type, "int", length)) { return TSDB_DATA_TYPE_INT; + } else if (0 == strCompareN(type, "int unsigned", length)) { + return TSDB_DATA_TYPE_UINT; } else if (0 == strCompareN(type, "uint", length)) { return TSDB_DATA_TYPE_UINT; } else if (0 == strCompareN(type, "bigint", length)) { return TSDB_DATA_TYPE_BIGINT; + } else if (0 == strCompareN(type, "bigint unsigned", length)) { + return TSDB_DATA_TYPE_UBIGINT; } else if (0 == strCompareN(type, "ubigint", length)) { return TSDB_DATA_TYPE_UBIGINT; } else if (0 == strCompareN(type, "float", length)) { From 77206771c815669f6a3c88d2b5f0cc7682020dc5 Mon Sep 17 00:00:00 2001 From: Jinqing Kuang Date: Sun, 16 Feb 2025 23:22:42 +0800 Subject: [PATCH 03/17] feat(stream)[TS-5469]: add event notification usage instructions Add usage syntax and detailed description of event notification fields in the user man fields in the user manual. --- docs/en/14-reference/03-taos-sql/14-stream.md | 224 +++++++++++++++++- docs/zh/14-reference/03-taos-sql/14-stream.md | 223 ++++++++++++++++- 2 files changed, 445 insertions(+), 2 deletions(-) diff --git a/docs/en/14-reference/03-taos-sql/14-stream.md b/docs/en/14-reference/03-taos-sql/14-stream.md index 704a7ebcbc..24d9bae468 100644 --- a/docs/en/14-reference/03-taos-sql/14-stream.md +++ b/docs/en/14-reference/03-taos-sql/14-stream.md @@ -9,7 +9,7 @@ import imgStream from './assets/stream-processing-01.png'; ## Creating Stream Computing ```sql -CREATE STREAM [IF NOT EXISTS] stream_name [stream_options] INTO stb_name[(field1_name, field2_name [PRIMARY KEY], ...)] [TAGS (create_definition [, create_definition] ...)] SUBTABLE(expression) AS subquery +CREATE STREAM [IF NOT EXISTS] stream_name [stream_options] INTO stb_name[(field1_name, field2_name [PRIMARY KEY], ...)] [TAGS (create_definition [, create_definition] ...)] SUBTABLE(expression) AS subquery [notification_definition] stream_options: { TRIGGER [AT_ONCE | WINDOW_CLOSE | MAX_DELAY time | FORCE_WINDOW_CLOSE] WATERMARK time @@ -85,6 +85,8 @@ CREATE STREAM streams1 IGNORE EXPIRED 1 WATERMARK 100s INTO streamt1 AS SELECT _wstart, count(*), avg(voltage) from meters PARTITION BY tbname COUNT_WINDOW(10); ``` +notification_definition clause specifies the addresses to which notifications should be sent when designated events occur during window computations, such as window opening or closing. For more details, see [Stream Computing Event Notifications](#stream-computing-event-notifications). + ## Stream Computation Partitioning You can use `PARTITION BY TBNAME`, tags, regular columns, or expressions to partition a stream for multi-partition computation. Each partition's timeline and window are independent, aggregating separately, and writing into different subtables of the target table. @@ -305,3 +307,223 @@ CREATE SNODE ON DNODE [id] The id is the serial number of the dnode in the cluster. Please be mindful of the selected dnode, as the intermediate state of stream computing will automatically be backed up on it. Starting from version 3.3.4.0, in a multi-replica environment, creating a stream will perform an **existence check** of snode, requiring the snode to be created first. If the snode does not exist, the stream cannot be created. + +## Stream Computing Event Notifications + +### User Guide + +Stream computing supports sending event notifications to external systems when windows open or close. Users can specify the events to be notified and the target addresses for receiving notification messages using the notification_definition clause. + +```sql +notification_definition: + NOTIFY (url [, url] ...) ON (event_type [, event_type] ...) [notification_options] + +event_type: + 'WINDOW_OPEN' + | 'WINDOW_CLOSE' + +notification_options: { + NOTIFY_HISTORY [0|1] + ON_FAILURE [DROP|PAUSE] +} +``` + +The rules for the syntax above are as follows: +1. `url`: Specifies the target address for the notification. It must include the protocol, IP or domain name, port, and may include a path and parameters. Currently, only the websocket protocol is supported. For example: 'ws://localhost:8080', 'ws://localhost:8080/notify', 'wss://localhost:8080/notify?key=foo'. +2. `event_type`: Defines the events that trigger notifications. Supported event types include: + 1. 'WINDOW_OPEN': Window open event; triggered when any type of window opens. + 2. 'WINDOW_CLOSE': Window close event; triggered when any type of window closes. +3. `NOTIFY_HISTORY`: Controls whether to trigger notifications during the computation of historical data. The default value is 0, which means no notifications are sent. +4. `ON_FAILURE`: Determines whether to allow dropping some events if sending notifications fails (e.g., in poor network conditions). The default value is `PAUSE`: + 1. PAUSE means that the stream computing task is paused if sending a notification fails. taosd will retry until the notification is successfully delivered and the task resumes. + 2. DROP means that if sending a notification fails, the event information is discarded, and the stream computing task continues running unaffected. + +For example, the following creates a stream that computes the per-minute average current from electric meters and sends notifications to two target addresses when the window opens and closes. It does not send notifications for historical data and does not allow dropping notifications on failure: + +```sql +CREATE STREAM avg_current_stream FILL_HISTORY 1 + AS SELECT _wstart, _wend, AVG(current) FROM meters + INTERVAL (1m) + NOTIFY ('ws://localhost:8080/notify', 'wss://192.168.1.1:8080/notify?key=foo') + ON ('WINDOW_OPEN', 'WINDOW_CLOSE'); + NOTIFY_HISTORY 0 + ON_FAILURE PAUSE; +``` + +When the specified events are triggered, taosd will send a POST request to the given URL(s) with a JSON message body. A single request may contain events from several streams, and the event types may differ. + +The details of the event information depend on the type of window: + +1. Time Window: At the opening, the start time is sent; at the closing, the start time, end time, and computation result are sent. +2. State Window: At the opening, the start time, previous window's state, and current window's state are sent; at closing, the start time, end time, computation result, current window state, and next window state are sent. +3. Session Window: At the opening, the start time is sent; at the closing, the start time, end time, and computation result are sent. +4. Event Window: At the opening, the start time along with the data values and corresponding condition index that triggered the window opening are sent; at the closing, the start time, end time, computation result, and the triggering data value and condition index for window closure are sent. +5. Count Window: At the opening, the start time is sent; at the closing, the start time, end time, and computation result are sent. + +An example structure for the notification message is shown below: + +```json +{ + "messageId": "unique-message-id-12345", + "timestamp": 1733284887203, + "streams": [ + { + "streamName": "avg_current_stream", + "events": [ + { + "tableName": "t_a667a16127d3b5a18988e32f3e76cd30", + "eventType": "WINDOW_OPEN", + "eventTime": 1733284887097, + "windowId": "window-id-67890", + "windowType": "Time", + "windowStart": 1733284800000 + }, + { + "tableName": "t_a667a16127d3b5a18988e32f3e76cd30", + "eventType": "WINDOW_CLOSE", + "eventTime": 1733284887197, + "windowId": "window-id-67890", + "windowType": "Time", + "windowStart": 1733284800000, + "windowEnd": 1733284860000, + "result": { + "_wstart": 1733284800000, + "avg(current)": 1.3 + } + } + ] + }, + { + "streamName": "max_voltage_stream", + "events": [ + { + "tableName": "t_96f62b752f36e9b16dc969fe45363748", + "eventType": "WINDOW_OPEN", + "eventTime": 1733284887231, + "windowId": "window-id-13579", + "windowType": "Event", + "windowStart": 1733284800000, + "triggerCondition": { + "conditionIndex": 0, + "fieldValue": { + "c1": 10, + "c2": 15 + } + }, + }, + { + "tableName": "t_96f62b752f36e9b16dc969fe45363748", + "eventType": "WINDOW_CLOSE", + "eventTime": 1733284887231, + "windowId": "window-id-13579", + "windowType": "Event", + "windowStart": 1733284800000, + "windowEnd": 1733284810000, + "triggerCondition": { + "conditionIndex": 1, + "fieldValue": { + "c1": 20 + "c2": 3 + } + }, + "result": { + "_wstart": 1733284800000, + "max(voltage)": 220 + } + } + ] + } + ] +} +``` + +The following sections explain the fields in the notification message. + +### Root-Level Field Descriptions + +1. "messageId": A string that uniquely identifies the notification message. It ensures that the entire message can be tracked and de-duplicated. +2. "timestamp": A long integer timestamp representing the time when the notification message was generated, accurate to the millisecond (i.e., the number of milliseconds since '00:00, Jan 1 1970 UTC'). +3. "streams": An array containing the event information for multiple stream tasks. (See the following sections for details.) + +### "stream" Object Field Descriptions + +1. "streamName": A string representing the name of the stream task, used to identify which stream the events belong to. +2. "events": An array containing the list of event objects for the stream task. Each event object includes detailed information. (See the next sections for details.) + +### "event" Object Field Descriptions + +#### Common Fields + +These fields are common to all event objects. +1. "tableName": A string indicating the name of the target subtable. +2. "eventType": A string representing the event type ("WINDOW_OPEN", "WINDOW_CLOSE", or "WINDOW_INVALIDATION"). +3. "eventTime": A long integer timestamp that indicates when the event was generated, accurate to the millisecond (i.e., the number of milliseconds since '00:00, Jan 1 1970 UTC'). +4. "windowId": A string representing the unique identifier for the window. This ID ensures that the open and close events for the same window can be correlated. In the case that taosd restarts due to a fault, some events may be sent repeatedly, but the windowId remains constant for the same window. +5. "windowType": A string that indicates the window type ("Time", "State", "Session", "Event", or "Count"). + +#### Fields for Time Windows + +These fields are present only when "windowType" is "Time". +1. When "eventType" is "WINDOW_OPEN", the following field is included: + 1. "windowStart": A long integer timestamp representing the start time of the window, matching the time precision of the result table. +2. When "eventType" is "WINDOW_CLOSE", the following fields are included: + 1. "windowStart": A long integer timestamp representing the start time of the window. + 1. "windowEnd": A long integer timestamp representing the end time of the window. + 1. "result": An object containing key-value pairs of the computed result columns and their corresponding values. + +#### Fields for State Windows + +These fields are present only when "windowType" is "State". +1. When "eventType" is "WINDOW_OPEN", the following fields are included: + 1. "windowStart": A long integer timestamp representing the start time of the window. + 1. "prevState": A value of the same type as the state column, representing the state of the previous window. If there is no previous window (i.e., this is the first window), it will be NULL. + 1. "curState": A value of the same type as the state column, representing the current window's state. +2. When "eventType" is "WINDOW_CLOSE", the following fields are included: + 1. "windowStart": A long integer timestamp representing the start time of the window. + 1. "windowEnd": A long integer timestamp representing the end time of the window. + 1. "curState": The current window's state. + 1. "nextState": The state for the next window. + 1. "result": An object containing key-value pairs of the computed result columns and their corresponding values. + +#### Fields for Session Windows + +These fields are present only when "windowType" is "Session". +1. When "eventType" is "WINDOW_OPEN", the following field is included: + 1. "windowStart": A long integer timestamp representing the start time of the window. +2. When "eventType" is "WINDOW_CLOSE", the following fields are included: + 1. "windowStart": A long integer timestamp representing the start time of the window. + 1. "windowEnd": A long integer timestamp representing the end time of the window. + 1. "result": An object containing key-value pairs of the computed result columns and their corresponding values. + +#### Fields for Event Windows + +These fields are present only when "windowType" is "Event". +1. When "eventType" is "WINDOW_OPEN", the following fields are included: + 1. "windowStart": A long integer timestamp representing the start time of the window. + 1. "triggerCondition": An object that provides information about the condition that triggered the window to open. It includes: + 1. "conditionIndex": An integer representing the index of the condition that triggered the window, starting from 0. + 1. "fieldValue": An object containing key-value pairs of the column names related to the condition and their respective values. +2. When "eventType" is "WINDOW_CLOSE", the following fields are included: + 1. "windowStart": A long integer timestamp representing the start time of the window. + 1. "windowEnd": A long integer timestamp representing the end time of the window. + 1. "triggerCondition": An object that provides information about the condition that triggered the window to close. It includes: + 1. "conditionIndex": An integer representing the index of the condition that triggered the closure, starting from 0. + 1. "fieldValue": An object containing key-value pairs of the related column names and their respective values. + 1. "result": An object containing key-value pairs of the computed result columns and their corresponding values. + +#### Fields for Count Windows + +These fields are present only when "windowType" is "Count". +1. When "eventType" is "WINDOW_OPEN", the following field is included: + 1. "windowStart": A long integer timestamp representing the start time of the window. +2. When "eventType" is "WINDOW_CLOSE", the following fields are included: + 1. "windowStart": A long integer timestamp representing the start time of the window. + 1. "windowEnd": A long integer timestamp representing the end time of the window. + 1. "result": An object containing key-value pairs of the computed result columns and their corresponding values. + +#### Fields for Window Invalidation + +Due to scenarios such as data disorder, updates, or deletions during stream computing, windows that have already been generated might be removed or their results need to be recalculated. In such cases, a notification with the eventType "WINDOW_INVALIDATION" is sent to inform which windows have been invalidated. +For events with "eventType" as "WINDOW_INVALIDATION", the following fields are included: +1. "windowStart": A long integer timestamp representing the start time of the window. +1. "windowEnd": A long integer timestamp representing the end time of the window. diff --git a/docs/zh/14-reference/03-taos-sql/14-stream.md b/docs/zh/14-reference/03-taos-sql/14-stream.md index 5e5a82b95c..e255102f76 100644 --- a/docs/zh/14-reference/03-taos-sql/14-stream.md +++ b/docs/zh/14-reference/03-taos-sql/14-stream.md @@ -8,7 +8,7 @@ description: 流式计算的相关 SQL 的详细语法 ## 创建流式计算 ```sql -CREATE STREAM [IF NOT EXISTS] stream_name [stream_options] INTO stb_name[(field1_name, field2_name [PRIMARY KEY], ...)] [TAGS (create_definition [, create_definition] ...)] SUBTABLE(expression) AS subquery +CREATE STREAM [IF NOT EXISTS] stream_name [stream_options] INTO stb_name[(field1_name, field2_name [PRIMARY KEY], ...)] [TAGS (create_definition [, create_definition] ...)] SUBTABLE(expression) AS subquery [notification_definition] stream_options: { TRIGGER [AT_ONCE | WINDOW_CLOSE | MAX_DELAY time | FORCE_WINDOW_CLOSE] WATERMARK time @@ -83,6 +83,8 @@ CREATE STREAM streams1 IGNORE EXPIRED 1 WATERMARK 100s INTO streamt1 AS SELECT _wstart, count(*), avg(voltage) from meters PARTITION BY tbname COUNT_WINDOW(10); ``` +notification_definition 子句定义了窗口计算过程中,在窗口打开/关闭等指定事件发生时,需要向哪些地址发送通知。详见 [流式计算的事件通知](#流失计算的事件通知) + ## 流式计算的 partition 可以使用 PARTITION BY TBNAME,tag,普通列或者表达式,对一个流进行多分区的计算,每个分区的时间线与时间窗口是独立的,会各自聚合,并写入到目的表中的不同子表。 @@ -297,3 +299,222 @@ CREATE SNODE ON DNODE [id] ``` 其中的 id 是集群中的 dnode 的序号。请注意选择的dnode,流计算的中间状态将自动在其上进行备份。 从 3.3.4.0 版本开始,在多副本环境中创建流会进行 snode 的**存在性检查**,要求首先创建 snode。如果 snode 不存在,无法创建流。 + +## 流式计算的事件通知 + +### 使用说明 + +流式计算支持在窗口打开/关闭时,向外部系统发送相关的事件通知。用户通过 notification_definition 来指定需要通知的事件,以及用于接收通知消息的目标地址。 + +```sql +notification_definition: + NOTIFY (url [, url] ...) ON (event_type [, event_type] ...) [notification_options] + +event_type: + 'WINDOW_OPEN' + | 'WINDOW_CLOSE' + +notification_options: { + NOTIFY_HISTORY [0|1] + ON_FAILURE [DROP|PAUSE] +} +``` + +上述语法中的相关规则含义如下: +1. `url`: 指定通知的目标地址,必须包括协议、IP 或域名、端口号,并允许包含路径、参数。目前仅支持 websocket 协议。例如:'ws://localhost:8080','ws://localhost:8080/notify','wss://localhost:8080/notify?key=foo'。 +1. `event_type`: 定义需要通知的事件,支持的事件类型有: + 1. 'WINDOW_OPEN':窗口打开事件,所有类型的窗口打开时都会触发 + 1. 'WINDOW_CLOSE':窗口关闭事件,所有类型的窗口关闭时都会触发 +1. `NOTIFY_HISTORY`: 控制是否在计算历史数据时触发通知,默认值为0,即不触发 +1. `ON_FAILURE`: 向通知地址发送通知失败时(比如网络不佳场景)是否允许丢弃部分事件,默认值为 `PAUSE` + 1. PAUSE 表示发送通知失败时暂停流计算任务。taosd 会重试发送通知,直到发送成功后,任务自动恢复运行。 + 1. DROP 表示发送通知失败时直接丢弃事件信息,流计算任务继续运行,不受影响 + +比如,以下示例创建一个流,计算电表电流的每分钟平均值,并在窗口打开、关闭时向两个通知地址发送通知,计算历史数据时不发送通知,不允许在通知发送失败时丢弃通知: + +```sql +CREATE STREAM avg_current_stream FILL_HISTORY 1 + AS SELECT _wstart, _wend, AVG(current) FROM meters + INTERVAL (1m) + NOTIFY ('ws://localhost:8080/notify', 'wss://192.168.1.1:8080/notify?key=foo') + ON ('WINDOW_OPEN', 'WINDOW_CLOSE'); + NOTIFY_HISTORY 0 + ON_FAILURE PAUSE; +``` + +当触发指定的事件时,taosd 会向指定的 URL 发送 POST 请求,消息体为 JSON 格式。一个请求可能包含若干个流的若干个事件,且事件类型不一定相同。 +事件信息视窗口类型而定: + +1. 时间窗口:开始时发送起始时间;结束时发送起始时间、结束时间、计算结果。 +1. 状态窗口:开始时发送起始时间、前一个窗口的状态值、当前窗口的状态值;结束时发送起始时间、结束时间、计算结果、当前窗口的状态值、下一个窗口的状态值。 +1. 会话窗口:开始时发送起始时间;结束时发送起始时间、结束时间、计算结果。 +1. 事件窗口:开始时发送起始时间,触发窗口打开的数据值和对应条件编号;结束时发送起始时间、结束时间、计算结果、触发窗口关闭的数据值和对应条件编号。 +1. 计数窗口:开始时发送起始时间;结束时发送起始时间、结束时间、计算结果。 + +通知消息的结构示例如下: + +```json +{ + "messageId": "unique-message-id-12345", + "timestamp": 1733284887203, + "streams": [ + { + "streamName": "avg_current_stream", + "events": [ + { + "tableName": "t_a667a16127d3b5a18988e32f3e76cd30", + "eventType": "WINDOW_OPEN", + "eventTime": 1733284887097, + "windowId": "window-id-67890", + "windowType": "Time", + "windowStart": 1733284800000 + }, + { + "tableName": "t_a667a16127d3b5a18988e32f3e76cd30", + "eventType": "WINDOW_CLOSE", + "eventTime": 1733284887197, + "windowId": "window-id-67890", + "windowType": "Time", + "windowStart": 1733284800000, + "windowEnd": 1733284860000, + "result": { + "_wstart": 1733284800000, + "avg(current)": 1.3 + } + } + ] + }, + { + "streamName": "max_voltage_stream", + "events": [ + { + "tableName": "t_96f62b752f36e9b16dc969fe45363748", + "eventType": "WINDOW_OPEN", + "eventTime": 1733284887231, + "windowId": "window-id-13579", + "windowType": "Event", + "windowStart": 1733284800000, + "triggerCondition": { + "conditionIndex": 0, + "fieldValue": { + "c1": 10, + "c2": 15 + } + }, + }, + { + "tableName": "t_96f62b752f36e9b16dc969fe45363748", + "eventType": "WINDOW_CLOSE", + "eventTime": 1733284887231, + "windowId": "window-id-13579", + "windowType": "Event", + "windowStart": 1733284800000, + "windowEnd": 1733284810000, + "triggerCondition": { + "conditionIndex": 1, + "fieldValue": { + "c1": 20 + "c2": 3 + } + }, + "result": { + "_wstart": 1733284800000, + "max(voltage)": 220 + } + } + ] + } + ] +} +``` + +后续小节是通知消息中各个字段的说明。 + +### 根级字段说明 + +1. "messageId": 字符串类型,是通知消息的唯一标识符,确保整条消息可以被追踪和去重。 +1. "timestamp": 长整型时间戳,表示通知消息生成的时间,精确到毫秒,即: '00:00, Jan 1 1970 UTC' 以来的毫秒数。 +1. "streams": 对象数组,包含多个流任务的事件信息。(详细信息见下节) + +### stream 对象的字段说明 + +1. "streamName": 字符串类型,流任务的名称,用于标识事件所属的流。 +1. "events": 对象数组,该流任务下的事件列表,包含一个或多个事件对象。(详细信息见下节) + +### event 对象的字段说明 + +#### 通用字段 + +这部分是所有 event 对象所共有的字段。 +1. "tableName": 字符串类型,是对应目标子表的表名。 +1. "eventType": 字符串类型,表示事件类型 ("WINDOW_OPEN", "WINDOW_CLOSE" 或 "WINDOW_INVALIDATION")。 +1. "eventTime": 长整型时间戳,表示事件生成时间,精确到毫秒,即: '00:00, Jan 1 1970 UTC' 以来的毫秒数。 +1. "windowId": 字符串类型,窗口的唯一标识符,确保打开和关闭事件的 ID 一致,便于外部系统将两者关联。如果 taosd 发生故障重启,部分事件可能会重复发送,会保证同一窗口的 windowId 保持不变。 +1. "windowType": 字符串类型,表示窗口类型 ("Time", "State", "Session", "Event", "Count")。 + +#### 时间窗口相关字段 + +这部分是 "windowType" 为"Time" 时 event 对象才有的字段。 +1. 如果 "eventType" 为 "WINDOW_OPEN",则包含如下字段: + 1. "windowStart": 长整型时间戳,表示窗口的开始时间,精度与结果表的时间精度一致。 +1. 如果 "eventType" 为 "WINDOW_CLOSE",则包含如下字段: + 1. "windowStart": 长整型时间戳,表示窗口的开始时间,精度与结果表的时间精度一致。 + 1. "windowEnd": 长整型时间戳,表示窗口的结束时间,精度与结果表的时间精度一致。 + 1. "result": 计算结果,为键值对形式,包含窗口计算的结果列列名及其对应的值。 + +#### 状态窗口相关字段 + +这部分是 "windowType" 为"State" 时 event 对象才有的字段。 +1. 如果 "eventType" 为 "WINDOW_OPEN",则包含如下字段: + 1. "windowStart": 长整型时间戳,表示窗口的开始时间,精度与结果表的时间精度一致。 + 1. "prevState": 与状态列的类型相同,表示上一个窗口的状态值。如果没有上一个窗口(即: 现在是第一个窗口),则为 NULL。 + 1. "curState": 与状态列的类型相同,表示当前窗口的状态值。 +1. 如果 "eventType" 为 "WINDOW_CLOSE",则包含如下字段: + 1. "windowStart": 长整型时间戳,表示窗口的开始时间,精度与结果表的时间精度一致。 + 1. "windowEnd": 长整型时间戳,表示窗口的结束时间,精度与结果表的时间精度一致。 + 1. "curState": 与状态列的类型相同,表示当前窗口的状态值。 + 1. "nextState": 与状态列的类型相同,表示下一个窗口的状态值。 + 1. "result": 计算结果,为键值对形式,包含窗口计算的结果列列名及其对应的值。 + +#### 会话窗口相关字段 + +这部分是 "windowType" 为"Session" 时 event 对象才有的字段。 +1. 如果 "eventType" 为 "WINDOW_OPEN",则包含如下字段: + 1. "windowStart": 长整型时间戳,表示窗口的开始时间,精度与结果表的时间精度一致。 +1. 如果 "eventType" 为 "WINDOW_CLOSE",则包含如下字段: + 1. "windowStart": 长整型时间戳,表示窗口的开始时间,精度与结果表的时间精度一致。 + 1. "windowEnd": 长整型时间戳,表示窗口的结束时间,精度与结果表的时间精度一致。 + 1. "result": 计算结果,为键值对形式,包含窗口计算的结果列列名及其对应的值。 + +#### 事件窗口相关字段 + +这部分是 "windowType" 为"Event" 时 event 对象才有的字段。 +1. 如果 "eventType" 为 "WINDOW_OPEN",则包含如下字段: + 1. "windowStart": 长整型时间戳,表示窗口的开始时间,精度与结果表的时间精度一致。 + 1. "triggerCondition": 触发窗口开始的条件信息,包括以下字段: + 1. "conditionIndex": 整型,表示满足的触发窗口开始的条件的索引,从0开始编号。 + 1. "fieldValue": 键值对形式,包含条件列列名及其对应的值。 +1. 如果 "eventType" 为 "WINDOW_CLOSE",则包含如下字段: + 1. "windowStart": 长整型时间戳,表示窗口的开始时间,精度与结果表的时间精度一致。 + 1. "windowEnd": 长整型时间戳,表示窗口的结束时间,精度与结果表的时间精度一致。 + 1. "triggerCondition": 触发窗口关闭的条件信息,包括以下字段: + 1. "conditionIndex": 整型,表示满足的触发窗口关闭的条件的索引,从0开始编号。 + 1. "fieldValue": 键值对形式,包含条件列列名及其对应的值。 + 1. "result": 计算结果,为键值对形式,包含窗口计算的结果列列名及其对应的值。 + +#### 计数窗口相关字段 + +这部分是 "windowType" 为"Count" 时 event 对象才有的字段。 +1. 如果 "eventType" 为 "WINDOW_OPEN",则包含如下字段: + 1. "windowStart": 长整型时间戳,表示窗口的开始时间,精度与结果表的时间精度一致。 +1. 如果 "eventType" 为 "WINDOW_CLOSE",则包含如下字段: + 1. "windowStart": 长整型时间戳,表示窗口的开始时间,精度与结果表的时间精度一致。 + 1. "windowEnd": 长整型时间戳,表示窗口的结束时间,精度与结果表的时间精度一致。 + 1. "result": 计算结果,为键值对形式,包含窗口计算的结果列列名及其对应的值。 + +#### 窗口失效相关字段 + +因为流计算过程中会遇到数据乱序、更新、删除等情况,可能造成已生成的窗口被删除,或者结果需要重新计算。此时会向通知地址发送一条 "WINDOW_INVALIDATION" 的通知,说明哪些窗口已经被删除。 +这部分是 "eventType" 为 "WINDOW_INVALIDATION" 时,event 对象才有的字段。 +1. "windowStart": 长整型时间戳,表示窗口的开始时间,精度与结果表的时间精度一致。 +1. "windowEnd": 长整型时间戳,表示窗口的结束时间,精度与结果表的时间精度一致。 From 792708317f6945b46f749cb2730f81aef69b5fa6 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Feb 2025 09:15:16 +0800 Subject: [PATCH 04/17] Update index.md --- docs/zh/06-advanced/06-TDgpt/04-forecast/index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md b/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md index 07ca48c6aa..8167155111 100644 --- a/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md +++ b/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md @@ -106,3 +106,7 @@ taos> select _flow, _fhigh, _frowts, forecast(i32) from foo; - PatchTST (Patch Time Series Transformer) - Temporal Fusion Transformer - TimesNet + +## 算法有效性评估工具 +TDgpt 提供预测分析算法有效性评估工具,通过调用该工具并设置合适的参数,能够使用 TDengine 中的数据作为回测依据,评估不同预测算法或相同的预测算法在不同的参数或训练模型的下的预测有效性。 + From 57b1eaaea3886d7eae67adaf8c0f499db00b5b05 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Feb 2025 09:34:10 +0800 Subject: [PATCH 05/17] doc: add figures. --- .../06-TDgpt/pic/ad-result-figure.png | Bin 0 -> 42407 bytes docs/zh/06-advanced/06-TDgpt/pic/ad-result.png | Bin 0 -> 20151 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/zh/06-advanced/06-TDgpt/pic/ad-result-figure.png create mode 100644 docs/zh/06-advanced/06-TDgpt/pic/ad-result.png diff --git a/docs/zh/06-advanced/06-TDgpt/pic/ad-result-figure.png b/docs/zh/06-advanced/06-TDgpt/pic/ad-result-figure.png new file mode 100644 index 0000000000000000000000000000000000000000..b1bf23178639f91b56ec1db66e5d0a63aba81cb3 GIT binary patch literal 42407 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 literal 0 HcmV?d00001 diff --git a/docs/zh/06-advanced/06-TDgpt/pic/ad-result.png b/docs/zh/06-advanced/06-TDgpt/pic/ad-result.png new file mode 100644 index 0000000000000000000000000000000000000000..80667c5733336d2f666c2160a36fbd77402bac26 GIT binary patch literal 20151 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 Date: Tue, 18 Feb 2025 09:34:29 +0800 Subject: [PATCH 06/17] Update index.md --- .../06-TDgpt/05-anomaly-detection/index.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md b/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md index 632492ce72..738886deb4 100644 --- a/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md +++ b/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md @@ -67,3 +67,35 @@ Query OK, 1 row(s) in set (0.028946s) ### 内置异常检测算法 分析平台内置了6个异常检查模型,分为3个类别,分别是[基于统计学的算法](./02-statistics-approach.md)、[基于数据密度的算法](./03-data-density.md)、以及[基于机器学习的算法](./04-machine-learning.md)。在不指定异常检测使用的方法的情况下,默认调用 IQR 进行异常检测。 +### 异常检测算法有效性比较工具 +TDgpt 提供自动化的工具对比不同数据集的不同算法监测有效性,针对异常检测算法提供查全率(recall)和查准率(precision)两个指标衡量不同算法的有效性。 +通过在配置文件中(analysis.ini)设置以下的选项可以调用需要使用的异常检测算法,异常检测算法测试用数据的时间范围、是否生成标注结果的图片、调用的异常检测算法以及相应的参数。 +调用异常检测算法比较之前,需要人工手动标注异常监测数据集的结果,即设置[anno_res]选项的数值,第几个数值是异常点,需要标注在数组中,如下测试集中,第 9 个点是异常点,我们就标注异常结果为 [9]. + +```bash +[ad] +# training data start time +start_time = 2021-01-01T01:01:01 + +# training data end time +end_time = 2021-01-01T01:01:11 + +# draw the results or not +gen_figure = true + +# annotate the anomaly_detection result +anno_res = [9] + +# algorithms list that is involved in the comparion +[ad.algos] +ksigma={"k": 2} +iqr={} +grubbs={} +lof={"algo":"auto", "n_neighbor": 3} +``` + +对比程序执行完成以后,会自动生成名称为`ad_result.xlsx` 的文件,第一个卡片是算法运行结果(如下图所示),分别包含了算法名称、执行调用参数、查全率、查准率、执行时间 5 个指标。 + + +如果设置了 `gen_figure` 为 `true`,比较程序会自动将每个参与比较的算法分析结果采用图片方式呈现出来(如下图所示)。 + From f245abe3703b049004bdc1418b60b0c68696d882 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Feb 2025 09:38:55 +0800 Subject: [PATCH 07/17] Update index.md --- docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md b/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md index 738886deb4..37fc0eaded 100644 --- a/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md +++ b/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md @@ -4,6 +4,8 @@ description: 异常检测算法 --- import ad from '../pic/anomaly-detection.png'; +import ad-result from '../pic/ad-result.png'; +import ad-result-figure '../pic/ad-result-figure.png' TDengine 中定义了异常(状态)窗口来提供异常检测服务。异常窗口可以视为一种特殊的**事件窗口(Event Window)**,即异常检测算法确定的连续异常时间序列数据所在的时间窗口。与普通事件窗口区别在于——时间窗口的起始时间和结束时间均是分析算法识别确定,不是用户给定的表达式进行判定。因此,在 `WHERE` 子句中使用 `ANOMALY_WINDOW` 关键词即可调用时序数据异常检测服务,同时窗口伪列(`_WSTART`, `_WEND`, `_WDURATION`)也能够像其他时间窗口一样用于描述异常窗口的起始时间(`_WSTART`)、结束时间(`_WEND`)、持续时间(`_WDURATION`)。例如: @@ -96,6 +98,9 @@ lof={"algo":"auto", "n_neighbor": 3} 对比程序执行完成以后,会自动生成名称为`ad_result.xlsx` 的文件,第一个卡片是算法运行结果(如下图所示),分别包含了算法名称、执行调用参数、查全率、查准率、执行时间 5 个指标。 +异常检测对比结果 -如果设置了 `gen_figure` 为 `true`,比较程序会自动将每个参与比较的算法分析结果采用图片方式呈现出来(如下图所示)。 +如果设置了 `gen_figure` 为 `true`,比较程序会自动将每个参与比较的算法分析结果采用图片方式呈现出来(如下图所示为 ksigma 的异常检测结果标注)。 + +异常检测标注图 From bac287a23cfdd409b2d469bdfae1ddd80204af74 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Feb 2025 09:43:43 +0800 Subject: [PATCH 08/17] Update index.md --- .../06-advanced/06-TDgpt/04-forecast/index.md | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md b/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md index 8167155111..9a0dc037e9 100644 --- a/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md +++ b/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md @@ -108,5 +108,26 @@ taos> select _flow, _fhigh, _frowts, forecast(i32) from foo; - TimesNet ## 算法有效性评估工具 -TDgpt 提供预测分析算法有效性评估工具,通过调用该工具并设置合适的参数,能够使用 TDengine 中的数据作为回测依据,评估不同预测算法或相同的预测算法在不同的参数或训练模型的下的预测有效性。 +TDgpt 提供预测分析算法有效性评估工具,调用该工具并设置合适的参数,能够使用 TDengine 中的数据作为回测依据,评估不同预测算法或相同的预测算法在不同的参数或训练模型的下的预测有效性。预测有效性的评估使用 `MSE` 和 `MAE` 指标作为依据,后续还将增加 `MAPE`指标。 + +```ini +[forecast] +# 训练数据的周期,每个周期包含多少个输入点 +period = 10 + +# 使用范围内最后 10 条记录作为预测结果 +rows = 10 + +# 训练数据开始时间 +start_time = 2024-12-28T14:55:10.885 + +# 训练数据结束时间 +end_time = 2024-12-31T10:07:01.300 + +# 输出结果的起始时间 +res_start_time = 1730000000000 + +# 是否绘制预测结果图 +gen_figure = true +``` From ccee92e6b877822b08fa6e2764a451f4316833bd Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Feb 2025 10:16:26 +0800 Subject: [PATCH 09/17] doc: add figures. --- docs/zh/06-advanced/06-TDgpt/pic/fc-result.png | Bin 0 -> 62207 bytes docs/zh/06-advanced/06-TDgpt/pic/fc.png | Bin 0 -> 13240 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/zh/06-advanced/06-TDgpt/pic/fc-result.png create mode 100644 docs/zh/06-advanced/06-TDgpt/pic/fc.png diff --git a/docs/zh/06-advanced/06-TDgpt/pic/fc-result.png b/docs/zh/06-advanced/06-TDgpt/pic/fc-result.png new file mode 100644 index 0000000000000000000000000000000000000000..6bcca3934cc33a14a4a3a20a8c8c434de621dbf3 GIT binary patch literal 62207 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

|kD8O3Z`pzBA;#g@+>89o#KKDX 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 literal 0 HcmV?d00001 diff --git a/docs/zh/06-advanced/06-TDgpt/pic/fc.png b/docs/zh/06-advanced/06-TDgpt/pic/fc.png new file mode 100644 index 0000000000000000000000000000000000000000..ff11a5dc488e8ff4196398babb19620170c98b30 GIT binary patch literal 13240 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 Q-8Q#W!J3NI@~?ycA9RTndH?_b literal 0 HcmV?d00001 From 444e1c3aa36288899bf54f48891c5cdfe14731bc Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Feb 2025 10:26:26 +0800 Subject: [PATCH 10/17] Update index.md --- docs/zh/06-advanced/06-TDgpt/04-forecast/index.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md b/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md index 9a0dc037e9..795b6e7b54 100644 --- a/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md +++ b/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md @@ -108,7 +108,7 @@ taos> select _flow, _fhigh, _frowts, forecast(i32) from foo; - TimesNet ## 算法有效性评估工具 -TDgpt 提供预测分析算法有效性评估工具,调用该工具并设置合适的参数,能够使用 TDengine 中的数据作为回测依据,评估不同预测算法或相同的预测算法在不同的参数或训练模型的下的预测有效性。预测有效性的评估使用 `MSE` 和 `MAE` 指标作为依据,后续还将增加 `MAPE`指标。 +TDgpt 提供预测分析算法有效性评估工具 `analytics_compare`,调用该工具并设置合适的参数,能够使用 TDengine 中的数据作为回测依据,评估不同预测算法或相同的预测算法在不同的参数或训练模型的下的预测有效性。预测有效性的评估使用 `MSE` 和 `MAE` 指标作为依据,后续还将增加 `MAPE`指标。 ```ini [forecast] @@ -119,10 +119,10 @@ period = 10 rows = 10 # 训练数据开始时间 -start_time = 2024-12-28T14:55:10.885 +start_time = 1949-01-01T00:00:00 # 训练数据结束时间 -end_time = 2024-12-31T10:07:01.300 +end_time = 1960-12-01T00:00:00 # 输出结果的起始时间 res_start_time = 1730000000000 @@ -131,3 +131,8 @@ res_start_time = 1730000000000 gen_figure = true ``` +算法对比分析运行完成以后,生成 fc-results.xlsx 文件,其中包含了调用算法的预测分析误差、执行时间、调用参数等信息。如下图所示: + + +如果设置了 `gen_figure` 为 true,分析结果中还会有绘制的分析预测结果图(如下图所示)。 + From d1bca419ea274c48e23bfd4be869a28460f2efa9 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Feb 2025 10:29:01 +0800 Subject: [PATCH 11/17] Update index.md --- docs/zh/06-advanced/06-TDgpt/04-forecast/index.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md b/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md index 795b6e7b54..ab97883072 100644 --- a/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md +++ b/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md @@ -3,6 +3,9 @@ title: 预测算法 description: 预测算法 --- +import fc-result from '../pic/fc.png'; +import fc-result-figure '../pic/fc-result.png'; + 时序数据预测处理以持续一个时间段的时序数据作为输入,预测接下来一个连续时间区间内时间序列数据趋势。用户可以指定输出的(预测)时间序列数据点的数量,因此其输出的结果行数不确定。为此,TDengine 使用新 SQL 函数 `FORECAST` 提供时序数据预测服务。基础数据(用于预测的历史时间序列数据)是该函数的输入,预测结果是该函数的输出。用户可以通过 `FORECAST` 函数调用 Anode 提供的预测算法提供的服务。 在后续章节中,使用时序数据表`foo`作为示例,介绍预测和异常检测算法的使用方式,`foo` 表的模式如下: @@ -133,6 +136,10 @@ gen_figure = true 算法对比分析运行完成以后,生成 fc-results.xlsx 文件,其中包含了调用算法的预测分析误差、执行时间、调用参数等信息。如下图所示: +预测对比结果 + 如果设置了 `gen_figure` 为 true,分析结果中还会有绘制的分析预测结果图(如下图所示)。 +预测对比结果 + From a546f509d8a3b33435bfcc08340b91bf4b9767e2 Mon Sep 17 00:00:00 2001 From: Yaming Pei Date: Tue, 18 Feb 2025 10:30:36 +0800 Subject: [PATCH 12/17] docs: add api table for odbc (#29751) * docs: add api table for zh-cn * docs: add api table for en-us * docs: fix some misdescriptions for odbc --- docs/en/14-reference/05-connector/50-odbc.md | 148 ++++++++++++++--- docs/zh/14-reference/05-connector/50-odbc.mdx | 150 +++++++++++++++--- 2 files changed, 262 insertions(+), 36 deletions(-) diff --git a/docs/en/14-reference/05-connector/50-odbc.md b/docs/en/14-reference/05-connector/50-odbc.md index 7f71436739..b4c3e23e13 100644 --- a/docs/en/14-reference/05-connector/50-odbc.md +++ b/docs/en/14-reference/05-connector/50-odbc.md @@ -138,7 +138,7 @@ The table below explains how the ODBC connector maps server data types to defaul | BIGINT | SQL_BIGINT | SQL_C_SBIGINT | | BIGINT UNSIGNED | SQL_BIGINT | SQL_C_UBIGINT | | FLOAT | SQL_REAL | SQL_C_FLOAT | -| DOUBLE | SQL_REAL | SQL_C_DOUBLE | +| DOUBLE | SQL_DOUBLE | SQL_C_DOUBLE | | BINARY | SQL_BINARY | SQL_C_BINARY | | SMALLINT | SQL_SMALLINT | SQL_C_SSHORT | | SMALLINT UNSIGNED | SQL_SMALLINT | SQL_C_USHORT | @@ -146,33 +146,145 @@ The table below explains how the ODBC connector maps server data types to defaul | TINYINT UNSIGNED | SQL_TINYINT | SQL_C_UTINYINT | | BOOL | SQL_BIT | SQL_C_BIT | | NCHAR | SQL_VARCHAR | SQL_C_CHAR | -| JSON | SQL_VARCHAR | SQL_C_CHAR | | VARCHAR | SQL_VARCHAR | SQL_C_CHAR | +| JSON | SQL_WVARCHAR | SQL_C_WCHAR | | GEOMETRY | SQL_VARBINARY | SQL_C_BINARY | | VARBINARY | SQL_VARBINARY | SQL_C_BINARY | ## API Reference -This section summarizes the ODBC API by functionality. For a complete ODBC API reference, please visit the [ODBC Programmer's Reference page](http://msdn.microsoft.com/en-us/library/ms714177.aspx). +### API List -### Data Source and Driver Management +- **Currently exported ODBC functions are**: + +| ODBC/Setup API | Linux | macOS | Windows | Note | +| :----- | :---- | :---- | :---- | :---- | +| ConfigDSN | ❌ | ❌ | ✅ | | +| ConfigDriver | ❌ | ❌ | ✅ | | +| ConfigTranslator | ❌ | ❌ | ❌ | | +| SQLAllocHandle | ✅ | ✅ | ✅ | | +| SQLBindCol | ✅ | ✅ | ✅ | Column-Wise Binding only | +| SQLBindParameter | ✅ | ✅ | ✅ | Column-Wise Binding only | +| SQLBrowseConnect | ❌ | ❌ | ❌ | | +| SQLBulkOperations | ❌ | ❌ | ❌ | TDengine has no counterpart | +| SQLCloseCursor | ✅ | ✅ | ✅ | | +| SQLColAttribute | ✅ | ✅ | ✅ | | +| SQLColumnPrivileges | ❌ | ❌ | ❌ | TDengine has no strict counterpart | +| SQLColumns | ✅ | ✅ | ✅ | | +| SQLCompleteAsync | ❌ | ❌ | ❌ | | +| SQLConnect | ✅ | ✅ | ✅ | | +| SQLCopyDesc | ❌ | ❌ | ❌ | | +| SQLDescribeCol | ✅ | ✅ | ✅ | | +| SQLDescribeParam | ✅ | ✅ | ✅ | | +| SQLDisconnect | ✅ | ✅ | ✅ | | +| SQLDriverConnect | ✅ | ✅ | ✅ | | +| SQLEndTran | ✅ | ✅ | ✅ | TDengine is non-transactional, thus this is at most simulating | +| SQLExecDirect | ✅ | ✅ | ✅ | | +| SQLExecute | ✅ | ✅ | ✅ | | +| SQLExtendedFetch | ❌ | ❌ | ❌ | | +| SQLFetch | ✅ | ✅ | ✅ | | +| SQLFetchScroll | ✅ | ✅ | ✅ | TDengine has no counterpart, just implement SQL_FETCH_NEXT | +| SQLForeignKeys | ❌ | ❌ | ❌ | TDengine has no counterpart | +| SQLFreeHandle | ✅ | ✅ | ✅ | | +| SQLFreeStmt | ✅ | ✅ | ✅ | | +| SQLGetConnectAttr | ✅ | ✅ | ✅ | Supports partial attributes; unsupported attributes are listed below. | +| SQLGetCursorName | ❌ | ❌ | ❌ | TDengine has no counterpart | +| SQLGetData | ✅ | ✅ | ✅ | | +| SQLGetDescField | ❌ | ❌ | ❌ | | +| SQLGetDescRec | ❌ | ❌ | ❌ | | +| SQLGetDiagField | ✅ | ✅ | ✅ | | +| SQLGetDiagRec | ✅ | ✅ | ✅ | | +| SQLGetEnvAttr | ✅ | ✅ | ✅ | | +| SQLGetInfo | ✅ | ✅ | ✅ | | +| SQLGetStmtAttr | ✅ | ✅ | ✅ | Supports partial attributes; unsupported attributes are listed below. | +| SQLGetTypeInfo | ✅ | ✅ | ✅ | | +| SQLMoreResults | ✅ | ✅ | ✅ | | +| SQLNativeSql | ❌ | ❌ | ❌ | TDengine has no counterpart | +| SQLNumParams | ✅ | ✅ | ✅ | | +| SQLNumResultCols | ✅ | ✅ | ✅ | | +| SQLParamData | ❌ | ❌ | ❌ | TDengine has no counterpart | +| SQLPrepare | ✅ | ✅ | ✅ | | +| SQLPrimaryKeys | ✅ | ✅ | ✅ | | +| SQLProcedureColumns | ❌ | ❌ | ❌ | TDengine has no counterpart | +| SQLProcedures | ❌ | ❌ | ❌ | TDengine has no counterpart | +| SQLPutData | ❌ | ❌ | ❌ | TDengine has no counterpart | +| SQLRowCount | ✅ | ✅ | ✅ | | +| SQLSetConnectAttr | ✅ | ✅ | ✅ | Supports partial attributes; unsupported attributes are listed below. | +| SQLSetCursorName | ❌ | ❌ | ❌ | TDengine has no counterpart | +| SQLSetDescField | ❌ | ❌ | ❌ | | +| SQLSetDescRec | ❌ | ❌ | ❌ | | +| SQLSetEnvAttr | ✅ | ✅ | ✅ | | +| SQLSetPos | ❌ | ❌ | ❌ | TDengine has no counterpart | +| SQLSetStmtAttr | ✅ | ✅ | ✅ | Supports partial attributes; unsupported attributes are listed below. | +| SQLSpecialColumns | ❌ | ❌ | ❌ | TDengine has no counterpart | +| SQLStatistics | ❌ | ❌ | ❌ | TDengine has no counterpart | +| SQLTablePrivileges | ❌ | ❌ | ❌ | TDengine has no strict counterpart | +| SQLTables | ✅ | ✅ | ✅ | | + +- **Non-supported-statement-attributes (SQLSetStmtAttr)** + +| Attribute | Note | +| :----- | :---- | +| SQL_ATTR_CONCURRENCY | TDengine has no updatable-CURSOR machanism | +| SQL_ATTR_FETCH_BOOKMARK_PTR | TDengine has no BOOKMARK machanism | +| SQL_ATTR_IMP_PARAM_DESC | | +| SQL_ATTR_IMP_ROW_DESC | | +| SQL_ATTR_KEYSET_SIZE | | +| SQL_ATTR_PARAM_BIND_OFFSET_PTR | | +| SQL_ATTR_PARAM_OPERATION_PTR | | +| SQL_ATTR_ROW_NUMBER | Readonly attribute | +| SQL_ATTR_ROW_OPERATION_PTR | | +| SQL_ATTR_SIMULATE_CURSOR | | + +- **Non-supported-connection-attributes (SQLSetConnectAttr)** + +| Attribute | Note | +| :----- | :---- | +| SQL_ATTR_AUTO_IPD | Readonly attribute | +| SQL_ATTR_CONNECTION_DEAD | Readonly attribute | +| SQL_ATTR_ENLIST_IN_DTC | | +| SQL_ATTR_PACKET_SIZE | | +| SQL_ATTR_TRACE | | +| SQL_ATTR_TRACEFILE | | +| SQL_ATTR_TRANSLATE_LIB | | +| SQL_ATTR_TRANSLATE_OPTION | | + +- **Enable any programming language with ODBC-bindings/ODBC-plugings to communicate with TDengine:** + +| programming language | ODBC-API or bindings/plugins | +| :----- | :---- | +| C/C++ | ODBC-API | +| CSharp | System.Data.Odbc | +| Erlang | odbc module | +| Go | [odbc](https://github.com/alexbrainman/odbc), database/sql | +| Haskell | HDBC, HDBC-odbc | +| Common Lisp | plain-odbc | +| Nodejs | odbc | +| Python3 | pyodbc | +| Rust | odbc | + +### API Functional Categories + +This section summarizes the ODBC API by functionality. For a complete ODBC API reference, please visit the [Microsoft Open Database Connectivity (ODBC)](https://learn.microsoft.com/en-us/sql/odbc/microsoft-open-database-connectivity-odbc). + +#### Data Source and Driver Management - API: ConfigDSN - - **Supported**: Yes + - **Supported**: Yes (Windows only) - **Standard**: ODBC - **Function**: Configures data sources - API: ConfigDriver - - **Supported**: Yes + - **Supported**: Yes (Windows only) - **Standard**: ODBC - **Function**: Used to perform installation and configuration tasks related to a specific driver - API: ConfigTranslator - - **Supported**: Yes + - **Supported**: No - **Standard**: ODBC - **Function**: Used to parse the DSN configuration, translating or converting between DSN configuration and actual database driver configuration -### Connecting to Data Sources +#### Connecting to Data Sources - API: SQLAllocHandle - **Supported**: Yes @@ -204,7 +316,7 @@ This section summarizes the ODBC API by functionality. For a complete ODBC API r - **Standard**: Deprecated - **Function**: In ODBC 3.x, the ODBC 2.x function SQLAllocConnect has been replaced by SQLAllocHandle -### Retrieving Information about Drivers and Data Sources +#### Retrieving Information about Drivers and Data Sources - API: SQLDataSources - **Supported**: No @@ -231,7 +343,7 @@ This section summarizes the ODBC API by functionality. For a complete ODBC API r - **Standard**: ISO 92 - **Function**: Returns information about supported data types -### Setting and Retrieving Driver Properties +#### Setting and Retrieving Driver Properties - API: SQLSetConnectAttr - **Supported**: Yes @@ -283,7 +395,7 @@ This section summarizes the ODBC API by functionality. For a complete ODBC API r - **Standard**: Deprecated - **Purpose**: In ODBC 3.x, the ODBC 2.0 function SQLSetStmtOption has been replaced by SQLGetStmtAttr -### Preparing SQL Requests +#### Preparing SQL Requests - API: SQLAllocStmt - **Supported**: Not supported @@ -320,7 +432,7 @@ This section summarizes the ODBC API by functionality. For a complete ODBC API r - **Standard**: ODBC - **Purpose**: Sets options that control cursor behavior -### Submitting Requests +#### Submitting Requests - API: SQLExecute - **Supported**: Supported @@ -357,7 +469,7 @@ This section summarizes the ODBC API by functionality. For a complete ODBC API r - **Standard**: ISO 92 - **Function**: When using stream input mode, it can be used to send data blocks to output parameters -### Retrieving Results and Information About Results +#### Retrieving Results and Information About Results - API: SQLRowCount - **Support**: Supported @@ -419,7 +531,7 @@ This section summarizes the ODBC API by functionality. For a complete ODBC API r - **Standard**: ODBC - **Function**: Performs bulk insert and bulk bookmark operations, including updates, deletions, and fetching by bookmark -### Retrieving Error or Diagnostic Information +#### Retrieving Error or Diagnostic Information - API: SQLError - **Support**: Not supported @@ -436,7 +548,7 @@ This section summarizes the ODBC API by functionality. For a complete ODBC API r - **Standard**: ISO 92 - **Function**: Returns additional diagnostic information (multiple diagnostic results) -### Retrieving Information About System Table Entries Related to the Data Source +#### Retrieving Information About System Table Entries Related to the Data Source - API: SQLColumnPrivileges - **Support**: Not supported @@ -488,7 +600,7 @@ This section summarizes the ODBC API by functionality. For a complete ODBC API r - **Standard**: ODBC - **Function**: Returns column information for stored procedures, including details of input and output parameters -### Transaction Execution +#### Transaction Execution - API: SQLTransact - **Support**: Not supported @@ -498,9 +610,9 @@ This section summarizes the ODBC API by functionality. For a complete ODBC API r - API: SQLEndTran - **Support**: Supported - **Standard**: ISO 92 - - **Function**: Used to commit or rollback transactions, TDengine does not support transactions, therefore rollback operation is not supported + - **Function**: Used to commit or rollback transactions. TDengine is non-transactional, so this function can at most simulate commit or rollback operations. If there are any outstanding connections or statements, neither commit nor rollback will succeed -### Connection Termination +#### Connection Termination - API: SQLDisconnect - **Support**: Supported diff --git a/docs/zh/14-reference/05-connector/50-odbc.mdx b/docs/zh/14-reference/05-connector/50-odbc.mdx index 4bc006fd9e..7d71b847c6 100644 --- a/docs/zh/14-reference/05-connector/50-odbc.mdx +++ b/docs/zh/14-reference/05-connector/50-odbc.mdx @@ -133,7 +133,7 @@ TDengine ODBC 支持两种连接 TDengine 数据库方式:WebSocket 连接与 | BIGINT | SQL_BIGINT | SQL_C_SBIGINT | | BIGINT UNSIGNED | SQL_BIGINT | SQL_C_UBIGINT | | FLOAT | SQL_REAL | SQL_C_FLOAT | -| DOUBLE | SQL_REAL | SQL_C_DOUBLE | +| DOUBLE | SQL_DOUBLE | SQL_C_DOUBLE | | BINARY | SQL_BINARY | SQL_C_BINARY | | SMALLINT | SQL_SMALLINT | SQL_C_SSHORT | | SMALLINT UNSIGNED | SQL_SMALLINT | SQL_C_USHORT | @@ -141,35 +141,149 @@ TDengine ODBC 支持两种连接 TDengine 数据库方式:WebSocket 连接与 | TINYINT UNSIGNED | SQL_TINYINT | SQL_C_UTINYINT | | BOOL | SQL_BIT | SQL_C_BIT | | NCHAR | SQL_VARCHAR | SQL_C_CHAR | -| JSON | SQL_VARCHAR | SQL_C_CHAR | | VARCHAR | SQL_VARCHAR | SQL_C_CHAR | +| JSON | SQL_WVARCHAR | SQL_C_WCHAR | | GEOMETRY | SQL_VARBINARY | SQL_C_BINARY | | VARBINARY | SQL_VARBINARY | SQL_C_BINARY | ## API 参考 -本节按功能分类汇总了 ODBC API,关于完整的 ODBC API 参考,请访问 http://msdn.microsoft.com/en-us/library/ms714177.aspx 的 ODBC 程序员参考页面。 +### API 列表 -### 数据源和驱动程序管理 +- **目前导出的ODBC函数有**: + +| ODBC/Setup API | Linux | macOS | Windows | Note | +| :----- | :---- | :---- | :---- | :---- | +| ConfigDSN | ❌ | ❌ | ✅ | | +| ConfigDriver | ❌ | ❌ | ✅ | | +| ConfigTranslator | ❌ | ❌ | ❌ | | +| SQLAllocHandle | ✅ | ✅ | ✅ | | +| SQLBindCol | ✅ | ✅ | ✅ | 只能按列绑定 | +| SQLBindParameter | ✅ | ✅ | ✅ | 只能按列绑定 | +| SQLBrowseConnect | ❌ | ❌ | ❌ | | +| SQLBulkOperations | ❌ | ❌ | ❌ | TDengine 没有对应的 | +| SQLCloseCursor | ✅ | ✅ | ✅ | | +| SQLColAttribute | ✅ | ✅ | ✅ | | +| SQLColumnPrivileges | ❌ | ❌ | ❌ | TDengine 没有严格对应的 | +| SQLColumns | ✅ | ✅ | ✅ | | +| SQLCompleteAsync | ❌ | ❌ | ❌ | | +| SQLConnect | ✅ | ✅ | ✅ | | +| SQLCopyDesc | ❌ | ❌ | ❌ | | +| SQLDescribeCol | ✅ | ✅ | ✅ | | +| SQLDescribeParam | ✅ | ✅ | ✅ | | +| SQLDisconnect | ✅ | ✅ | ✅ | | +| SQLDriverConnect | ✅ | ✅ | ✅ | | +| SQLEndTran | ✅ | ✅ | ✅ | TDengine是非事务性的,因此这最多是模拟 | +| SQLExecDirect | ✅ | ✅ | ✅ | | +| SQLExecute | ✅ | ✅ | ✅ | | +| SQLExtendedFetch | ❌ | ❌ | ❌ | | +| SQLFetch | ✅ | ✅ | ✅ | | +| SQLFetchScroll | ✅ | ✅ | ✅ | TDengine 没有对应的, 仅仅实现 SQL_FETCH_NEXT | +| SQLForeignKeys | ❌ | ❌ | ❌ | TDengine 没有对应的 | +| SQLFreeHandle | ✅ | ✅ | ✅ | | +| SQLFreeStmt | ✅ | ✅ | ✅ | | +| SQLGetConnectAttr | ✅ | ✅ | ✅ | 支持部分属性,下面列出了不支持的属性 | +| SQLGetCursorName | ❌ | ❌ | ❌ | TDengine 没有对应的 | +| SQLGetData | ✅ | ✅ | ✅ | | +| SQLGetDescField | ❌ | ❌ | ❌ | | +| SQLGetDescRec | ❌ | ❌ | ❌ | | +| SQLGetDiagField | ✅ | ✅ | ✅ | | +| SQLGetDiagRec | ✅ | ✅ | ✅ | | +| SQLGetEnvAttr | ✅ | ✅ | ✅ | | +| SQLGetInfo | ✅ | ✅ | ✅ | | +| SQLGetStmtAttr | ✅ | ✅ | ✅ | 支持部分属性,下面列出了不支持的属性 | +| SQLGetTypeInfo | ✅ | ✅ | ✅ | | +| SQLMoreResults | ✅ | ✅ | ✅ | | +| SQLNativeSql | ❌ | ❌ | ❌ | TDengine 没有对应的 | +| SQLNumParams | ✅ | ✅ | ✅ | | +| SQLNumResultCols | ✅ | ✅ | ✅ | | +| SQLParamData | ❌ | ❌ | ❌ | TDengine 没有对应的 | +| SQLPrepare | ✅ | ✅ | ✅ | | +| SQLPrimaryKeys | ✅ | ✅ | ✅ | | +| SQLProcedureColumns | ❌ | ❌ | ❌ | TDengine 没有对应的 | +| SQLProcedures | ❌ | ❌ | ❌ | TDengine 没有对应的 | +| SQLPutData | ❌ | ❌ | ❌ | TDengine 没有对应的 | +| SQLRowCount | ✅ | ✅ | ✅ | | +| SQLSetConnectAttr | ✅ | ✅ | ✅ | 支持部分属性,下面列出了不支持的属性 | +| SQLSetCursorName | ❌ | ❌ | ❌ | TDengine 没有对应的 | +| SQLSetDescField | ❌ | ❌ | ❌ | | +| SQLSetDescRec | ❌ | ❌ | ❌ | | +| SQLSetEnvAttr | ✅ | ✅ | ✅ | | +| SQLSetPos | ❌ | ❌ | ❌ | TDengine 没有对应的 | +| SQLSetStmtAttr | ✅ | ✅ | ✅ | 支持部分属性,下面列出了不支持的属性 | +| SQLSpecialColumns | ❌ | ❌ | ❌ | TDengine 没有对应的 | +| SQLStatistics | ❌ | ❌ | ❌ | TDengine 没有对应的 | +| SQLTablePrivileges | ❌ | ❌ | ❌ | TDengine 没有严格对应的 | +| SQLTables | ✅ | ✅ | ✅ | | + +- **不支持的语句属性 (SQLSetStmtAttr)** + +| Attribute | Note | +| :----- | :---- | +| SQL_ATTR_CONCURRENCY | TDengine 没有 updatable-CURSOR 机制 | +| SQL_ATTR_FETCH_BOOKMARK_PTR | TDengine 没有 BOOKMARK 机制 | +| SQL_ATTR_IMP_PARAM_DESC | | +| SQL_ATTR_IMP_ROW_DESC | | +| SQL_ATTR_KEYSET_SIZE | | +| SQL_ATTR_PARAM_BIND_OFFSET_PTR | | +| SQL_ATTR_PARAM_OPERATION_PTR | | +| SQL_ATTR_ROW_NUMBER | 只读属性 | +| SQL_ATTR_ROW_OPERATION_PTR | | +| SQL_ATTR_SIMULATE_CURSOR | | + +- **不支持的连接属性 (SQLSetConnectAttr)** + +| Attribute | Note | +| :----- | :---- | +| SQL_ATTR_AUTO_IPD | 只读属性 | +| SQL_ATTR_CONNECTION_DEAD | 只读属性 | +| SQL_ATTR_ENLIST_IN_DTC | | +| SQL_ATTR_PACKET_SIZE | | +| SQL_ATTR_TRACE | | +| SQL_ATTR_TRACEFILE | | +| SQL_ATTR_TRANSLATE_LIB | | +| SQL_ATTR_TRANSLATE_OPTION | | + +- **允许任何带有 ODBC 库的编程语言与 TDengine 通信:** + +| programming language | ODBC-API or bindings/plugins | +| :----- | :---- | +| C/C++ | ODBC-API | +| CSharp | System.Data.Odbc | +| Erlang | odbc module | +| Go | [odbc](https://github.com/alexbrainman/odbc), database/sql | +| Haskell | HDBC, HDBC-odbc | +| Common Lisp | plain-odbc | +| Nodejs | odbc | +| Python3 | pyodbc | +| Rust | odbc | + + + +### API 功能分类 + +本节按功能分类汇总了 ODBC API,关于完整的 ODBC API 参考,请访问 [Microsoft Open Database Connectivity (ODBC)](https://learn.microsoft.com/en-us/sql/odbc/microsoft-open-database-connectivity-odbc)。 + +#### 数据源和驱动程序管理 - API: ConfigDSN - - **是否支持**: 支持 + - **是否支持**: 支持(仅 Windows) - **标准**: ODBC - **作用**: 配置数据源 - API: ConfigDriver - - **是否支持**: 支持 + - **是否支持**: 支持(仅 Windows) - **标准**: ODBC - **作用**: 用于执行与特定驱动程序相关的安装和配置任务 - API: ConfigTranslator - - **是否支持**: 支持 + - **是否支持**: 不支持 - **标准**: ODBC - **作用**: 用于解析DSN的配置,在DSN配置和实际数据库驱动程序配置之间进行翻译或转换 -### 连接到数据源 +#### 连接到数据源 - API: SQLAllocHandle - **是否支持**: 支持 @@ -202,7 +316,7 @@ TDengine ODBC 支持两种连接 TDengine 数据库方式:WebSocket 连接与 - **作用**: 在 ODBC 3.x 中,ODBC 2.x 函数 SQLAllocConnect 已替换为 SQLAllocHandle -### 获取有关驱动程序和数据源的信息 +#### 获取有关驱动程序和数据源的信息 - API: SQLDataSources - **是否支持**: 不支持 @@ -230,7 +344,7 @@ TDengine ODBC 支持两种连接 TDengine 数据库方式:WebSocket 连接与 - **作用**: 返回有关支持的数据类型的信息 -### 设置和检索驱动程序属性 +#### 设置和检索驱动程序属性 - API: SQLSetConnectAttr - **是否支持**: 支持 @@ -283,7 +397,7 @@ TDengine ODBC 支持两种连接 TDengine 数据库方式:WebSocket 连接与 - **作用**: 在 ODBC 3.x 中,ODBC 2.0 函数 SQLSetStmtOption 已替换为 SQLGetStmtAttr -### 准备SQL请求 +#### 准备SQL请求 - API: SQLAllocStmt - **是否支持**: 不支持 @@ -321,7 +435,7 @@ TDengine ODBC 支持两种连接 TDengine 数据库方式:WebSocket 连接与 - **作用**: 设置控制光标行为的选项 -### 提交请求 +#### 提交请求 - API: SQLExecute - **是否支持**: 支持 @@ -359,7 +473,7 @@ TDengine ODBC 支持两种连接 TDengine 数据库方式:WebSocket 连接与 - **作用**: 当使用流输入方式时,可以用于向输出参数发送数据块 -### 检索结果和关于结果的信息 +#### 检索结果和关于结果的信息 - API: SQLRowCount - **是否支持**: 支持 @@ -422,7 +536,7 @@ TDengine ODBC 支持两种连接 TDengine 数据库方式:WebSocket 连接与 - **作用**: 执行批量插入和批量书签操作,包括更新、删除和按书签提取 -### 检索错误或诊断信息 +#### 检索错误或诊断信息 - API: SQLError - **是否支持**: 不支持 @@ -440,7 +554,7 @@ TDengine ODBC 支持两种连接 TDengine 数据库方式:WebSocket 连接与 - **作用**: 返回附加诊断信息(多条诊断结果) -### 获取有关数据源的系统表项的信息 +#### 获取有关数据源的系统表项的信息 - API: SQLColumnPrivileges - **是否支持**: 不支持 @@ -493,7 +607,7 @@ TDengine ODBC 支持两种连接 TDengine 数据库方式:WebSocket 连接与 - **作用**: 返回存储过程的列信息,包括输入输出参数的详细信息 -### 执行事务 +#### 执行事务 - API: SQLTransact - **是否支持**: 不支持 @@ -503,10 +617,10 @@ TDengine ODBC 支持两种连接 TDengine 数据库方式:WebSocket 连接与 - API: SQLEndTran - **是否支持**: 支持 - **标准**: ISO 92 - - **作用**: 用于提交或回滚事务,TDengine 不支持事务,因此不支持回滚操作 + - **作用**: 用于提交或回滚事务。TDengine 是非事务性的,因此 SQLEndTran 函数最多只能模拟提交或回滚操作。如果有任何未完成的连接或语句,无论是提交(commit)还是回滚(rollback)都不会成功 -### 终止连接 +#### 终止连接 - API: SQLDisconnect - **是否支持**: 支持 From abb2d20e4113ebd2699f0b4042fb67f7db671c5d Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Feb 2025 10:53:17 +0800 Subject: [PATCH 13/17] Update index.md --- docs/zh/06-advanced/06-TDgpt/04-forecast/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md b/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md index ab97883072..8f4aca7f71 100644 --- a/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md +++ b/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md @@ -4,7 +4,7 @@ description: 预测算法 --- import fc-result from '../pic/fc.png'; -import fc-result-figure '../pic/fc-result.png'; +import fc-result-figure from '../pic/fc-result.png'; 时序数据预测处理以持续一个时间段的时序数据作为输入,预测接下来一个连续时间区间内时间序列数据趋势。用户可以指定输出的(预测)时间序列数据点的数量,因此其输出的结果行数不确定。为此,TDengine 使用新 SQL 函数 `FORECAST` 提供时序数据预测服务。基础数据(用于预测的历史时间序列数据)是该函数的输入,预测结果是该函数的输出。用户可以通过 `FORECAST` 函数调用 Anode 提供的预测算法提供的服务。 From 1599cd6e1540ba0782b2a6608f9333c5fbfda552 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Feb 2025 10:54:02 +0800 Subject: [PATCH 14/17] Update index.md --- docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md b/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md index 37fc0eaded..b983f73b4b 100644 --- a/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md +++ b/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md @@ -5,7 +5,7 @@ description: 异常检测算法 import ad from '../pic/anomaly-detection.png'; import ad-result from '../pic/ad-result.png'; -import ad-result-figure '../pic/ad-result-figure.png' +import ad-result-figure from '../pic/ad-result-figure.png'; TDengine 中定义了异常(状态)窗口来提供异常检测服务。异常窗口可以视为一种特殊的**事件窗口(Event Window)**,即异常检测算法确定的连续异常时间序列数据所在的时间窗口。与普通事件窗口区别在于——时间窗口的起始时间和结束时间均是分析算法识别确定,不是用户给定的表达式进行判定。因此,在 `WHERE` 子句中使用 `ANOMALY_WINDOW` 关键词即可调用时序数据异常检测服务,同时窗口伪列(`_WSTART`, `_WEND`, `_WDURATION`)也能够像其他时间窗口一样用于描述异常窗口的起始时间(`_WSTART`)、结束时间(`_WEND`)、持续时间(`_WDURATION`)。例如: From 5f2f6bf9b92d297a1c3cdb5cdd4327ca483aa453 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Feb 2025 11:00:14 +0800 Subject: [PATCH 15/17] Update index.md --- docs/zh/06-advanced/06-TDgpt/04-forecast/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md b/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md index 8f4aca7f71..e3b59cf735 100644 --- a/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md +++ b/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md @@ -136,10 +136,10 @@ gen_figure = true 算法对比分析运行完成以后,生成 fc-results.xlsx 文件,其中包含了调用算法的预测分析误差、执行时间、调用参数等信息。如下图所示: -预测对比结果 +预测对比结果 如果设置了 `gen_figure` 为 true,分析结果中还会有绘制的分析预测结果图(如下图所示)。 -预测对比结果 +预测对比结果 From 8b870bd6914cbb5e6b62eb493800ec1e090603c3 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Feb 2025 11:12:33 +0800 Subject: [PATCH 16/17] Update index.md --- .../zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md b/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md index b983f73b4b..7eff427518 100644 --- a/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md +++ b/docs/zh/06-advanced/06-TDgpt/05-anomaly-detection/index.md @@ -4,8 +4,8 @@ description: 异常检测算法 --- import ad from '../pic/anomaly-detection.png'; -import ad-result from '../pic/ad-result.png'; -import ad-result-figure from '../pic/ad-result-figure.png'; +import ad_result from '../pic/ad-result.png'; +import ad_result_figure from '../pic/ad-result-figure.png'; TDengine 中定义了异常(状态)窗口来提供异常检测服务。异常窗口可以视为一种特殊的**事件窗口(Event Window)**,即异常检测算法确定的连续异常时间序列数据所在的时间窗口。与普通事件窗口区别在于——时间窗口的起始时间和结束时间均是分析算法识别确定,不是用户给定的表达式进行判定。因此,在 `WHERE` 子句中使用 `ANOMALY_WINDOW` 关键词即可调用时序数据异常检测服务,同时窗口伪列(`_WSTART`, `_WEND`, `_WDURATION`)也能够像其他时间窗口一样用于描述异常窗口的起始时间(`_WSTART`)、结束时间(`_WEND`)、持续时间(`_WDURATION`)。例如: @@ -98,9 +98,9 @@ lof={"algo":"auto", "n_neighbor": 3} 对比程序执行完成以后,会自动生成名称为`ad_result.xlsx` 的文件,第一个卡片是算法运行结果(如下图所示),分别包含了算法名称、执行调用参数、查全率、查准率、执行时间 5 个指标。 -异常检测对比结果 +异常检测对比结果 如果设置了 `gen_figure` 为 `true`,比较程序会自动将每个参与比较的算法分析结果采用图片方式呈现出来(如下图所示为 ksigma 的异常检测结果标注)。 -异常检测标注图 +异常检测标注图 From 6e152c9264ac54d77f571ed3b1f2dfc6d1790d17 Mon Sep 17 00:00:00 2001 From: Haojun Liao Date: Tue, 18 Feb 2025 11:13:08 +0800 Subject: [PATCH 17/17] Update index.md --- docs/zh/06-advanced/06-TDgpt/04-forecast/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md b/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md index e3b59cf735..bbfd108491 100644 --- a/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md +++ b/docs/zh/06-advanced/06-TDgpt/04-forecast/index.md @@ -3,8 +3,8 @@ title: 预测算法 description: 预测算法 --- -import fc-result from '../pic/fc.png'; -import fc-result-figure from '../pic/fc-result.png'; +import fc_result from '../pic/fc.png'; +import fc_result_figure from '../pic/fc-result.png'; 时序数据预测处理以持续一个时间段的时序数据作为输入,预测接下来一个连续时间区间内时间序列数据趋势。用户可以指定输出的(预测)时间序列数据点的数量,因此其输出的结果行数不确定。为此,TDengine 使用新 SQL 函数 `FORECAST` 提供时序数据预测服务。基础数据(用于预测的历史时间序列数据)是该函数的输入,预测结果是该函数的输出。用户可以通过 `FORECAST` 函数调用 Anode 提供的预测算法提供的服务。 @@ -136,10 +136,10 @@ gen_figure = true 算法对比分析运行完成以后,生成 fc-results.xlsx 文件,其中包含了调用算法的预测分析误差、执行时间、调用参数等信息。如下图所示: -预测对比结果 +预测对比结果 如果设置了 `gen_figure` 为 true,分析结果中还会有绘制的分析预测结果图(如下图所示)。 -预测对比结果 +预测对比结果