From 5dac2a793592b85b509de7060dc0715ac051cb28 Mon Sep 17 00:00:00 2001 From: Jake Smith Date: Thu, 31 Mar 2022 16:53:59 +0100 Subject: [PATCH 01/24] HPCC-27450 Revise index-blob-leaks changes Remove unrequired post index-normalize next/transform finishedRow calls (that clear any unloaded blobs) And remove unecessary call that was added to CRoxieServerDiskNormalizeActivity Signed-off-by: Jake Smith --- ecl/hthor/hthorkey.cpp | 7 +------ roxie/ccd/ccdactivities.cpp | 6 +++--- roxie/ccd/ccdserver.cpp | 1 - thorlcr/activities/indexread/thindexreadslave.cpp | 7 +------ 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/ecl/hthor/hthorkey.cpp b/ecl/hthor/hthorkey.cpp index f84d3cee39c..de9c2aa4ec3 100644 --- a/ecl/hthor/hthorkey.cpp +++ b/ecl/hthor/hthorkey.cpp @@ -1226,10 +1226,7 @@ const void *CHThorIndexNormalizeActivity::nextRow() { expanding = helper.next(); if (!expanding) - { - callback.finishedRow(); // next could filter break; - } const void * ret = createNextRow(); if (ret) @@ -1249,14 +1246,13 @@ const void *CHThorIndexNormalizeActivity::nextRow() agent.reportProgress(NULL); expanding = helper.first(klManager->queryKeyBuffer()); + callback.finishedRow(); // first() can lookup blobs if (expanding) { const void * ret = createNextRow(); if (ret) return ret; } - else - callback.finishedRow(); // first could filter } } } @@ -1267,7 +1263,6 @@ const void * CHThorIndexNormalizeActivity::createNextRow() { outBuilder.ensureRow(); size32_t thisSize = helper.transform(outBuilder); - callback.finishedRow(); if (thisSize == 0) { return NULL; diff --git a/roxie/ccd/ccdactivities.cpp b/roxie/ccd/ccdactivities.cpp index 2f33f830f5c..009402364da 100644 --- a/roxie/ccd/ccdactivities.cpp +++ b/roxie/ccd/ccdactivities.cpp @@ -3171,7 +3171,9 @@ class CRoxieIndexNormalizeActivity : public CRoxieIndexActivity } indexRecordsRead++; - if (normalizeHelper->first(tlk->queryKeyBuffer())) + bool firstMatch = normalizeHelper->first(tlk->queryKeyBuffer()); + callback.finishedRow(); + if (firstMatch) // first() can lookup blobs { do { @@ -3194,7 +3196,6 @@ class CRoxieIndexNormalizeActivity : public CRoxieIndexActivity totalSizeSent += rowBuilder.writeToOutput(transformedSize, true); } } while (normalizeHelper->next()); - callback.finishedRow(); if (totalSizeSent > indexReadChunkSize && !continuationFailed) { @@ -3209,7 +3210,6 @@ class CRoxieIndexNormalizeActivity : public CRoxieIndexActivity } else { - callback.finishedRow(); // first() could have filtered postFiltered++; skipped++; } diff --git a/roxie/ccd/ccdserver.cpp b/roxie/ccd/ccdserver.cpp index 2c82c6b4773..18318e0c35d 100644 --- a/roxie/ccd/ccdserver.cpp +++ b/roxie/ccd/ccdserver.cpp @@ -22499,7 +22499,6 @@ class CRoxieServerDiskNormalizeActivity : public CRoxieServerDiskReadBaseActivit } transformedSize = normalizeHelper->transform(rowBuilder); firstPending = !normalizeHelper->next(); - reader->finishedRow(); if (transformedSize) break; } diff --git a/thorlcr/activities/indexread/thindexreadslave.cpp b/thorlcr/activities/indexread/thindexreadslave.cpp index e7a3c3c64b6..012900113b5 100644 --- a/thorlcr/activities/indexread/thindexreadslave.cpp +++ b/thorlcr/activities/indexread/thindexreadslave.cpp @@ -1393,7 +1393,6 @@ class CIndexNormalizeSlaveActivity : public CIndexReadSlaveBase { RtlDynamicRowBuilder row(allocator); size32_t sz = helper->transform(row); - callback.finishedRow(); if (sz==0) return NULL; if (getDataLinkCount() >= rowLimit) @@ -1478,10 +1477,7 @@ class CIndexNormalizeSlaveActivity : public CIndexReadSlaveBase { expanding = helper->next(); if (!expanding) - { - callback.finishedRow(); // next() could filter? break; - } OwnedConstThorRow row = createNextRow(); if (row) @@ -1496,6 +1492,7 @@ class CIndexNormalizeSlaveActivity : public CIndexReadSlaveBase { ++progress; expanding = helper->first(rec); + callback.finishedRow(); // first() can lookup blobs if (expanding) { OwnedConstThorRow row = createNextRow(); @@ -1503,8 +1500,6 @@ class CIndexNormalizeSlaveActivity : public CIndexReadSlaveBase return row.getClear(); break; } - else - callback.finishedRow(); // first() could filter? } else { From e2ac7b940ac3248f1bde2e01bebbd0650d421bd8 Mon Sep 17 00:00:00 2001 From: Anthony Fishbeck Date: Wed, 13 Apr 2022 19:50:20 -0400 Subject: [PATCH 02/24] HPCC-25925 Provide access to user secrets from ECL code utf8 passphrase := getsecret('mysecret', 'mypassphrase'); data binkey := getsecret('mysecret', 'binary_key'); Signed-off-by: Anthony Fishbeck --- .../ContainerizedMods/ConfigureValues.xml | 16 +++--- ecl/hql/hqlattr.cpp | 3 +- ecl/hql/hqlexpr.cpp | 4 +- ecl/hql/hqlexpr.hpp | 2 +- ecl/hql/hqlgram.y | 7 +++ ecl/hql/hqlgram2.cpp | 3 +- ecl/hql/hqlir.cpp | 2 +- ecl/hql/hqllex.l | 1 + ecl/hql/reservedwords.cpp | 1 + ecl/hqlcpp/hqlcatom.cpp | 2 + ecl/hqlcpp/hqlcatom.hpp | 1 + ecl/hqlcpp/hqlcpp.cpp | 19 +++++++ ecl/hqlcpp/hqlcpp.ipp | 1 + ecl/hqlcpp/hqlcppsys.ecl | 1 + helm/examples/secrets/README.md | 53 +++++++++++++++++-- helm/examples/secrets/crypt.key | 15 ++++++ helm/examples/secrets/crypto_secret.ecl | 47 ++++++++++++++++ helm/examples/secrets/hpcc_vault_policies.hcl | 2 +- helm/examples/secrets/values-secrets.yaml | 8 +++ helm/hpcc/templates/eclagent.yaml | 2 +- helm/hpcc/templates/localroxie.yaml | 2 +- helm/hpcc/templates/roxie.yaml | 2 +- helm/hpcc/templates/thor.yaml | 2 +- helm/hpcc/values.schema.json | 5 +- helm/hpcc/values.yaml | 17 ++++-- rtl/eclrtl/eclrtl.cpp | 17 ++++++ rtl/eclrtl/eclrtl.hpp | 2 + 27 files changed, 212 insertions(+), 25 deletions(-) create mode 100644 helm/examples/secrets/crypt.key create mode 100644 helm/examples/secrets/crypto_secret.ecl diff --git a/docs/EN_US/ContainerizedHPCC/ContainerizedMods/ConfigureValues.xml b/docs/EN_US/ContainerizedHPCC/ContainerizedMods/ConfigureValues.xml index c01821f8e2d..2532da629ca 100644 --- a/docs/EN_US/ContainerizedHPCC/ContainerizedMods/ConfigureValues.xml +++ b/docs/EN_US/ContainerizedHPCC/ContainerizedMods/ConfigureValues.xml @@ -884,7 +884,10 @@ into the system if you don't want it in the source. Such as code with embedded code, you can have that defined in the code sign sections. If you have information that you don't want public but need to run it you - could use secrets. + could use secrets. There is a category named "eclUser" which is where + you would put secrets that you want to be readable directly from ECL code. + Other secret categories, including the "ecl" category, are read internally + by system components and not exposed directly to ECL code. @@ -892,11 +895,12 @@ Vaults is another way to do Secrets. The vaults section mirrors the secret section but leverages HashiCorp Vault - for the storage of secrets. There is an additional category for vaults - named "ecl-user". The intent of the ecl-user vault secrets is to be - readable directly from ECL code. Other secret categories are read - internally by system components and not exposed directly to ECL - code. + for the storage of secrets. There is a category for vaults named "eclUser". + The intent of the eclUser vault category is to be readable directly from ECL + code. Only add vault configurations to the "eclUser" category that you want + ECL users to be able to access. Other vault categories, including the "ecl" + category, are read internally by system components and not exposed directly + to ECL code. diff --git a/ecl/hql/hqlattr.cpp b/ecl/hql/hqlattr.cpp index 7c48a08bade..af0e32c7e6a 100644 --- a/ecl/hql/hqlattr.cpp +++ b/ecl/hql/hqlattr.cpp @@ -151,6 +151,7 @@ unsigned getOperatorMetaFlags(node_operator op) case no_pure: case no_sequence: case no_getenv: + case no_getsecret: //Selection operators - could be arithmetic, string, dataset etc. case no_map: @@ -624,7 +625,7 @@ unsigned getOperatorMetaFlags(node_operator op) case no_unused34: case no_unused35: case no_unused36: case no_unused37: case no_unused38: case no_unused40: case no_unused41: case no_unused42: case no_unused43: case no_unused44: case no_unused45: case no_unused46: case no_unused47: case no_unused48: case no_unused49: case no_unused50: case no_unused52: - case no_unused80: case no_unused81: + case no_unused80: case no_unused102: case no_is_null: case no_position: diff --git a/ecl/hql/hqlexpr.cpp b/ecl/hql/hqlexpr.cpp index 2fc1f715ea1..3bfc3adbcf0 100644 --- a/ecl/hql/hqlexpr.cpp +++ b/ecl/hql/hqlexpr.cpp @@ -2002,6 +2002,7 @@ const char *getOpString(node_operator op) case no_selectindirect: return ".<>"; case no_isomitted: return "no_isomitted"; case no_getenv: return "GETENV"; + case no_getsecret: return "GETSECRET"; case no_once: return "ONCE"; case no_persist_check: return "no_persist_check"; case no_create_initializer: return "no_create_initializer"; @@ -2026,7 +2027,7 @@ const char *getOpString(node_operator op) case no_unused34: case no_unused35: case no_unused36: case no_unused37: case no_unused38: case no_unused40: case no_unused41: case no_unused42: case no_unused43: case no_unused44: case no_unused45: case no_unused46: case no_unused47: case no_unused48: case no_unused49: case no_unused50: case no_unused52: - case no_unused80: case no_unused81: + case no_unused80: case no_unused102: return "unused"; /* if fail, use "hqltest -internal" to find out why. */ @@ -4226,6 +4227,7 @@ void CHqlRealExpression::initFlagsBeforeOperands() case no_sectioninput: case no_wuid: case no_getenv: + case no_getsecret: infoFlags2 &= ~HEF2constant; break; case no_counter: diff --git a/ecl/hql/hqlexpr.hpp b/ecl/hql/hqlexpr.hpp index 25295ea8596..f41a632831d 100644 --- a/ecl/hql/hqlexpr.hpp +++ b/ecl/hql/hqlexpr.hpp @@ -711,7 +711,7 @@ enum node_operator : unsigned short { no_sectioninput, no_forcegraph, no_eventextra, - no_unused81, + no_getsecret, no_related, no_executewhen, no_definesideeffect, diff --git a/ecl/hql/hqlgram.y b/ecl/hql/hqlgram.y index 8b8bb8c265d..781b6c9afd6 100644 --- a/ecl/hql/hqlgram.y +++ b/ecl/hql/hqlgram.y @@ -237,6 +237,7 @@ static void eclsyntaxerror(HqlGram * parser, const char * s, short yystate, int FULL FUNCTION GETENV + GETSECRET GLOBAL GRAPH GROUP @@ -7145,6 +7146,12 @@ primexpr1 parser->normalizeExpression($5, type_stringorunicode, false); $$.setExpr(createValue(no_getenv, makeVarStringType(UNKNOWN_LENGTH), $3.getExpr(), $5.getExpr()), $1); } + | GETSECRET '(' expression ',' expression ')' + { + parser->normalizeExpression($3, type_stringorunicode, false); + parser->normalizeExpression($5, type_stringorunicode, false); + $$.setExpr(createValue(no_getsecret, makeDataType(UNKNOWN_LENGTH), $3.getExpr(), $5.getExpr()), $1); + } | __STAND_ALONE__ { $$.setExpr(createValue(no_debug_option_value, makeBoolType(), createConstant("standAloneExe"))); diff --git a/ecl/hql/hqlgram2.cpp b/ecl/hql/hqlgram2.cpp index b4c7bc94988..8c5f3314eab 100644 --- a/ecl/hql/hqlgram2.cpp +++ b/ecl/hql/hqlgram2.cpp @@ -11445,6 +11445,7 @@ static void getTokenText(StringBuffer & msg, int token) case FULL: msg.append("FULL"); break; case FUNCTION: msg.append("FULL"); break; case GETENV: msg.append("GETENV"); break; + case GETSECRET: msg.append("GETSECRET"); break; case GLOBAL: msg.append("GLOBAL"); break; case GRAPH: msg.append("GRAPH"); break; case GROUP: msg.append("GROUP"); break; @@ -11851,7 +11852,7 @@ void HqlGram::simplifyExpected(int *expected) GROUP, GROUPED, KEYED, UNGROUP, JOIN, PULL, ROLLUP, ITERATE, PROJECT, NORMALIZE, PIPE, DENORMALIZE, CASE, MAP, HTTPCALL, SOAPCALL, LIMIT, PARSE, FAIL, MERGE, PRELOAD, ROW, TOPN, ALIAS, LOCAL, NOFOLD, NOCOMBINE, NOHOIST, NOTHOR, IF, GLOBAL, __COMMON__, __COMPOUND__, TOK_ASSERT, _EMPTY_, COMBINE, ROWS, REGROUP, XMLPROJECT, SKIP, LOOP, CLUSTER, NOLOCAL, REMOTE, PROCESS, ALLNODES, THISNODE, GRAPH, MERGEJOIN, STEPPED, NONEMPTY, HAVING, - TOK_CATCH, '@', SECTION, WHEN, IFF, COGROUP, HINT, INDEX, PARTITION, AGGREGATE, SUBSORT, TOK_ERROR, CHOOSE, TRACE, QUANTILE, UNORDERED, 0); + TOK_CATCH, '@', SECTION, WHEN, IFF, COGROUP, HINT, INDEX, PARTITION, AGGREGATE, SUBSORT, TOK_ERROR, CHOOSE, TRACE, QUANTILE, UNORDERED, GETSECRET, 0); simplify(expected, EXP, ABS, SIN, COS, TAN, SINH, COSH, TANH, ACOS, ASIN, ATAN, ATAN2, COUNT, CHOOSE, MAP, CASE, IF, HASH, HASH32, HASH64, HASHMD5, CRC, LN, TOK_LOG, POWER, RANDOM, ROUND, ROUNDUP, SQRT, TRUNCATE, LENGTH, TRIM, INTFORMAT, REALFORMAT, ASSTRING, TRANSFER, MAX, MIN, EVALUATE, SUM, diff --git a/ecl/hql/hqlir.cpp b/ecl/hql/hqlir.cpp index 970eff1c281..27e696fe6df 100644 --- a/ecl/hql/hqlir.cpp +++ b/ecl/hql/hqlir.cpp @@ -644,7 +644,7 @@ const char * getOperatorIRText(node_operator op) EXPAND_CASE(no,sectioninput); EXPAND_CASE(no,forcegraph); EXPAND_CASE(no,eventextra); - EXPAND_CASE(no,unused81); + EXPAND_CASE(no,getsecret); EXPAND_CASE(no,related); EXPAND_CASE(no,executewhen); EXPAND_CASE(no,definesideeffect); diff --git a/ecl/hql/hqllex.l b/ecl/hql/hqllex.l index 89e4714732c..991851992c3 100644 --- a/ecl/hql/hqllex.l +++ b/ecl/hql/hqllex.l @@ -760,6 +760,7 @@ FROMXML { RETURNSYM(FROMXML); } FULL { RETURNSYM(FULL); } FUNCTION { RETURNHARD(FUNCTION); } GETENV { RETURNSYM(GETENV); } +GETSECRET { RETURNSYM(GETSECRET); } GLOBAL { RETURNSYM(GLOBAL); } GRAPH { RETURNSYM(GRAPH); } GROUP { RETURNSYM(GROUP); } diff --git a/ecl/hql/reservedwords.cpp b/ecl/hql/reservedwords.cpp index dbd8b104e86..eec18178e10 100644 --- a/ecl/hql/reservedwords.cpp +++ b/ecl/hql/reservedwords.cpp @@ -67,6 +67,7 @@ static const char * eclReserved2[] = { //HPCC and OS environment settings "clustersize", "getenv", + "getsecret", NULL }; diff --git a/ecl/hqlcpp/hqlcatom.cpp b/ecl/hqlcpp/hqlcatom.cpp index 7b48bdfb8bd..7281cc68287 100644 --- a/ecl/hqlcpp/hqlcatom.cpp +++ b/ecl/hqlcpp/hqlcatom.cpp @@ -340,6 +340,7 @@ IIdAtom * getClusterSizeId; IIdAtom * getDatasetHashId; IIdAtom * getECLId; IIdAtom * getEnvId; +IIdAtom * getSecretId; IIdAtom * getEventExtraId; IIdAtom * getEventNameId; IIdAtom * getExpandLogicalNameId; @@ -1005,6 +1006,7 @@ MODULE_INIT(INIT_PRIORITY_HQLATOM-1) MAKEID(getDatasetHash); MAKEID(getECL); MAKEID(getEnv); + MAKEID(getSecret); MAKEID(getEventExtra); MAKEID(getEventName); MAKEID(getExpandLogicalName); diff --git a/ecl/hqlcpp/hqlcatom.hpp b/ecl/hqlcpp/hqlcatom.hpp index 35add4d7a35..d0087945299 100644 --- a/ecl/hqlcpp/hqlcatom.hpp +++ b/ecl/hqlcpp/hqlcatom.hpp @@ -338,6 +338,7 @@ extern IIdAtom * getClusterSizeId; extern IIdAtom * getDatasetHashId; extern IIdAtom * getECLId; extern IIdAtom * getEnvId; +extern IIdAtom * getSecretId; extern IIdAtom * getEventExtraId; extern IIdAtom * getEventNameId; extern IIdAtom * getExpandLogicalNameId; diff --git a/ecl/hqlcpp/hqlcpp.cpp b/ecl/hqlcpp/hqlcpp.cpp index 1b75f8a992e..4b5d836755b 100644 --- a/ecl/hqlcpp/hqlcpp.cpp +++ b/ecl/hqlcpp/hqlcpp.cpp @@ -2751,6 +2751,12 @@ void HqlCppTranslator::buildExprAssign(BuildCtx & ctx, const CHqlBoundTarget & t buildExprAssign(ctx, target, mapped); break; } + case no_getsecret: + { + OwnedHqlExpr mapped = cvtGetSecretToCall(expr); + buildExprAssign(ctx, target, mapped); + break; + } default: doBuildExprAssign(ctx, target, expr); break; @@ -3408,6 +3414,12 @@ void HqlCppTranslator::buildExpr(BuildCtx & ctx, IHqlExpression * expr, CHqlBoun buildExpr(ctx, mapped, tgt); return; } + case no_getsecret: + { + OwnedHqlExpr mapped = cvtGetSecretToCall(expr); + buildExpr(ctx, mapped, tgt); + return; + } case no_notnot: { OwnedHqlExpr castChild = ensureExprType(expr->queryChild(0), queryBoolType()); @@ -10576,6 +10588,13 @@ IHqlExpression * HqlCppTranslator::cvtGetEnvToCall(IHqlExpression * expr) return bindFunctionCall(getEnvId, args); } +IHqlExpression * HqlCppTranslator::cvtGetSecretToCall(IHqlExpression * expr) +{ + HqlExprArray args; + args.append(*LINK(expr->queryChild(0))); + args.append(*LINK(expr->queryChild(1))); + return bindFunctionCall(getSecretId, args); +} //--------------------------------------------------------------------------- void HqlCppTranslator::doBuildAssignToFromUnicode(BuildCtx & ctx, const CHqlBoundTarget & target, IHqlExpression * expr) diff --git a/ecl/hqlcpp/hqlcpp.ipp b/ecl/hqlcpp/hqlcpp.ipp index 344583ff039..5ddea279d32 100644 --- a/ecl/hqlcpp/hqlcpp.ipp +++ b/ecl/hqlcpp/hqlcpp.ipp @@ -1476,6 +1476,7 @@ public: void buildWorkflowPersistCheck(BuildCtx & ctx, IHqlExpression * expr); IHqlExpression * cvtGetEnvToCall(IHqlExpression * expr); + IHqlExpression * cvtGetSecretToCall(IHqlExpression * expr); //Statements void doBuildStmtApply(BuildCtx & ctx, IHqlExpression * expr); diff --git a/ecl/hqlcpp/hqlcppsys.ecl b/ecl/hqlcpp/hqlcppsys.ecl index d0608f82c59..21baf798363 100644 --- a/ecl/hqlcpp/hqlcppsys.ecl +++ b/ecl/hqlcpp/hqlcppsys.ecl @@ -617,6 +617,7 @@ const char * cppSystemText[] = { //MORE: Should this be utf8? " varstring getenv(const varstring name, const varstring defaultValue) : pure,ctxmethod,entrypoint='getEnv';", + " data getsecret(const varstring name, const varstring key) : eclrtl,pure,library='eclrtl',entrypoint='rtlGetEclUserSecret';", " integer4 queryFailCode() : gctxmethod,entrypoint='queryLastFailCode';", " string getFailMessage(const varstring tag) : gctxmethod,entrypoint='getLastFailMessage';", diff --git a/helm/examples/secrets/README.md b/helm/examples/secrets/README.md index 820e0a16b84..a5980679fbd 100644 --- a/helm/examples/secrets/README.md +++ b/helm/examples/secrets/README.md @@ -104,6 +104,26 @@ vault write auth/kubernetes/role/hpcc-vault-access \ ttl=24h ``` +## 'eclUser' category secrets + +Create example vault 'eclUser' secrets: + +```bash +vault kv put secret/eclUser/vault-example crypt.key=@examples/secrets/crypt.key +``` + +Create example kubernetes secret: + +```bash +kubectl create secret generic k8s-example --from-file=crypt.key=examples/secrets/crypt.key +``` + + +## 'ecl' category secrets + +Secrets in the 'ecl' category are not accessible by ECL code directly and therefore not visible to ECL users. They can be used by internal ECL feartures +and commands. For example: + ## HTTP-CONNECT Secrets: This example focuses on ECL secrets to provide HTTP connection strings and credentials for ECL SOAPCALL and HTTPCALL commands. @@ -118,7 +138,7 @@ Besides the URL values can currently be set for proxy (trusted for keeping these ## Create example vault secret: -Create example vault secrets: +Create example vault 'ecl' secrets: ```bash vault kv put secret/ecl/http-connect-vaultsecret url=@examples/secrets/url-basic username=@examples/secrets/username password=@examples/secrets/password @@ -136,7 +156,7 @@ Create example kubernetes secret: kubectl create secret generic http-connect-basicsecret --from-file=url=examples/secrets/url-basic --from-file=examples/secrets/username --from-file=examples/secrets/password ``` -## Installing the HPCC with the HTTP-CONNECT Secrets added to ECL components +## Installing the HPCC with the secrets added to ECL components Install the HPCC helm chart with the secrets just defined added to all components that run ECL. @@ -149,8 +169,35 @@ Use kubectl to check the status of the deployed pods. Wait until all pods are r ```bash kubectl get pods ``` +-------------------------------------------------------------------------------------------------------- + +If you don't already have the HPCC client tools installed please install them now: + +https://hpccsystems.com/download#HPCC-Platform + + +## Using the created 'eclUser' category secrets directly in ECL code + +The following ecl commands will run the three example ECL files on hthor. + +```bash +ecl run hthor examples/secrets/crypto_secret.ecl +``` + +The expected result would be: + +```xml + + + top secret + + + For your eyes only + + +``` -## Using the created secrets via HTTPCALL from within ECL code +## Using the created 'ecl' category secrets via HTTPCALL from within ECL code If you don't already have the HPCC client tools installed please install them now: diff --git a/helm/examples/secrets/crypt.key b/helm/examples/secrets/crypt.key new file mode 100644 index 00000000000..e377be55c0c --- /dev/null +++ b/helm/examples/secrets/crypt.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQCWnKkGM0l3Y6pKhxMq87hAGBL6FfEo2HC6XCSQuaAMLkdf7Yjn +3FpvFIEO6A1ZYJy70cT8+HOFta+sSUyMn2fDc5cvVdX8v7XCycYXEBeZ4KsTCHHP +CUoO/nxNbxhNz09T8dx/JsIH50LHipR6FTLTSCXRN9KVLaPXs5DdQx6PjQIDAQAB +AoGBAI3iOY0AxcX2GxeolhMRlFK1GzODdjN/avr1EPFSHYc8Fbs1/5JF0N/yFf6f +9utrC1wYqpWRzOp6lWsdhkK3bLyO8Yv9BHaraTdx+bWFRloAmkK5eBNSNtpM8HSN +geEMsuFb5qgA10lUluaNSVJzgXJQbK6HCrpkKpPXGzTflQPhAkEAxTRv9l0/zgEW +wpCc5oPFaYw0tLeSRb1ogZIGQRPuoObMlN2uSeAZ0CtFHm3J5GUbm7KlkDHwL2lg +XJjrW6146QJBAMOEC0wDskrX4FKnUjPpOcfhcOgRpFzXFxDiMcrV3/syAaCkH9F0 +IE3/qIUFtDrjZWQqB+R55d6VH/yqh7SBuwUCQGEchnoqx23ZyWx8rFcz0rY8TgQk +Vgqz0E/mKeBIQX3IyjwQwnAsxGlntXzbkc1AIQ1WNwPAI8glO0e+IkCeN2kCQQCK +R0ZyIZ8kvd+CtaI24rmh+3kOOQQFQX6ny0KqEW/TSj/KbKmwSrBaWfnG8wzQJWnd +WLiyR+Bi9xdjbPyDlsk9AkEAj0dX02SY3zdnA9CoxB2FKuRL4LQ8A9Sjw4FbD6TM +dUgVydJ3WMPY1AQCZ+/5iHXKUIbGeEq/vBJKymJTKrz88w== +-----END RSA PRIVATE KEY----- diff --git a/helm/examples/secrets/crypto_secret.ecl b/helm/examples/secrets/crypto_secret.ecl new file mode 100644 index 00000000000..f9f7783f64b --- /dev/null +++ b/helm/examples/secrets/crypto_secret.ecl @@ -0,0 +1,47 @@ +/*############################################################################## + + HPCC SYSTEMS software Copyright (C) 2022 HPCC Systems®. + + Licensed under the Apache License, Version 2.0 (the 'License'); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an 'AS IS' BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +############################################################################## */ +import Std; + +STRING pubKey := '-----BEGIN PUBLIC KEY-----' + '\n' + +'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCWnKkGM0l3Y6pKhxMq87hAGBL6' + '\n' + +'FfEo2HC6XCSQuaAMLkdf7Yjn3FpvFIEO6A1ZYJy70cT8+HOFta+sSUyMn2fDc5cv' + '\n' + +'VdX8v7XCycYXEBeZ4KsTCHHPCUoO/nxNbxhNz09T8dx/JsIH50LHipR6FTLTSCXR' + '\n' + +'N9KVLaPXs5DdQx6PjQIDAQAB' + '\n' + +'-----END PUBLIC KEY-----' + '\n'; + +//-------------- +//K8S Example +//-------------- + +DATA k8sData := x'5C62E1843162330ED7BDAB7F37E50F892A669B54B8A466ED421F14954AA0505BA9EADAC4DA1D1FB1FD53EBDCF729D1049F893B3EE53ECCE48813A546CF58EBBB26EF5B9247002F7A8D1F90C3C372544501A126CEFC4B385BF540931FC0224D4736E4E1E4CF0C67D035063900887C240C8C4F365F0186ED0515E98B23C75E482A'; +VARSTRING k8sKey := (VARSTRING) getSecret('k8s-example', 'crypt.key'); +k8sEncModule := Std.Crypto.PKEncryptionFromBuffer('RSA', pubKey, k8sKey); + +output( (STRING)k8sEncModule.Decrypt(k8sData), named('k8s_message')); + + + +//-------------- +//Vault Example +//-------------- + +DATA vaultData := x'227ACC7749A442CFBA6404AD59304DD608E3D1544B293221FA0A9E44AAAD272A3EEFF15387ABB54F1F375D35C034BB03623A5100942764356046DDDBA9963F7DDF1B7FED431769815F02BA2FDB4D1ECE7E5835FA392AB7FE5292979F80B469A062750F79039633CA60EDE01D292ED4B364C1BA7E8F1301F5DB33883872945A70'; +VARSTRING vaultKey := (VARSTRING) getSecret('vault-example', 'crypt.key'); + +vaultEncModule := Std.Crypto.PKEncryptionFromBuffer('RSA', pubKey, vaultKey); + +output( (STRING)vaultEncModule.Decrypt(vaultData), named('vault_message')); diff --git a/helm/examples/secrets/hpcc_vault_policies.hcl b/helm/examples/secrets/hpcc_vault_policies.hcl index bba29b0a4d4..a95b2f83256 100644 --- a/helm/examples/secrets/hpcc_vault_policies.hcl +++ b/helm/examples/secrets/hpcc_vault_policies.hcl @@ -2,7 +2,7 @@ path "secret/data/ecl/*" { capabilities = ["read"] } -path "secret/data/ecl-user/*" { +path "secret/data/eclUser/*" { capabilities = ["read"] } diff --git a/helm/examples/secrets/values-secrets.yaml b/helm/examples/secrets/values-secrets.yaml index 63032ec4f04..48fc9c179a4 100644 --- a/helm/examples/secrets/values-secrets.yaml +++ b/helm/examples/secrets/values-secrets.yaml @@ -4,6 +4,8 @@ secrets: ecl: http-connect-basicsecret: "http-connect-basicsecret" + eclUser: + k8s-example: "k8s-example" vaults: storage: @@ -17,6 +19,12 @@ vaults: #Note the data node in the URL is there for the REST APIs use. The path inside the vault starts after /data url: http://${env.VAULT_SERVICE_HOST}:${env.VAULT_SERVICE_PORT}/v1/secret/data/ecl/${secret} kind: kv-v2 + eclUser: + #vault using kubernetes auth + - name: my-eclUser-vault + #Note the data node in the URL is there for the REST APIs use. The path inside the vault starts after /data + url: http://${env.VAULT_SERVICE_HOST}:${env.VAULT_SERVICE_PORT}/v1/secret/data/eclUser/${secret} + kind: kv-v2 authn: - name: my-authn-vault #Note the data node in the URL is there for the REST APIs use. The path inside the vault starts after /data diff --git a/helm/hpcc/templates/eclagent.yaml b/helm/hpcc/templates/eclagent.yaml index c5120beb048..61f617fda48 100644 --- a/helm/hpcc/templates/eclagent.yaml +++ b/helm/hpcc/templates/eclagent.yaml @@ -112,7 +112,7 @@ data: {{- if not .disabled -}} {{- $env := concat ($.Values.global.env | default list) (.env | default list) -}} {{- $apptype := .type | default "hthor" -}} -{{- $secretsCategories := list "system" "ecl-user" "ecl" "storage" }} +{{- $secretsCategories := list "system" "eclUser" "ecl" "storage" }} {{- $commonCtx := dict "root" $ "me" . "secretsCategories" $secretsCategories "includeCategories" (list "lz" "data" "remote" "spill" "dll" "debug") "env" $env }} {{- $configSHA := include "hpcc.getConfigSHA" ($commonCtx | merge (dict "configMapHelper" "hpcc.agentConfigMap" "component" "eclagent" "excludeKeys" (print "global," $apptype ".replicas"))) }} {{- include "hpcc.checkDefaultStoragePlane" $commonCtx }} diff --git a/helm/hpcc/templates/localroxie.yaml b/helm/hpcc/templates/localroxie.yaml index d8e912b74ca..6d0a299859d 100644 --- a/helm/hpcc/templates/localroxie.yaml +++ b/helm/hpcc/templates/localroxie.yaml @@ -43,7 +43,7 @@ data: {{ range $roxie := $.Values.roxie -}} {{- if not $roxie.disabled -}} {{- $env := concat ($.Values.global.env | default list) (.env | default list) -}} -{{- $secretsCategories := list "system" "ecl-user" "ecl" "storage" }} +{{- $secretsCategories := list "system" "eclUser" "ecl" "storage" }} {{- $commonCtx := dict "root" $ "me" $roxie "includeCategories" (list "lz" "data" "remote" "spill" "dll" "debug") "secretsCategories" $secretsCategories "env" $env }} {{- $configSHA := include "hpcc.getConfigSHA" ($commonCtx | merge (dict "configMapHelper" "hpcc.localroxieConfigMap" "component" "roxie" "excludeKeys" "global")) }} {{- include "hpcc.checkDefaultStoragePlane" $commonCtx }} diff --git a/helm/hpcc/templates/roxie.yaml b/helm/hpcc/templates/roxie.yaml index 159d82112bc..72c69c08292 100644 --- a/helm/hpcc/templates/roxie.yaml +++ b/helm/hpcc/templates/roxie.yaml @@ -80,7 +80,7 @@ data: {{ range $roxie := $.Values.roxie -}} {{- if not $roxie.disabled -}} {{- $env := concat ($.Values.global.env | default list) (.env | default list) -}} -{{- $secretsCategories := list "system" "ecl-user" "ecl" "storage" }} +{{- $secretsCategories := list "system" "eclUser" "ecl" "storage" }} {{- $toposerver := ($roxie.topoServer | default dict) -}} {{- $commonCtx := dict "root" $ "me" $roxie "includeCategories" (list "lz" "data" "remote" "spill" "dll" "debug") "secretsCategories" $secretsCategories "toposerver" $toposerver "env" $env }} {{- $_ := set $commonCtx "toponame" (printf "%s-toposerver" $roxie.name) -}} diff --git a/helm/hpcc/templates/thor.yaml b/helm/hpcc/templates/thor.yaml index 2a1c5a24d95..ea2dffbc7c0 100644 --- a/helm/hpcc/templates/thor.yaml +++ b/helm/hpcc/templates/thor.yaml @@ -294,7 +294,7 @@ data: {{ range $.Values.thor -}} {{- if not .disabled -}} {{- $env := concat ($.Values.global.env | default list) (.env | default list) -}} -{{- $secretsCategories := list "system" "ecl-user" "ecl" "storage" }} +{{- $secretsCategories := list "system" "eclUser" "ecl" "storage" }} {{- $commonCtx := dict "root" $ "me" . "includeCategories" (list "lz" "data" "remote" "spill" "dll" "debug") "secretsCategories" $secretsCategories "env" $env -}} {{- $_ := set $commonCtx "eclAgentName" (printf "%s-eclagent" .name) -}} {{- $_ := set $commonCtx "thorAgentName" (printf "%s-thoragent" .name) -}} diff --git a/helm/hpcc/values.schema.json b/helm/hpcc/values.schema.json index acda02a57b0..4e3cdf9c5d9 100644 --- a/helm/hpcc/values.schema.json +++ b/helm/hpcc/values.schema.json @@ -74,6 +74,9 @@ "ecl": { "$ref": "#/definitions/secrets" }, + "eclUser": { + "$ref": "#/definitions/secrets" + }, "codeSign": { "$ref": "#/definitions/secrets" }, @@ -108,7 +111,7 @@ "ecl": { "$ref": "#/definitions/vaultCategory" }, - "ecl-user": { + "eclUser": { "$ref": "#/definitions/vaultCategory" }, "codeSign": { diff --git a/helm/hpcc/values.yaml b/helm/hpcc/values.yaml index 70d12700405..169239ad51c 100644 --- a/helm/hpcc/values.yaml +++ b/helm/hpcc/values.yaml @@ -287,7 +287,13 @@ secrets: #ldapadmincredskey: "admincredssecretname" ## Default k/v for LDAP authentication secrets ecl: {} - ## Category for secrets published to all components that run ecl + ## Category for secrets published to all components that run ecl. These secrets are for use by internal + ## ECL processing. For example HTTPCALL and SOAPCALL have built in support for secrets that are not directly + ## accessible to users, that is, accessed directly via ECL code. + + eclUser: {} + ## Category for secrets accessible via ecl code. These are secrets that users can access directly. Be cautious about + ## what secrets you add to this category as they are easily accessed by ECL code. codeSign: {} #gpg-private-key-1: codesign-gpg-key-1 @@ -304,8 +310,8 @@ secrets: ## Category to provide passwords for eclccserver to access private git repos ## The vaults section mirrors the secret section but leverages vault for the storage of secrets. -## There is an additional category for vaults named "ecl-user". In the future "ecl-user" vault -## secrets will be readable directly from ECL code. Other secret categories are read internally +## There is an additional category for vaults named "eclUser". "eclUser" vault +## secrets are readable directly from ECL code. Other secret categories are read internally ## by system components and not exposed directly to ECL code. ## ## For each vault: @@ -322,8 +328,9 @@ vaults: ecl: - ecl-user: - #ECL code will have direct access to these secrets + eclUser: + ## Category for vaults accessible via ecl code. These are vaults that users can access directly. Be cautious + ## about what vaults you add to this category as they are easily accessed by ECL code. esp: diff --git a/rtl/eclrtl/eclrtl.cpp b/rtl/eclrtl/eclrtl.cpp index 66d9a1eac7b..5be15072a09 100644 --- a/rtl/eclrtl/eclrtl.cpp +++ b/rtl/eclrtl/eclrtl.cpp @@ -25,6 +25,7 @@ #include "jlib.hpp" #include "jptree.hpp" #include "junicode.hpp" +#include "jsecrets.hpp" #include "eclrtl.hpp" #include "rtlbcd.hpp" #include "eclhelper.hpp" @@ -6269,6 +6270,22 @@ void rtlBase64Decode(size32_t & tlen, void * & tgt, size32_t slen, const char * } } +void rtlGetEclUserSecret(size32_t & outlen, void * & out, const char *name, const char *key) +{ + Owned secret = getSecret("eclUser", name); + if (secret) + { + MemoryBuffer data; + if (secret->getPropBin(key, data)) + { + outlen = data.length(); + out = data.detach(); + return; + } + } + out = nullptr; + outlen = 0; +} //--------------------------------------------------------------------------- diff --git a/rtl/eclrtl/eclrtl.hpp b/rtl/eclrtl/eclrtl.hpp index d2077c3da43..90080f42bbc 100644 --- a/rtl/eclrtl/eclrtl.hpp +++ b/rtl/eclrtl/eclrtl.hpp @@ -764,6 +764,8 @@ ECLRTL_API void rtlFreeException(IException * e); ECLRTL_API IAtom * rtlCreateFieldNameAtom(const char * name); +ECLRTL_API void rtlGetEclUserSecret(size32_t & outlen, void * & out, const char *name, const char *key); + /** * Wrapper function to encode input binary data with base 64 code. * From ef6353a84c15fdf7dc2649b4515d417225ca43b5 Mon Sep 17 00:00:00 2001 From: Gavin Halliday Date: Wed, 27 Apr 2022 16:27:58 +0100 Subject: [PATCH 03/24] HPCC-27579 Improve the stress tests for the threading classes Signed-off-by: Gavin Halliday --- testing/unittests/unittests.cpp | 83 +++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 13 deletions(-) diff --git a/testing/unittests/unittests.cpp b/testing/unittests/unittests.cpp index 757de1aa0f9..f14d12b6b81 100644 --- a/testing/unittests/unittests.cpp +++ b/testing/unittests/unittests.cpp @@ -716,10 +716,13 @@ class ThreadedPersistStressTest : public CppUnit::TestFixture testThreadsX(2); testThreadsX(3); testThreadsX(4); + testThreadsX(5); + testThreadsX(6); } void testThreadsX(unsigned mode) { unsigned iters = 10000; + testThreadsXX(mode, 0, iters); testThreadsXX(mode, 10, iters); testThreadsXX(mode, 1000, iters); testThreadsXX(mode, 2000, iters); @@ -731,11 +734,11 @@ class ThreadedPersistStressTest : public CppUnit::TestFixture } void testThreadsXX(unsigned mode, unsigned count, unsigned iters) { - unsigned start = msTick(); - class Thread : public IThreaded + unsigned start = usTick(); + class Threaded : public IThreaded { public: - Thread(unsigned _count) : count(_count) {} + Threaded(unsigned _count) : count(_count) {} virtual void threadmain() override { ret = call_from_thread(count); @@ -743,11 +746,24 @@ class ThreadedPersistStressTest : public CppUnit::TestFixture unsigned count; unsigned ret = 0; } t1(count), t2(count), t3(count); + class MyThread : public Thread + { + public: + MyThread(unsigned _count) : count(_count) {} + virtual int run() override + { + ret = call_from_thread(count); + return 0; + } + unsigned count; + unsigned ret = 0; + }; + + unsigned ret = 0; switch (mode) { case 0: { - unsigned ret = 0; CThreadedPersistent thread1("1", &t1), thread2("2", &t2), thread3("3", &t3); for (unsigned i = 0; i < iters; i++) { @@ -760,12 +776,10 @@ class ThreadedPersistStressTest : public CppUnit::TestFixture thread3.join(INFINITE); } ret += t1.ret + t2.ret + t3.ret; - DBGLOG("ThreadedPersistant %d , %d, %d", count, msTick() - start, ret); break; } case 1: { - unsigned ret = 0; for (unsigned i = 0; i < iters; i++) { t1.threadmain(); @@ -774,12 +788,10 @@ class ThreadedPersistStressTest : public CppUnit::TestFixture ret = call_from_thread(count); } ret += t1.ret + t2.ret + t3.ret; - DBGLOG("Sequential %d , %d, %d", count, msTick() - start, ret); break; } case 2: { - unsigned ret = 0; CThreaded tthread1("1", &t1), tthread2("2", &t2), tthread3("3", &t3); for (unsigned i = 0; i < iters; i++) { @@ -792,12 +804,10 @@ class ThreadedPersistStressTest : public CppUnit::TestFixture tthread3.join(); } ret += t1.ret + t2.ret + t3.ret; - DBGLOG("CThreaded %d , %d, %d", count, msTick() - start, ret); break; } case 3: { - unsigned ret = 0; for (unsigned i = 0; i < iters; i++) { class casyncfor: public CAsyncFor @@ -814,13 +824,11 @@ class ThreadedPersistStressTest : public CppUnit::TestFixture afor.For(4, 4); ret = afor.ret; } - DBGLOG("AsyncFor %d , %d, %d", count, msTick() - start, ret); break; } case 4: { CPersistentTask task1("1", &t1), task2("2", &t2), task3("3", &t3); - unsigned ret = 0; for (unsigned i = 0; i < iters; i++) { task1.start(); @@ -832,10 +840,59 @@ class ThreadedPersistStressTest : public CppUnit::TestFixture task3.join(INFINITE); } ret += t1.ret + t2.ret + t3.ret; - DBGLOG("PersistantTask %d , %d, %d", count, msTick() - start, ret); + break; + } + case 5: + { + MyThread thread1(count), thread2(count), thread3(count); + for (unsigned i = 0; i < iters; i++) + { + thread1.start(); + thread2.start(); + thread3.start(); + ret = call_from_thread(count); + thread1.join(INFINITE); + thread2.join(INFINITE); + thread3.join(INFINITE); + } + ret += thread1.ret + thread2.ret + thread3.ret; + break; + } + case 6: + { + IArrayOf threads; + for (unsigned i = 0; i < iters; i++) + { + MyThread * thread1 = new MyThread(count); + MyThread * thread2 = new MyThread(count); + MyThread * thread3 = new MyThread(count); + threads.append(*thread1); + threads.append(*thread2); + threads.append(*thread3); + + thread1->start(); + thread2->start(); + thread3->start(); + ret = call_from_thread(count); + thread1->join(INFINITE); + thread2->join(INFINITE); + thread3->join(INFINITE); + ret += thread1->ret + thread2->ret + thread3->ret; + +#if 0 + if (i >= 600) + { + threads.remove(0); + threads.remove(0); + threads.remove(0); + } +#endif + } break; } } + constexpr const char * modes[] = { "ThreadedPersistant", "Sequential", "CThreaded", "AsyncFor", "PersistantTask", "Thread", "ManyThread" }; + DBGLOG("%s %d, %d [%u], %u", modes[mode], count, usTick() - start, (usTick() - start) / iters / 4, ret); } }; From 2a3db20292bec53f79c26da9edf864387ac51b1c Mon Sep 17 00:00:00 2001 From: Gavin Halliday Date: Wed, 27 Apr 2022 17:49:27 +0100 Subject: [PATCH 04/24] HPCC-27578 Avoid modifying the thread name when a thread terminates Signed-off-by: Gavin Halliday --- system/jlib/jthread.cpp | 17 +++++++---------- system/jlib/jthread.hpp | 2 +- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/system/jlib/jthread.cpp b/system/jlib/jthread.cpp index 1f5f3bc4645..94d8fbc1b4e 100644 --- a/system/jlib/jthread.cpp +++ b/system/jlib/jthread.cpp @@ -143,16 +143,6 @@ void *Thread::_threadmain(void *v) t->tidlog = threadLogID(); #endif int ret = t->begin(); - char *&threadname = t->cthreadname.threadname; - if (threadname) { - memsize_t l=strlen(threadname); - char *newname = (char *)malloc(l+8+1); - memcpy(newname,"Stopped ",8); - memcpy(newname+8,threadname,l+1); - char *oldname = threadname; - threadname = newname; - free(oldname); - } { // need to ensure joining thread does not race with us to release t->Link(); // extra safety link @@ -378,6 +368,13 @@ void Thread::start() startRelease(); } +StringBuffer & Thread::getInfo(StringBuffer &str) +{ + const char * status = alive ? "" : "Stopped "; + str.appendf("%8" I64F "X %6" I64F "d %u: %s%s",(__int64)threadid,(__int64)threadid,tidlog,status,getName()); + return str; +} + void Thread::startRelease() { assertex(!alive); diff --git a/system/jlib/jthread.hpp b/system/jlib/jthread.hpp index aad55cda381..85e4c73218e 100644 --- a/system/jlib/jthread.hpp +++ b/system/jlib/jthread.hpp @@ -125,7 +125,7 @@ class jlib_decl Thread : public CInterface, public IThread virtual void start(); virtual void startRelease(); - StringBuffer &getInfo(StringBuffer &str) { str.appendf("%8" I64F "X %6" I64F "d %u: %s",(__int64)threadid,(__int64)threadid,tidlog,getName()); return str; } + StringBuffer &getInfo(StringBuffer &str); const char *getLogInfo(int &thandle,unsigned &tid) { #ifdef _WIN32 thandle = (int)(memsize_t)hThread; From 5ed0daa39d9bab0064a4a47702a4790db44cd7f8 Mon Sep 17 00:00:00 2001 From: "RISK\\fumeva01" Date: Tue, 3 May 2022 11:12:47 -0300 Subject: [PATCH 05/24] HPCC-27596 Update Portuguese language for 8.6.X Signed-off-by: RISK\fumeva01 --- .../ContainerizedHPCCSystemsPlatform.xml | 16 +- .../ContainerizedMods/ConfigureValues.xml | 1407 +++++++++++++++++ .../ContainerizedMods/ContainerLogging.xml | 495 ++++++ .../ContainerizedMods/CustomConfig.xml | 319 ++++ .../ContainerizedMods/LocalDeployment.xml | 332 +++- .../ContainerizedMods/PVCStorage.xml | 211 --- .../DynamicESDL/DESDL-Mods/ESDLESPstruct.xml | 2 +- .../DESDL-Mods/ESDLbooleanbool.xml | 2 +- .../ECLR_mods/BltInFunc-JOIN.xml | 5 +- .../ECLR_mods/ResrvdKywds-LIKELY.xml | 6 +- .../SLR-Mods/Copy.xml | 3 +- .../CT_Mods/CT_Comm_Line_DFU.xml | 38 +- .../HPCCClientTools/CT_Mods/CT_ECL_CLI.xml | 293 +++- .../HPCCClientTools/CT_Mods/CT_ECL_IDE.xml | 2 +- .../HPCCClientTools/CT_Mods/CT_Overview.xml | 19 +- .../CT_Mods/CT_Overview_withoutIDE.xml | 17 +- .../Inst-Mods/UserSecurityMaint.xml | 9 + .../Inst-Mods/hpcc_ldap.xml | 2 +- docs/PT_BR/images/CL-Img01-1.jpg | Bin 0 -> 80166 bytes docs/PT_BR/images/CL-Img02-1.jpg | Bin 0 -> 127653 bytes docs/PT_BR/images/CL-Img03-1.jpg | Bin 0 -> 12646 bytes 21 files changed, 2916 insertions(+), 262 deletions(-) create mode 100644 docs/PT_BR/ContainerizedHPCC/ContainerizedMods/ConfigureValues.xml create mode 100644 docs/PT_BR/ContainerizedHPCC/ContainerizedMods/ContainerLogging.xml create mode 100644 docs/PT_BR/ContainerizedHPCC/ContainerizedMods/CustomConfig.xml delete mode 100644 docs/PT_BR/ContainerizedHPCC/ContainerizedMods/PVCStorage.xml create mode 100644 docs/PT_BR/images/CL-Img01-1.jpg create mode 100644 docs/PT_BR/images/CL-Img02-1.jpg create mode 100644 docs/PT_BR/images/CL-Img03-1.jpg diff --git a/docs/PT_BR/ContainerizedHPCC/ContainerizedHPCCSystemsPlatform.xml b/docs/PT_BR/ContainerizedHPCC/ContainerizedHPCCSystemsPlatform.xml index 365cdd18ac7..546423e35f8 100644 --- a/docs/PT_BR/ContainerizedHPCC/ContainerizedHPCCSystemsPlatform.xml +++ b/docs/PT_BR/ContainerizedHPCC/ContainerizedHPCCSystemsPlatform.xml @@ -176,10 +176,10 @@ suficiente para que recomendamos o uso de uma ferramenta especial para editá-lo. - O values.yaml do helm chart - padrão tem menos de 100 linhas e pode ser editado em qualquer editor - e/ou modificado por meio das substituições da linha de comando. Também - é autodocumentado com comentários extensos. + O values.yaml do gráfico de + helm padrão é relativamente pequeno e pode ser aberto em qualquer + editor e/ou modificado por meio das substituições de linha de comando + do helm. Também é auto-documentado com extensos comentários. @@ -239,6 +239,12 @@ - + + + + diff --git a/docs/PT_BR/ContainerizedHPCC/ContainerizedMods/ConfigureValues.xml b/docs/PT_BR/ContainerizedHPCC/ContainerizedMods/ConfigureValues.xml new file mode 100644 index 00000000000..d2625fca6e0 --- /dev/null +++ b/docs/PT_BR/ContainerizedHPCC/ContainerizedMods/ConfigureValues.xml @@ -0,0 +1,1407 @@ + + + + Configuração dos Valores + + Este capítulo descreve a configuração do HPCC Systems para uma + implantação Kubernetes em contêineres. As seções a seguir detalham como as + configurações são fornecidas aos charts do helm, como descobrir quais opções + estão disponíveis e alguns detalhes da estrutura do arquivo de configuração. + As seções subsequentes também fornecerão uma breve explicação de alguns dos + conteúdos do arquivo padrão values.yaml, usado na + configuração do HPCC Systems para uma implantação em contêiner. + + + O Ambiente do Contêiner + + Uma das ideias por trás de nossa mudança para a nuvem foi tentar + simplificar a configuração do sistema e, ao mesmo tempo, fornecer uma + solução flexível o suficiente para atender às demandas de nossa + comunidade, aproveitando os recursos do contêiner sem sacrificar o + desempenho. + + Toda a configuração do HPCC Systems no espaço do contêiner é + governada por um único arquivo, um arquivo + values.yaml e seu arquivo de esquema + associado. + + + O <emphasis>values.yaml</emphasis> e como é utilizado + + O arquivo values.yaml são os valores de + configuração fornecidos para um chart do Helm. O arquivo + values.yaml é usado pelo chart do Helm para + controlar como o HPCC Systems é implantado na nuvem. Esse arquivo de + valores é usado para configurar e obter uma instância do HPCC Systems em + execução no Kubernetes. O arquivo values.yaml + define tudo o que acontece para configurar e/ou definir seu + sistema para uma implantação em contêiner. Você deve usar o arquivo de + valores fornecido como base para modelar as personalizações específicas + para sua implantação de acordo com seus requisitos. + + O arquivo values.yaml do HPCC Systems pode + ser encontrado no repositório github do HPCC Systems. Para usar o chart + Helm do HPCC Systems, primeiro adicione o repositório de charts hpcc + usando o Helm e, em seguida, acesse os valores do chart Helm dos charts + nesse repositório. + + Por exemplo, ao adicionar o repositório "hpcc", conforme + recomendado antes de instalar o chart do Helm com o seguinte + comando: + + helm repo add hpcc https://hpcc-systems.github.io/helm-chart + + + Agora você pode visualizar os charts entregues do HPCC Systems e + ver os valores lá emitindo: + + helm show values hpcc/hpcc + + Você pode capturar a saída deste comando, ver como os padrões são + configurados e usá-lo como base para sua customização. + + + + O values-schema.json + + O values-schema.json é um arquivo JSON que + declara o que é válido e o que não está dentro da soma total dos valores + mesclados que são passados para o Helm no momento da instalação. Ele + define quais valores são permitidos e valida o arquivo de valores em + relação a eles. Todos os itens principais são declarados no arquivo de + esquema, enquanto o arquivo default values.yaml + também contém comentários sobre os elementos mais importantes. Se você + quiser saber quais opções estão disponíveis para qualquer componente + específico, o esquema é um bom lugar para começar. + + O arquivo de esquema normalmente contém (para uma propriedade) um + nome e uma descrição. Muitas vezes, incluirá detalhes do tipo e os itens + que pode conter se for uma lista ou dicionário. Por exemplo: + + "roxie": { + "description": "roxie process", + "type": "array" + "items": { "$ref": "#/definitions/roxie" } + }, + + Cada plano, no arquivo de esquema, tem uma lista de propriedades + geralmente contendo um prefixo (caminho), um subcaminho (subcaminho) e + propriedades adicionais. Por exemplo, para um plano de armazenamento, o + arquivo de esquema possui uma lista de propriedades, incluindo o + prefixo. Os "planos" neste caso são uma referência ($ref) para outra + seção do esquema. O arquivo de esquema deve ser completo e conter tudo o + que é necessário, incluindo descrições que devem ser relativamente + autoexplicativas. + + "storage": { + "type": "object", + "properties": { + "hostGroups": { + "$ref": "#/definitions/hostGroups" + }, + "planes": { + "$ref": "#/definitions/storagePlanes" + } + }, + "additionalProperties": false + + + Observe o valor de additionalProperties + normalmente no final de cada seção no esquema. Ele especifica se os + valores permitem propriedades adicionais ou não. Se esse valor + additionalProperties estiver presente e definido + como false, nenhuma outra propriedade será permitida e a lista de + propriedades estará completa. + + Ao trabalhar com o HPCC Systems values.yam, o + arquivo de valores deve ser validado em relação a esse esquema. Se + houver um valor que não seja permitido conforme definido no arquivo de + esquema, ele não será iniciado e, em vez disso, gerará um ERRO. + + + + + Componentes HPCC Systems no Arquivo + <emphasis>values.yaml</emphasis> + + Os chart do Helm do HPCC Systems são enviados com valores de + estoque/padrão. Esses charts do Helm têm um conjunto de valores padrão + idealmente para serem usados como guia na configuração de sua implantação. + Geralmente, cada componente do HPCC Systems é uma lista. Essa lista define + as propriedades para cada instância do componente. + + Esta seção fornecerá detalhes adicionais e qualquer percepção digna + de nota para os componentes do HPCC Systems definidos no arquivo + values.yaml. + + + Os Componentes do HPCC Systems + + Uma das principais diferenças entre o bare metal e o + contêiner/nuvem é que o armazenamento bare metal está diretamente + vinculado aos nós do job trabalho Thor ou Thor e aos nós de trabalho + Roxie, ou mesmo no caso do servidor ECLCC as DLLs. Nos contêineres, eles + são completamente separados e qualquer coisa relacionada a arquivos é + definida no arquivo values.yaml + + Em contêineres, as instâncias de componentes são executadas + dinamicamente. Por exemplo, se você configurou seu sistema para usar um + Thor de 50 vias, então um Thor de 50 vias será gerado quando um trabalho + for enfileirado para ele. Quando esse trabalho for concluído, a + instância Thor desaparecerá. Este é o mesmo padrão para os outros + componentes também. + + Cada componente deve ter uma entrada de recursos, no arquivo + valores.yaml entregues os recursos estão presentes, + mas comentados conforme indicado aqui. + + #resources: + # cpu: "1" + # memory: "4G" +O arquivo de valores de estoque funcionará e permitirá que + você mantenha um sistema funcional, porém você deve definir os recursos + dos componentes da maneira que melhor corresponda à sua estratégia + operacional. + + + Os serviços do Sistema + + A maioria dos componentes do HPCC Systems tem uma entrada de + definição de serviço, semelhante à entrada de recursos. Todos os + componentes que possuem definições de serviço seguem esse mesmo + padrão. + + Qualquer informação relacionada ao serviço precisa estar em um + objeto de serviço, por exemplo: + + service: + servicePort: 7200 + visibility: local + + + Isso se aplica à maioria dos componentes do HPCC Systems, ESP, + Dali, dafilesrv e Sasha. A especificação do Roxie é um pouco + diferente, pois tem seu serviço definido em "roxieservice". Cada Roxie + pode ter várias definições de "roxieservice". (ver esquema). + + + + Dali + + Ao configurar o Dali, que também possui uma seção de recursos, + ele também precisará de muita memória e uma boa quantidade de CPU. É + muito importante defini-los com cuidado. Caso contrário, o Kubernetes + pode atribuir todos os pods à mesma máquina virtual e os componentes + que lutam pela memória os esmagarão. Portanto, mais memória atribuída + melhor. Se você definir isso errado e um processo usar mais memória do + que o configurado, o Kubernetes matará o pod. + + + + Componentes: dafilesvrs, dfuserver + + Os componentes do HPCC Systems de dafilesvrs, eclccservers, + dfuserver, são declarados como listas no yaml, assim como o ECL + Agent. + + Considere o dfuserver que está nos + values.yaml entregues do HPCC Systems + como: + + dfuserver: +- name: dfuserver + maxJobs: 1 + + Se você adicionar um mydfuserver da seguinte maneira: + + dfuserver: +- name: dfuserver + maxJobs: 1 +- name: mydfuserver + maxJobs: 1 +Nesse cenário, você teria outro item aqui chamado + mydfuserver, ele apareceria no ECLWatch e você poderia enviar itens + para ele. + + Se você quiser adicionar outro dfuserver, poderá adicioná-lo à + lista da mesma forma. Você também pode instanciar outros componentes + adicionando-os às suas respectivas listas. + + + + ECL Agent e ECLCC Server + + Values of note for the ECL Agent and ECLCC Server. + + useChildProcess -- Conforme + definido no esquema, iniciada cada compilação da workunit como um + processo secundário em vez de em seu próprio contêiner. Quando você + envia um job ou consulta para compilar, ele é enfileirado e + processado, com essa opção definida como true, ele gerará um processo + secundário utilizando quase nenhuma sobrecarga adicional na + inicialização. Ideal para enviar muitos jobs pequenos para compilar. + No entanto, como cada job de compilação não é mais executado como um + pod independente com suas próprias especificações de recursos, mas é + executado como um processo secundário no próprio pod do servidor + ECLCC, o pod do servidor ECLCC deve ser definido com recursos + adequados para si mesmo (mínimo para ouvir para a fila etc.) e todos + os jobs que ele possa ter que executar em paralelo. + + Por exemplo, imagine que maxJobs está + definido como 4 e 4 consultas grandes são enfileiradas rapidamente, o + que significa que 4 processos secundário são iniciados, cada cpu + consumindo e memória dentro do pod do servidor ECLCC. Com o componente + configurado com useChildProcesses definido como + true, cada trabalho será executado no mesmo pod (até o valor de + maxJobs em paralelo). Portanto, com + useChildProcesses habilitado, os recursos do + componente devem ser definidos de forma que o pod tenha recursos + suficientes para lidar com as demandas de recursos de todos esses + trabalhos para poder ser executado em paralelo. + + Com useChildProcess ativado, pode ser bastante caro na maioria + dos modelos de preços de nuvem e bastante dispendioso se não houver + nenhum job em execução. Em vez disso, você pode definir esse + useChildprocess como false (o padrão) para + iniciar um pod para compilar cada consulta apenas com a memória + necessária para o trabalho que será descartado quando concluído. + Agora, esse modelo também ouviu, talvez 20 segundos a um minuto para + gerar o cluster Kubernetes para processar o trabalho. O que pode não + ser ideal para um ambiente que está enviando vários trabalhos + pequenos, mas sim jobs maiores que minimizariam o efeito da sobrecarga + ao iniciar o cluster Kubernetes. + + Definir useChildProcess como false permite + melhor a possibilidade de dimensionamento dinâmico. Para jobs que + levariam muito tempo para compilar, a sobrecarga extra (inicialização) + é mínima, e esse seria o caso ideal para ter o + useChildProcess como falso. Definir + useChildProcess como false permite apenas 1 pod + por compilação, embora haja um atributo para colocar um limite de + tempo nessa compilação. + + ChildProcessTimeLimit é o tempo + limite (em segundos) para compilação de processos secundários antes de + abortarem e usarem um contêiner separado, quando o + useChildProcesses é false. + + maxActive -- O número máximo de + jobs que podem ser executadas em paralelo. Novamente, tome cuidado + porque cada job precisará de memória suficiente para ser executado. + Por exemplo, se maxActive estiver definido como + 2000, você poderá enviar um trabalho muito grande e, nesse caso, gerar + cerca de 2.000 trabalhos usando uma quantidade considerável de + recursos, o que poderia gerar uma conta de compilação bastante cara, + novamente dependendo do seu provedor de nuvem e seu plano de + faturamento. + + + + Sasha + + A configuração para Sasha é uma exceção, pois é uma estrutura do + tipo dicionário e não uma lista. Você não pode ter mais de um + arquivador ou dfuwu-archiver, pois isso é uma limitação de valor, você + pode optar por ter o serviço ou não (defina o valor 'disabled' como + true). + + + + Thor + + As instâncias Thor são executadas dinamicamente, assim como os + outros componentes em contêineres. A configuração do Thor também + consiste em uma lista de instâncias do Thor. Cada instância gera + dinamicamente uma coleção de pods (manager + N workers) quando os + workers são enfileirados para ela. Quando ocioso, não há pods de + worker (ou manager) em execução. + + Se você quisesse um Thor de 50 vias, você definiria o número de + workers, o valor numWorkers para 50 e + você teria um Thor de 50 vias. Conforme indicado no exemplo a + seguir: + + thor: +- name: thor + prefix: thor + numWorkers: 50 + + Ao fazer isso, o ideal é renomear o recurso para algo que o + descreva claramente, como thor_50 como no exemplo + a seguir. + + -name: thor_50 + + A atualização do valor numWorkers + reiniciará o agente Thor ouvindo a fila, fazendo com que todos os + novos jobs usem a nova configuração.3 + + maxJobs -- Controla o número de + jobs, especificamente maxJobs define o número + máximo de jobs. + + maxGraphs -- Limita a + quantidade máxima de charts. Geralmente faz sentido manter esse valor + abaixo ou no mesmo número de maxJobs, pois nem + todos os jobs enviam charts e quando fazem os jobs Thor não estão + executando charts o tempo todo. Se houver mais de 2 charts enviados + (Thor), o segundo será bloqueado até que a próxima instância Thor + fique disponível. + + A ideia aqui é que os jobs podem passar uma quantidade + significativa de tempo fora dos charts, como aguardar um estado de + fluxo de trabalho (fora do próprio mecanismo Thor), bloqueado em uma + persistência ou atualizando super arquivos etc. ter um limite maior de + trabalhos simultâneos (maxJobs) do que gráficos + (instâncias maxGraphs / Thor). Como as instâncias + Thor (charts) são relativamente caras (muitos pods/maior uso de + recursos), enquanto os pods de fluxo de trabalho (jobs) são + comparativamente baratos. + + Assim, os valores de charts entregues (exemplo) definem + maxJobs como maior que + maxGraphs. Os jobs enfileirados para um Thor nem + sempre estão executando charts. Portanto, pode fazer sentido ter mais + desses trabalhos, que não consomem um Thor grande e todos os seus + recursos, mas restringem o número máximo de instâncias do Thor em + execução. + + Thor têm 3 componentes (o que corresponde as seções de + recurso). + + + + Workflow + + + + Manager + + + + Workers + + + + O Manager e os Workers são lançados juntos e normalmente + consomem bastante recursos (e nós). Enquanto o Workflow é barato e + geralmente não requer tantos recursos. Você pode esperar em um mundo + Kubernetes, muitos deles coexistiriam no mesmo nó (e, portanto, seriam + baratos). Portanto, faz sentido que maxJobs seja + maior e maxGraphs seja menor + + No Kubernetes, os jobs são executados de forma independente em + seus próprios pods. Enquanto no bare metal, podemos ter jobs que podem + afetar outros trabalhos porque estão sendo executados no mesmo espaço + de processo. + + + + Memórias Thor e hThor + + As seções de memory Thor e hThor permitem + que a memória de recursos do componente seja refinada em diferentes + áreas. + + Por exemplo, o "workerMemory" para um Thor é definido + como: + + thor: +- name: thor + prefix: thor + numWorkers: 2 + maxJobs: 4 + maxGraphs: 2 + managerResources: + cpu: "1" + memory: "2G" + workerResources: + cpu: "4" + memory: "4G" + workerMemory: + query: "3G" + thirdParty: "500M" + eclAgentResources: + cpu: "1" + memory: "2G" + + A seção "workerResources" informará ao + Kubernetes para recursos 4G por pod de worker. Por padrão, o Thor + reservará 90% dessa memória para usar na memória de consulta HPCC + (roxiemem). Os 10% restantes são deixados para todos os outros usos + não baseados em linha (roxiemem), como heap geral, sobrecarga do + sistema operacional etc. Não há permissão para qualquer biblioteca de + terceiros, plug-ins ou uso de linguagem incorporada dentro desse + padrão. Em outras palavras, se, por exemplo, o python incorporado + alocar 4G, o processo logo falhará com um erro de falta de memória, + quando começar a usar qualquer memória, pois esperava que 90% desse 4G + estivesse disponível gratuitamente para uso próprio. + + Esses padrões podem ser substituídos pelas seções de memória. + Neste exemplo, workerMemory.query define que 3G + da memória com recursos disponíveis deve ser atribuído à memória de + consulta e 500M para usos de "thirdParty". + + Isso limita o uso de roxiemem de memória do HPCC Systems para + exatamente 3G, deixando 1G livre para outros propósitos. O + "thirdParty" não é realmente alocado, mas é usado apenas como parte do + total em execução, para garantir que a configuração não especifique um + total nesta seção maior que a seção de recursos, por exemplo, se + "thirdParty" foi definido como " 2G" na seção acima, haveria uma + reclamação de tempo de execução quando Thor executasse que a definição + excedeu o limite de recursos. + + Também é possível substituir a porcentagem recomendada padrão + (90% por padrão), definindo maxMemPercentage. Se + "query" não estiver definida, ela será calculada como a memória máxima + recomendada menos a memória definida (por exemplo, + "thirdParty). + + No Thor existem 3 áreas de recursos, eclAgent, + ThorManager e ThorWorker(s). Cada um + tem uma área *Resource que define suas necessidades de recursos do + Kubernetes e uma seção *Memory correspondente que pode ser usada para + substituir os requisitos de alocação de memória padrão. + + Essas configurações também podem ser substituídas por consulta, + por meio de opções de workunits seguindo o padrão: + <memory-section-name>.<property>. Por exemplo: + #option('workerMemory.thirdParty', "1G"); + + Nota: Atualmente, há apenas + "consulta" (uso de HPCC roxiemem) e "thirdParty" para todos/qualquer + uso de terceiros. É possível que outras categorias sejam adicionadas + no futuro, como "python" ou "java" - que definem especificamente os + usos de memória para esses destinos. + + + + + + O arquivo HPCC Systems <emphasis>values.yaml</emphasis> + + O arquivo HPCC systems values.yaml entregue é + mais um exemplo que fornece uma configuração de tipo básico que deve ser + personalizada para suas necessidades específicas. Uma das principais + ideias por trás do arquivo de valores é poder personalizá-lo com relativa + facilidade para seu cenário específico. O chart entregue é configurado + para ser sensato o suficiente para ser entendido, ao mesmo tempo em que + permite uma personalização relativamente fácil para configurar um sistema + de acordo com seus requisitos específicos. Esta seção examinará mais de + perto alguns aspectos do arquivo values.yaml. + + O arquivo HPCC Systems Values entregue consiste principalmente nas + seguintes áreas: + + + + + + global + + storage + + visibilities + + + + data planes + + certificates + + security + + + + secrets + + components + + + + + + + + As seções subsequentes examinarão alguns deles mais de perto e por + que cada um deles está lá. + + + Armazenamento + + O armazenamento em contêiner é outro conceito-chave que difere do + bare metal. Existem algumas diferenças entre contêiner e armazenamento + em metal. A seção Storage é bastante bem definida entre o arquivo de + esquema e o values.yaml. Uma boa abordagem para + armazenamento é entender claramente suas necessidades de armazenamento e + descrevê-las, e uma vez que você tenha essa estrutura básica em mente, o + esquema pode ajudar a preencher os detalhes. O esquema deve ter uma + descrição decente para cada atributo. Todo o armazenamento deve ser + definido por meio de planos. Há um comentário relevante no arquivo + values.yaml descrevendo melhor o + armazenamento. + + ## storage: +## +## 1. If an engine component has the dataPlane property set, +# then that plane will be the default data location for that component. +## 2. If there is a plane definition with a category of "data" +# then the first matching plane will be the default data location +## +## If a data plane contains the storageClass property then an implicit pvc +# will be created for that data plane. +## +## If plane.pvc is defined, a Persistent Volume Claim must exist with that name, +# storageClass and storageSize are not used. +## +## If plane.storageClass is defined, storageClassName: <storageClass> +## If set to "-", storageClassName: "", which disables dynamic provisioning +## If set to "", choosing the default provisioner. +# (gp2 on AWS, standard on GKE, AWS & OpenStack) +## +## plane.forcePermissions=true is required by some types of provisioned +## storage, where the mounted filing system has insufficient permissions to be +## read by the hpcc pods. Examples include using hostpath storage (e.g. on +## minikube and docker for desktop), or using NFS mounted storage. + + + Existem diferentes categorias de armazenamento, para uma + implantação de HPCC Systems você deve ter no mínimo uma categoria dali, + uma categoria dll e pelo menos 1 categoria de dados. Esses tipos + geralmente são aplicáveis a todas as configurações, além de outras + categorias opcionais de dados. + + Todo o armazenamento deve estar em uma definição de plano de + armazenamento. Isso é melhor descrito no comentário na definição de + armazenamento no arquivo de valores. + + planes: + # name: <required> + # prefix: <path> # Root directory for accessing the plane + # (if pvc defined), + # # or url to access plane. + # category: data|dali|lz|dll|spill|temp # What category of data is stored on this plane? + # + # For dynamic pvc creation: + # storageClass: '' + # storageSize: 1Gi + # + # For persistent storage: + # pvc: <name> # The name of the persistant volume claim + # forcePermissions: false + # hosts: [ <host-list> ] # Inline list of hosts + # hostGroup: <name> # Name of the host group for bare metal + # # must match the name of the storage plane.. + # + # Other options: + # subPath: <relative-path> # Optional sub directory within <prefix> + # # to use as the root directory + # numDevices: 1 # number of devices that are part of the plane + # secret: <secret-id> # what secret is required to access the files. + # # This could optionally become a list if required + # (or add secrets:). + + # defaultSprayParts: 4 # The number of partitions created when spraying + # (default: 1) + + # cost: # The storage cost + # storageAtRest: 0.0135 # Storage at rest cost: cost per GiB/month + + Cada plano tem 3 campos obrigatórios: O nome, a categoria e o + prefixo. + + Quando o sistema estiver instalado, usando os valores fornecidos + em estoque, ele criará um volume de armazenamento com capacidade de 1 GB + através das seguintes propriedades. + + Por exemplo: + + - name: dali + storageClass: "" + storageSize: 1Gi + prefix: "/var/lib/HPCCSystems/dalistorage" + category: dali + + + Mais comumente o prefixo: define o caminho dentro do contêiner + onde o armazenamento está montado. O prefixo pode ser uma URL para + armazenamento de blobs. Todos os pods usarão o caminho (prefixo: ) para + acessar o armazenamento. + + Para o exemplo acima, quando você observar a lista de + armazenamento, o storageSize criará um volume com 1 + GB de capacidade. O prefixo será o caminho, a categoria é usada para + limitar o acesso aos dados e minimizar o número de volumes acessíveis de + cada componente. + + As listas de armazenamento dinâmico no arquivo + values.yaml são caracterizadas pelos valores + storageClass: e storageSize:. + + storageClass: define qual storage + deve ser usado para alocar o armazenamento. Uma classe de armazenamento + em branco indica que deve usar a classe de armazenamento de provedores + de nuvem padrão. + + storageSize: Conforme indicado no + exemplo, define a capacidade do volume. + + + Categoria de Armazenamento + + A categoria de armazenamento (Storage Category) é usada para + indicar o tipo de dados que está sendo armazenado nesse local. + Diferentes planos são usados para as diferentes categorias para isolar + os diferentes tipos de dados uns dos outros, mas também porque eles + geralmente exigem características de desempenho diferentes. Um plano + nomeado pode armazenar apenas uma categoria de dados. As seções a + seguir examinam as categorias de dados com suporte atualmente usadas + em nossa implantação em contêiner. + + category: data|dali|lz|dll|spill|temp # What category of data is stored on this plane? + + O próprio sistema pode gravar em qualquer plano de dados. É + assim que a categoria de dados pode ajudar a melhorar o desempenho. + Por exemplo, se você tiver um índice, o Roxie desejará acesso rápido + aos dados, em vez de outros componentes. + + Alguns componentes podem usar apenas 1 categoria, alguns podem + usar várias. O arquivo de valores pode conter mais de uma definição de + plano de armazenamento para cada categoria. O primeiro plano de + armazenamento na lista para cada categoria é usado como local padrão + para armazenar essa categoria de dados. Essas categorias minimizam a + exposição dos dados do avião a componentes que não precisam deles. Por + exemplo, o componente ECLCC Server não precisa saber sobre as landing + zones ou onde Dali armazena seus dados, portanto, ele monta apenas as + categorias de avião necessárias. + + + + Armazenamento Temporário + + O armazenamento temporário (Ephemeral storage) é alocado quando + o cluster HPCC Systems é instalado e excluído quando o chart é + desinstalado. Isso é útil para manter os custos de nuvem baixos, mas + pode não ser apropriado para seus dados. + + Em seu sistema, você deseja substituir o(s) valor(es) de estoque + fornecido(s) pelo armazenamento apropriado para suas necessidades + específicas. Os valores fornecidos criam volumes persistentes efêmeros + ou temporários que são excluídos automaticamente quando o chart é + desinstalado. Você provavelmente quer que o armazenamento seja + persistente. Você deve personalizar o armazenamento para uma + configuração mais adequada às suas necessidades. + + + + Armazenamento Persistente + + O Kubernetes usa declarações de volume persistentes (pvcs) para + fornecer acesso ao armazenamento de dados. O HPCC Systems oferece + suporte ao armazenamento em nuvem por meio do provedor de nuvem que + pode ser exposto por meio dessas declarações de volume + persistentes. + + As Declarações de Volume Persistentes podem ser criadas + substituindo os valores de armazenamento no chart do Helm entregue. Os + valores no arquivo example/local/values-localfile.yaml fornecidos + substituem as entradas correspondentes no chart de comando original da + pilha entregue do HPCC Systems. O chart localfile cria volumes de + armazenamento persistentes. Você pode usar o values-localfile.yaml + diretamente (como demonstrado em documentos/tutoriais separados) ou + pode usá-lo como base para criar seu próprio chart de + substituição. + + Para definir um plano de armazenamento que utiliza um PVC, você + deve decidir onde esses dados residirão. Você cria os diretórios de + armazenamento, com os nomes apropriados e, em seguida, pode instalar o + chart do Helm de arquivos locais para criar os volumes para usar a + opção de armazenamento local, como no exemplo a seguir: + + helm install mycluster hpcc/hpcc -f examples/local/values-localfile.yaml + + Nota: As configurações para os + PVCs devem ser ReadWriteMany, exceto para Dali que pode ser + ReadWriteOnce. + + Há vários recursos, blogs, tutoriais e até mesmo vídeos de + desenvolvedores que fornecem detalhes passo a passo para a criação de + volumes de armazenamento persistentes. + + + + Armazenamento Bare Metal + + Há dois aspectos no uso do armazenamento bare metal no sistema + Kubernetes. A primeira é a entrada hostGroups na + seção de armazenamento que fornece listas nomeadas de hosts. As + entradas hostGroups podem assumir uma das duas + formas. Essa é a forma mais comum e associa diretamente uma lista de + nomes de host a um nome: + + storage: + hostGroups: + - name: <name> "The name of the host group" + hosts: [ "a list of host names" ] + + + A segunda forma permite que um grupo de hosts seja derivado de + outro: + + storage: + hostGroups: + - name: "The name of the host group process" + hostGroup: "Name of the hostgroup to create a subset of" + count: <Number of hosts in the subset> + offset: <the first host to include in the subset> + delta: <Cycle offset to apply to the hosts> + + + Alguns exemplos típicos com clusters bare-metal são subconjuntos + menores do host ou os mesmos hosts, mas armazenando partes diferentes + em nós diferentes, por exemplo: + + Group: groupCDE + delta: 1 + + O segundo aspecto é adicionar uma propriedade à definição do + plano de armazenamento para indicar quais hosts estão associados a + ela. Existem duas opções: + + + + hostGroup: <name> O + nome do grupo de hosts para bare metal. O nome do hostGroup deve + corresponder ao nome do plano de armazenamento.. + + + + hosts: + <list-of-namesname> Uma lista embutida de hosts. + Principalmente útil para landing zones. + + + + Por Exemplo: + + storage: + planes: + - name: demoOne + category: data + prefix: "/home/demo/temp" + hostGroup: groupABCD # The name of the hostGroup + - name: myDropZone + category: lz + prefix: "/home/demo/mydropzone" + hosts: [ 'mylandingzone.com' ] # Inline reference to an external host. + + + + + Itens de armazenamento para componentes de HPCC Systems + + + Armazenamento geral de dados + + Os arquivos de dados gerais gerados pelo HPCC são armazenados em + dados. Para Thor, os custos de armazenamento de dados provavelmente + podem ser significativos. A velocidade de acesso sequencial é + importante, mas o acesso aleatório é muito menos importante. Para + ROXIE, a velocidade de acesso aleatório provavelmente será mais + importante. + + + + LZ + + LZ ou lz, utilizados para dados da landing zone. É aqui que + colocamos os dados brutos que chegam ao sistema. Uma landing zone onde + usuários externos podem ler e gravar arquivos. O HPCC Systems podem + importar ou exportar arquivos para uma landing zone. Normalmente, o + desempenho é um problema menor, pode ser armazenamento de bucket + blob/s3, acessado diretamente ou por meio de uma montagem NFS. + + + + dali + + A localização do repositório de metadados dali, que precisa dar + suporte ao acesso aleatório rápido. + + + + dll + + Onde as consultas ECL compiladas são armazenadas. O + armazenamento precisa permitir que objetos compartilhados sejam + carregados diretamente a partir dele de forma eficiente. Se você + quiser dados Dali e dll no mesmo plano, é possível usar o mesmo + prefixo para ambas as propriedades do subcaminho. Ambos usariam o + mesmo prefixo, mas deveriam ter subcaminhos diferentes. + + + + sasha + + Este é o local onde as workunitss são arquivadas, etc., são + armazenadas e normalmente é menos crítico de velocidade, exigindo + menores custos de armazenamento. + + + + spill + + Uma categoria opcional na qual os arquivos spill são gravados. + Os discos NVMe locais são potencialmente uma boa opção para + isso. + + + + temp + + Uma categoria opcional onde os arquivos temporários podem ser + gravados. + + + + + Valores de Segurança + + Esta seção examinará as seções de values.yaml + que tratam dos componentes de segurança do sistema. + + + Certificados + + A seção de certificados pode ser usada para permitir que o + cert-manager gere certificados TLS para cada componente na implantação + do HPCC Systems. + + certificates: + enabled: false + issuers: + local: + name: hpcc-local-issuer + + No arquivo yaml entregue, os certificados não estão habilitados, + conforme ilustrado acima. Você deve primeiro instalar o cert-manager + para usar esse recurso. + + + + Secrets + + A seção Secrets contém um conjunto de categorias, cada uma + contendo uma lista de secrets. A seção Secrets é onde obter + informações no sistema se você não as quiser na fonte. Como código + incorporado, você pode definir isso nas seções de sinal de código. Se + você tiver informações que não deseja que sejam públicas, mas precisa + executá-las, poderá usar segredos. + + + + Vaults + + Vaults é outra maneira de fazer Secrets. A seção de vaults + espelha a seção secrets, mas aproveita o HashiCorp Vault para o + armazenamento de secrets. Há uma categoria adicional para vaults + chamada "ecl-user". A intenção dos secrets do vaults do usuário ecl é + ser legível diretamente do código ECL. Outras categorias vaults são + lidas internamente pelos componentes do sistema e não expostas + diretamente ao código ECL. + + + + + Visibilidades + + A seção de visibilidades pode ser usada para definir rótulos, + anotações e tipos de serviço para qualquer serviço com a visibilidade + especificada. + + + + Réplicas e Resources + + Outros valores dignos de nota nos charts que têm relação com a + instalação e configuração do HPCC Systems. + + + Réplicas + + replicas: define quantos nós de réplica surgem, quantos pods são + executados para equilibrar uma carga. Para ilustrar, se você tiver um + Roxie de 1 via e definir réplicas como 2, você terá 2 Roxies de 1 + via. + + + + Recursos + + A maioria dos componentes tem uma seção de recursos que define + quantos recursos são atribuídos a esse componente. Nos arquivos de + valores entregues em estoque, as seções recursos: existem apenas para + fins ilustrativos e são comentadas. Qualquer implantação em nuvem que + venha a desempenhar qualquer função não trivial, esses valores devem + ser definidos adequadamente com recursos adequados para cada + componente, da mesma forma que você alocaria recursos físicos + adequados em um data center. Os recursos devem ser configurados de + acordo com os requisitos específicos do sistema e o ambiente em que + você os executaria. A definição inadequada de recursos pode resultar + em falta de memória e/ou remoção do Kubernetes, pois o sistema pode + usar quantidades ilimitadas de recursos, como memória e os nós ficarão + sobrecarregados, momento em que o Kubernetes começará a despejar os + pods. Portanto, se sua implantação estiver vendo despejos frequentes, + convém ajustar sua alocação de recursos. + + #resources: + # cpu: "1" + # memory: "4G" +Cada componente deve ter uma entrada de recursos, mas alguns + componentes, como Thor, possuem vários recursos. Os componentes + manager, worker e eclagent têm requisitos de recursos + diferentes. + + + + Taints, Tolerations, e placements + + Esta é uma consideração importante para sistemas em contêineres. + Taints e Tolerations são tipos de restrições de nó do Kubernetes + também referidas por Node Affinity. A + afinidade do nó é uma maneira de restringir os pods aos nós. Apenas + uma "afinidade" pode ser aplicada a um pod. Se um pod corresponder a + várias listas de "pods" de canais, somente a última definição de + "afinidade" será aplicada. + + Os taints e as tolerations trabalham juntos para garantir que os + pods não sejam agendados em nós inadequados. As tolerâncias são + aplicadas aos pods e permitem (mas não exigem) que os pods sejam + agendados em nós com taints correspondentes. Taints são o oposto - + elas permitem que um nó repele um conjunto de cápsulas. + + Por exemplo, todos os workers Thor devem estar no tipo + apropriado de VM. Se um grande job de Thor aparecer – então o nível de + taints entra em jogo. + + Para obter mais informações e exemplos de nossos Taints, + Tolerations e Placements, consulte nossa documentação do + desenvolvedor: + + https://github.com/hpcc-systems/HPCC-Platform/blob/master/helm/hpcc/docs/placements.md + + + Placement + + O Placement é responsável por encontrar o melhor nó para um + pod. Na maioria das vezes, o placement é tratado automaticamente + pelo Kubernetes. Você pode restringir um pod para que ele possa ser + executado apenas em um conjunto específico de nós. Usando canais, + você pode configurar o agendador do Kubernetes para usar uma lista + de "pods" para aplicar configurações aos pods. Por exemplo: + + placements: + - pods: [list] + placement: + <supported configurations> + + Os pods: [list] podem conter uma variedade de itens. + + + + Tipos de componentes do HPCC Systems, usando o + tipo de prefixo: pode ser: dali, esp, + eclagent, eclccserver, roxie, thor. Por exemplo + "tipo:esp" + + + + Alvo; o nome de um item de array dos tipos acima usando o + prefixo "target:" Por exemplo "target:roxie" ou + "target:thor". + + + + Pod, nome de metadados "Implantação" do nome do item de + matriz de um tipo. Por exemplo, "eclwatch", "mydali", + "thor-thoragent" + + + + Expressão regular do nome do trabalho: Por exemplo, + "compile-" ou "compile-". ou correspondência exata + "^compile-.$" + + + + Todos: para solicitar todos os componentes do HPCC + Systems. Os canais padrão para os pods que entregamos são + [all] + + + + Placements – no Kubernetes, o + conceito de placement permite distribuir seus pods por tipos de nós + com características particulares. Os placements seriam usados para + garantir que os pods ou trabalhos que desejam nós com + características específicas sejam colocados neles. + + Por exemplo, um cluster Thor pode ser direcionado para machine + learning usando nós com uma GPU. Outro trabalho pode querer nós com + uma boa quantidade de memória ou outro para mais CPU. Você pode usar + posicionamentos para garantir que os pods com requisitos específicos + sejam colocados nos nós apropriados. + + + + + + + Mais Helm e Yaml + + Esta seção destina-se a fornecer algumas informações úteis para + começar com uma implantação em contêiner. Existem vários recursos para + usar arquivos Kubernetes, Helm e Yaml. Anteriormente, abordamos o arquivo + values.yaml e o arquivo values-schema.json. Esta + seção expande alguns desses conceitos e como eles podem ser aplicados ao + usar a versão em contêiner da plataforma HPCC Systems. Para obter mais + informações sobre como usar arquivos Kubernetes, Helm ou YAML, ou para + implantações de nuvem ou contêiner, consulte a respectiva + documentação. + + + Estrutura do arquivo <emphasis>values.yaml</emphasis> + + O arquivo values.yaml é um arquivo yaml. Yaml + é uma linguagem de serialização de dados frequentemente usada como + formato para arquivos de configuração. A construção que compõe a maior + parte de um arquivo yaml é o par chave-valor, às vezes chamado de hash + ou dicionário. A construção do par chave-valor consiste em uma chave que + aponta para algum(s) valor(es). Os valores podem ser strings, números, + booleanos, inteiros, arrays ou dicionários e listas. Esses valores são + definidos pelo esquema. + + Em arquivos yaml, o recuo é usado para representar a estrutura e o + aninhamento do documento. Espaços à esquerda são significativos e + tabulações não são permitidas. + + + Dicionário + + Dicionários são coleções de mapeamentos de valores-chave. Todas + as chaves diferenciam maiúsculas de minúsculas e, como mencionamos + anteriormente, o recuo também é crucial. Essas chaves devem ser + seguidas por dois pontos (:) e um espaço. Os dicionários também podem + ser aninhados. + + Dictionary is a key: value, followed by another key: value:, for + example: + + logging: + detail: 80 + + + Isso é uma exemplo de dicionário para registro. + + Os dicionários nos arquivos de valores passados, como os do + arquivo myoverrides.yaml no exemplo abaixo, serão + mesclados nos dicionários correspondentes nos valores existentes, + começando com os valores padrão do chart helm entregue. + + helm install myhpcc hpcc/hpcc -f myoverrides.yaml + + Observe que você pode passar quantos arquivos yaml desejar, eles + serão mesclados na ordem em que aparecem na linha de comando. + + Quaisquer valores pré-existentes em um dicionário que não sejam + substituídos continuarão presentes no resultado mesclado. No entanto, + você pode excluir o conteúdo de um dicionário definindo-o como + nulo. + + + + Listas + + Listas são grupos de elementos começando no mesmo nível de recuo + começando com um - (um traço e um espaço). Cada elemento da lista é + recuado no mesmo nível e começa com um traço e um espaço. As listas + também podem ser aninhadas e podem ser listas de dicionários, que por + sua vez também podem ter propriedades de lista. + + Um exemplo de uma lista de dicionários, com + placement.tolerations como uma lista aninhada.: + + placements: +- pods: ["all"] + placement: + tolerations: + - key: "kubernetes.azure.com/scalesetpriority" + + + Uma chave é denotada usando um sinal de menos, que é um item de + entrada na lista, que é um dicionário com atributos aninhados. Em + seguida, o próximo sinal de menos (no mesmo nível de recuo) é a + próxima entrada nessa lista. + + + + + Global + + A primeira seção do arquivo values.yaml + descreve os valores globais. O global.image.root é uma string que indica + qual versão extrair. Global se aplica geralmente a tudo. + + # Default values for hpcc. + +global: + # Settings in the global section apply to all HPCC components in all subcharts + + image: + ## It is recommended to name a specific version rather than latest, for any non-trivial + ## For best results, the helm chart version and platform version should match - default if version + ## not specified. Do not override without good reason as undefined behavior may result. + ## version: x.y.z + root: "hpccsystems" # change this to pull from somewhere other than DockerHub hpccsystems + pullPolicy: IfNotPresent + + # logging sets the default logging information for all components. Can be overridden locally + logging: + detail: 80 + + + No trecho do arquivo value.yaml entregue do + HPCC Systems (acima) global: é um dicionário de + nível superior. Conforme observado nos comentários, as configurações na + seção global se aplicam a todos os componentes do HPCC Systems. Observe + a partir do recuo que os outros valores estão aninhados nesse dicionário + global. + + + Imagem + + Em nosso arquivo values.yaml entregue, o + valor imediatamente após global: é image: você + deve usar uma versão nomeada específica em vez de usar o "latest", + como também indicado nos comentários no arquivo de valores. A versão + do chart do Helm e a versão da plataforma devem corresponder. + Idealmente, você não deveria ter que definir o image.version. Por + padrão, ele corresponderá à versão do chart do helm. + + + + O valor root + + A entrada de nível de definição/dicionário global é root. Por + exemplo + + root: "hpccsystems" # change to pull your images somewhere other than DockerHub hpccsystems + + + No arquivo values.yaml, isso usa nosso + repositório específico do HPCC Systems. É possível que você queira + extrair de algum outro repositório, então é onde definir esse + valor. + + root: SomeValue + + + + Outros valores de chart + + Itens definidos na seção global são compartilhados entre + componentes. + + Examplos de valores global são seções de armazenamento e + segurança. + + storage: + planes: + + e também + + security: + eclSecurity: + # Possible values: + # allow - functionality is permitted + # deny - functionality is not permitted + # allowSigned - functionality permitted only if code signed + embedded: "allow" + pipe: "allow" + extern: "allow" + datafile: "allow"Nos exemplos acima, storage: e security: + são valores globais do chart. + + + + + Usage + + O arquivo values.yaml do HPCC Systems é usado + pelo chart Helm para controlar como o HPCC Systems é implantado. O + arquivo de valores contém dicionários e listas, e eles podem ser + aninhados para criar estruturas mais complexas. O arquivo HPCC Systems + values.yaml destina-se a ser um guia de instalação + de demonstração de início rápido que não é apropriado para uso prático + não trivial. Você deve personalizar sua implantação para uma que seja + mais adequada às suas necessidades específicas. Para personalizar sua + implantação, substitua os valores de estoque no arquivo + values.yaml, como no exemplo a seguir: + + helm install myhpcc hpcc/hpcc -f myoverrides.yaml + + O exemplo acima usa o arquivo + myoverrides.yaml por meio do parâmetro -f, que + substitui quaisquer valores especificados no arquivo + values.yaml do HPCC Systems. É importante observar + que isso mescla as substituições de myoverrides.yaml. Qualquer coisa que + esteja nos valores no próprio chart do helm que não seja substituído + pelos valores passados permanecerá ativo. Quando houver 2 arquivos yaml + como este exemplo (as pilhas values.yaml e + myoverrides.yaml), se houver uma entrada correspondente (qualquer coisa + que não seja um dicionário), o valor do segundo arquivo substituirá o + primeiro. Os dicionários, no entanto, sempre serão mesclados. + + Mais informações sobre implantações personalizadas são abordadas + em outras seções, bem como na documentação do Kubernetes Helm. Consultar + a documentação do Helm fornece detalhes completos para todos os aspectos + do uso do gráfico do Helm, e não apenas para alguns casos selecionados + descritos. + + + Caso de Uso + + Por exemplo, você deseja atualizar os detalhes do log. Você pode + ter outro arquivo yaml para atualizar esse valor ou qualquer outro + valor de lista usando um arquivo yaml de substituição. + + Como veremos mais adiante, os componentes são definidos como + listas, portanto, qualquer definição de um componente em um arquivo de + valores do usuário substituirá todas as instâncias do componente no + chart padrão. Você pode remover todos os componentes definidos em uma + lista, substituindo a lista por uma lista nula, por exemplo, + + thor: [] + + Isso removerá todo os componentes Thor. + + Outras opções (por exemplo, configurar os custos para cpu ou + acesso a arquivos) são implementadas como um dicionário, de modo que + as opções podem ser definidas seletivamente em um arquivo de valores + de usuários, e as outras opções serão mantidas. + + + + Mesclando e Sobrescrevendo + + Tendo vários arquivos yaml, como um para registro, outro para + armazenamento, outro para segredos e assim por diante, os arquivos + podem estar no controle de versão. Eles podem ser versionados, + verificados, etc. e têm o benefício de apenas definir/alterar a área + específica necessária, garantindo que todas as áreas não alteradas + sejam deixadas intocadas. A regra aqui para manter em mente onde + vários arquivos yaml são aplicados, os mais recentes sempre + substituirão os valores dos anteriores. Eles são mesclados em + sequência. + + Outro ponto a considerar, onde existe um dicionário global como + root: e seu valor é redefinido no 2º arquivo (como um dicionário) ele + não seria sobrescrito. Você não pode simplesmente substituir um + dicionário. Você pode redefinir um dicionário e defini-lo como nulo + (como o exemplo Thor na seção anterior), o que efetivamente o + eliminará. + + ATENÇÃO: Se você tivesse uma + definição global (como storage.planes) e a mesclasse onde ela fosse + redefinida, eliminaria todas as definições da lista. + + Outro meio de eliminar todos os valores em uma lista é passar um + conjunto vazio denotado por um [ ], como este exemplo: + + bundles: []Isso eliminaria + quaisquer propriedades definidas para pacotes configuráveis. + + + Geralmente aplicável + + Esses itens são geralmente aplicáveis para nossos arquivos + yaml do HPCC Systems Helm. + + + + Todos os nomes devem ser únicos. + + + + Todos os prefixos devem ser únicos. + + + + Os serviços devem ser únicos. + + + + yaml são mesclados em sequência. + + + + Geralmente em relação aos componentes do HPCC Systems, os + componentes são listas. Como dito anteriormente, se você tiver uma + lista de valores vazia [ ], isso invalidaria essa lista em outro + lugar. + + + + + + Uso adicional + + Os componentes são adicionados ou modificados passando + sobreposições. Os valores do chart são substituídos apenas, passando no + arquivo de valores de substituição usando -f, (para arquivo de + substituição) ou via --set, onde você pode substituir um único valor. Os + valores passados são sempre mesclados na ordem em que são fornecidos na + linha de comando do helm. + + Por exemplo, você pode + + helm install myhpcc hpcc/hpcc -f myoverrides.yamlPara + sobrepor quaisquer valores entregues no + values.yaml. Ou você pode usar --set conforme o + exemplo: + + helm install myhpcc hpcc/hpcc --set storage.daliStorage.plane=dali-plane + + Para substituir apenas o valor global.image.version. Novamente, a + ordem em que os valores são mesclados é a mesma em que são emitidos na + linha de comando. Agora considere: + + helm install myhpcc hpcc/hpcc -f myoverrides.yaml --set storage.daliStorage.plane=dali-plane + + No exemplo anterior, a flag --set no comando acima substitui o + valor de storage.daliStorage.plane (if) definido em myoverrides.yaml, + que substitui qualquer configuração de arquivo + values.yaml e resulta em defini-lo como dali-plane. + Portanto, independentemente do valor no arquivo yaml para essa + configuração específica, a ordem especificada na linha de comando o + substitui na ordem fornecida na linha de comando. + + + Opções de linha de comando + + Se a flag --set for usada na instalação do + helm ou na atualização do helm, esses valores serão simplesmente + convertidos em YAML no lado do cliente. + + Você pode especificar a flag -f várias vezes. A prioridade será + dada ao último arquivo (mais à direita) especificado.$ helm install myhpcc hpcc/hpcc -f myvalues.yaml -f override.yaml + + Para o exemplo acima, se myvalues.yaml e override.yaml + contivessem uma chave chamada 'Test', o valor definido em + override.yaml teria precedência. + + + + diff --git a/docs/PT_BR/ContainerizedHPCC/ContainerizedMods/ContainerLogging.xml b/docs/PT_BR/ContainerizedHPCC/ContainerizedMods/ContainerLogging.xml new file mode 100644 index 00000000000..b5c68ece587 --- /dev/null +++ b/docs/PT_BR/ContainerizedHPCC/ContainerizedMods/ContainerLogging.xml @@ -0,0 +1,495 @@ + + + + Logging em contêiner + + + Histórico de Logging + + Os logs de componentes do Bare-metal HPCC Systems são gravados em + arquivos persistentes no sistema de arquivos local. Em contraste, os logs + HPCC em contêiner são efêmeros e sua localização nem sempre é bem + definida. Os componentes do HPCC Systems fornecem logs informativos no + nível do aplicativo para fins de depuração de problemas, ações de + auditoria e monitoramento do progresso. + + Seguindo as metodologias em contêiner mais amplamente aceitas, as + informações de log de componentes do HPCC Systems são roteadas para os + fluxos de saída padrão em vez de arquivos locais. Em implantações em + contêiner, não há logs de componentes gravados em arquivos como nas + edições anteriores. + + Esses logs são gravados no fluxo de erro padrão (stderr). No nível + do nó, o conteúdo do erro padrão e dos fluxos de saída são redirecionados + para um local de destino por um mecanismo de contêiner. Em um ambiente + Kubernetes, o mecanismo de contêiner do Docker redireciona os fluxos para + um driver de log, que o Kubernetes configura para gravar em um arquivo no + formato JSON. Os logs são expostos pelo Kubernetes por meio do comando + "logs" apropriadamente chamado. + + Por exemplo: + + >kubectl logs myesp-6476c6659b-vqckq +>0000CF0F PRG INF 2020-05-12 17:10:34.910 1 10690 "HTTP First Line: GET / HTTP/1.1" +>0000CF10 PRG INF 2020-05-12 17:10:34.911 1 10690 "GET /, from 10.240.0.4" +>0000CF11 PRG INF 2020-05-12 17:10:34.911 1 10690 “TxSummary[activeReqs=22; rcv=5ms;total=6ms;]" + + É importante entender que esses logs são de natureza efêmera e podem + ser perdidos se o pod for despejado, o contêiner travar, o nó morrer etc. + Além disso, devido à natureza das soluções em contêiner, os logs + relacionados provavelmente se originam de vários locais e pode precisar + ser coletado e processado. É altamente recomendável desenvolver uma + estratégia de retenção e processamento com base em suas + necessidades. + + Muitas ferramentas estão disponíveis para ajudar a criar uma solução + apropriada com base em uma abordagem do tipo "faça você mesmo" ou em + recursos gerenciados disponíveis em provedores de nuvem. + + Para os ambientes mais simples, pode ser aceitável confiar no + processo padrão do Kubernetes que encaminha todo o conteúdo de + stdout/stderr para o arquivo. No entanto, à medida que a complexidade do + cluster aumenta ou a importância de reter o conteúdo dos logs aumenta, uma + arquitetura de log em nível de cluster deve ser empregada. + + O registro em nível de cluster para o cluster do HPCC Systems em + contêiner pode ser realizado incluindo um agente de registro em cada nó. A + tarefa de cada agente é expor os logs ou enviá-los por push para um + back-end de processamento de log. Os agentes de registro geralmente não + são fornecidos prontos para uso, mas há vários disponíveis, como o + Elasticsearch e o Stackdriver Logging. Vários provedores de nuvem oferecem + soluções integradas que coletam automaticamente todos os fluxos stdout/err + e fornecem armazenamento dinâmico e ferramentas analíticas poderosas, além + da capacidade de criar alertas personalizados com base em dados de + log. + + É sua responsabilidade determinar a solução apropriada para + processar os dados de log de streaming. + + + + Soluções de Processamento de Log + + Existem várias soluções de processamento de log disponíveis. Você + pode optar por integrar os dados de registro do HPCC Systems com qualquer + uma de suas soluções de registro existentes ou implementar outra + especificamente para os dados do HPCC Systems. A partir do HPCC Systems + versão 8.4, fornecemos uma solução de processamento de log leve e completa + para sua conveniência. Como afirmado existem várias soluções possíveis, + você deve escolher a opção que melhor atende às suas necessidades. As + seções a seguir examinarão duas soluções possíveis. + + + O chart Elastic4hpcclogs + + O HPCC Systems fornece um chart Helm gerenciado, + elastic4hpcclogs, que utiliza os charts Elastic + Stack Helm para Elastic Search, Filebeats e Kibana. Este gráfico + descreve uma instância local e mínima do Elastic Stack para + processamento de log de componentes do HPCC Systems. Depois de + implantados com êxito, os logs de componentes do HPCC produzidos no + mesmo namespace devem ser indexados automaticamente no ponto de + extremidade do Elastic Search. Os usuários podem consultar esses logs + emitindo consultas de API RESTful do Elastic Search ou por meio da + interface do usuário do Kibana (depois de criar um padrão de índice + simples). + + Pronto para uso, o Filebeat encaminha as entradas de log do + componente HPCC para um índice com nome genérico: 'hpcc-logs' - + <DATE_STAMP> e grava os dados de log em campos prefixados + 'hpcc.log.*'. Ele também agrega k8s, Docker e metadados do sistema para + ajudar o usuário a consultar as entradas de log de seu interesse. + + Um padrão de índice do Kibana é criado automaticamente com base no + layout de índice de batida de arquivo padrão. + + + + + Instalando o chart elastic4hpcclogs + + Instalar a solução simples fornecida é, como o nome indica, simples + e uma maneira conveniente de coletar e filtrar dados de log. Ele é + instalado por meio de nossos gráficos de leme do repositório HPCC Systems. + No diretório HPCC-platform/helm, o gráfico elastic4hpcclogs é fornecido + junto com os outros componentes da plataforma HPCC Systems. As próximas + seções mostrarão como instalar e configurar a solução Elastic stack + logging para HPCC Systems. + + + Adicionar o Repositório HPCC Systems + + O chart Elastic for HPCC Systems entregue pode ser encontrado no + repositório HPCC Systems Helm. Para buscar e implantar os gráficos + gerenciados do HPCC Systems, adicione o repositório do HPCC Systems + Helm, caso ainda não tenha feito isso: + + helm repo add hpcc https://hpcc-systems.github.io/helm-chart/ + + Depois que esse comando for concluído com êxito, o chart + elastic4hpcclogs estará acessível. + + Confirme se o chart apropriado foi puxado para baixo. + + helm list + + A emissão do comando helm list exibirá os gráficos e repositórios + do HPCC Systems disponíveis. O gráfico + elastic4hpcclogs está entre eles. + + + + + + Instalar o chart elastic4hpcc + + Instalar o chart elastic4hpcclogs utilizando + o seguinte comando: + + helm install <Instance_Name> hpcc/elastic4hpcclogs + + Forneça o nome que você deseja chamar sua instância do Elastic + Search para o parâmetro <Instance_Name>. Por exemplo, você poderia + chamar sua instância de "myelk" e, nesse caso, emitiria o comando de + instalação da seguinte maneira: + + helm install myelk hpcc/elastic4hpcclogs + + Após a conclusão bem-sucedida, a seguinte mensagem é + exibida: + + Thank you for installing elastic4hpcclogs. + A lightweight Elastic Search instance for HPCC component log processing. + +This deployment varies slightly from defaults set by Elastic, please review the effective values. + +PLEASE NOTE: Elastic Search declares PVC(s) which might require explicit manual removal + when no longer needed. + + + + + + + + + + + + + + + IMPORTANTE: O Elastic + Search declara PVC(s) que podem exigir remoção manual + explícita quando não forem mais necessários. Isso pode ser + particularmente importante para alguns provedores de nuvem que + podem acumular custos mesmo depois de não usar mais sua + instância. Você deve garantir que nenhum componente (como + PVCs) persista e continue acumulando custos. + + + + + + OBSERVAÇÃO: dependendo da versão do Kubernetes, os usuários podem + ser avisados sobre APIs obsoletas nos gráficos elásticos (ClusterRole e + ClusterRoleBinding estão obsoletos na v1.17+). As implantações baseadas + em Kubernetes < v1.22 não devem ser afetadas. + + + + Confirmar se seus Pods estão Prontos + + Confirme se os pods estão prontos. Ás vezes, após instalação, os + pods podem levar alguns segundos para aparecerem. Confirme se os pods + estão prontos antes de proceder. Para fazer isso, use o seguinte + comando: + + kubectl get pods + + Este comando retorna a seguinte informação, exibindo o status dos + pods. + + elasticsearch-master-0 1/1 Running 0 +myelk-filebeat-6wd2g 1/1 Running 0 +myelk-kibana-68688b4d4d-d489b 1/1 Running 0 + + + + Quando todos os pods estiverem indicando um estado 'ready' e + 'Running', incluindo os três componentes para filebeats, Elastic Search + e Kibana (destacado acima), você poderá prosseguir. + + + + Confirmar os Serviços Elastic + + Para garantir que os serviços Elastic estejam em execução, entre + com o seguinte comando: + + $ kubectl get svc + + Isso exibe as seguintes informações de confirmação: + + ... +elasticsearch-master ClusterIP 10.109.50.54 <none> 9200/TCP,9300/TCP 68m +elasticsearch-master-headless ClusterIP None <none> 9200/TCP,9300/TCP 68m +myelk-kibana LoadBalancer 10.110.129.199 localhost 5601:31465/TCP 68m +... + + Nota: O serviço myelk-kibana é declarado como LoadBalancer por + conveniência. + + + + Configurando Componentes do Elastic Stack + + Você pode precisar ou querer personalizar os componentes do + Elastic Stack. Os valores dos charts do componentes Elastic podem ser + substituídos como parte do comando de implantação do HPCC + Systems. + + Por exemplo: + + helm install myelk hpcc/elastic4hpcclogs --set elasticsearch.replicas=2 + + Consulte o repositório GitHub do Elastic Stack para obter a lista + completa de todas as opções do Filebeat, Elastic Search, LogStash e + Kibana com descrições. + + + + Use of HPCC Systems Component Logs in Kibana + + Uma vez ativado e em execução, você pode explorar e consultar os + logs de componentes do HPCC Systems na interface do usuário do Kibana. O + uso da interface do Kibana é bem suportado e documentado. Os padrões de + índice do Kibana são necessários para explorar os dados do Elastic + Search na interface do usuário do Kibana. A Elastic fornece explicações + detalhadas das informações necessárias para entender e utilizar + efetivamente a interface Elastic-Kibana. A documentação robusta do + Kibana deve ser consultada para obter mais informações sobre como usar a + interface do Kibana. Por favor, veja: + + https://www.elastic.co/ + + e + + https://www.elastic.co/elastic-stack/ + + Incluídos na documentação completa também estão vídeos de início + rápido e outros recursos úteis. + + + + + Azure AKS Insights + + O Azure AKS Insights é um recurso opcional projetado para ajudar a + monitorar o desempenho e a integridade de clusters baseados em Kubernetes. + Uma vez habilitado e associado um determinado AKS a um cluster do HPCC + Systems ativo, os logs do componente HPCC são capturados automaticamente + pelo Insights. Todos os dados STDERR/STDOUT são capturados e + disponibilizados para fins de monitoramento e/ou consulta. Como geralmente + acontece com os recursos do provedor de nuvem, o custo é uma consideração + importante e deve ser bem entendido antes da implementação. O conteúdo do + log é gravado no armazenamento de logs associado ao seu espaço de trabalho + do Log Analytics. + + + Habilitar Azure Insights + + A habilitação do Azure's Insights no cluster AKS de destino pode + ser feita no portal do Azure ou via CLI. Para obter documentação + detalhada do Azure: Habilite insights de contêiner: Enabling Azure's + Insights on the target AKS cluster can be done from the Azure portal or + via CLI. For detailed Azure documentation: Enable Container + insights: + + https://docs.microsoft.com/en-us/azure/azure-monitor/containers/container-insights-onboard + + + Portal Azure + + Para habilitar o insights do Azure no portal: + + + + Selecione cluster AKS de Destino + + + + Selecione Monitoring + + + + Selecione Insights + + + + Habilite - escolha ao workspace padrão + + + + + + Linha de Comando + + Para habilitar os Azure insights na linha de comando: + + Opcionalmente, crie o espaço de trabalho de análise de log + [espaço de trabalho padrão, caso contrário] + + Entre: + + az monitor log-analytics workspace create -g myresourcegroup -n myworkspace --query-access Enabled + + Habilitar no cluster AKS de destino (referência ao ID do recurso + do workspace da etapa anterior) + + az aks enable-addons -g myresourcegroup -n myaks -a monitoring --workspace-resource-id \ + "/subscriptions/xyz/resourcegroups/myresourcegroup/providers/ \ + microsoft.operationalinsights/workspaces/myworkspace" + + A interface do AKS Insights no Azure fornece visualizações de + métricas de integridade em nível de cluster/nó/contêiner centradas em + Kubernetes e links diretos para logs de contêiner por meio de + interfaces de "análise de log". Os logs podem ser consultados através + da linguagem de consulta “Kusto” (KQL). Consulte a documentação do + Azure para obter detalhes sobre como consultar os logs. + + Consulte a documentação do Azure para obter detalhes sobre como + consultar os logs. + + Exemplo de consulta KQL para buscar entradas de registro + "Transaction summary" de um contêiner ECLWatch: + + let ContainerIdList = KubePodInventory +| where ContainerName =~ 'xyz/myesp' +| where ClusterId =~ '/subscriptions/xyz/resourceGroups/xyz/providers/Microsoft. + ContainerService/managedClusters/aks-clusterxyz' +| distinct ContainerID; +ContainerLog +| where LogEntry contains "TxSummary[" +| where ContainerID in (ContainerIdList) +| project LogEntrySource, LogEntry, TimeGenerated, Computer, Image, Name, ContainerID +| order by TimeGenerated desc +| render table + + Amostra de saída + + + + Consultas mais complexas podem ser formuladas para buscar + informações específicas fornecidas em qualquer uma das colunas de log, + incluindo dados não formatados na mensagem de log. A interface do + Insights facilita a criação de alertas com base nessas consultas, que + podem ser usadas para acionar e-mails, SMS, execução de Logic App e + muitas outras ações. + + + + + + Controlando a saída de registro do HPCC Systems + + Os logs do HPCC Systems fornecem uma riqueza de informações que + podem ser usadas para benchmarking, auditoria, depuração, monitoramento, + etc. O tipo de informação fornecida nos logs e seu formato são controlados + trivialmente através da configuração padrão do Helm. Tenha em mente que no + modo de contêiner, cada linha de saída de log é passível de incorrer em um + custo dependendo do provedor e do plano que você possui e a verbosidade + deve ser cuidadosamente controlada usando as opções a seguir. Por padrão, + os logs de componentes não são filtrados e contêm as seguintes + colunas. + + Por padrão, os logs de componentes não são filtrados e contêm as + seguintes colunas: + + MessageID TargetAudience LogEntryClass JobID DateStamp TimeStamp ProcessId ThreadID QuotedLogMessage + + Os logs podem ser filtrados por TargetAudience, Category ou Detail + Level. Além disso, as colunas de saída podem ser configuradas. As + definições de configuração de registro podem ser aplicadas no nível global + ou de componente. + + + Target Audience Filtering + + Os públicos-alvo disponíveis incluem operador (OPR), usuário + (USR), programador (PRO), auditoria (ADT) ou todos. O filtro é + controlado pelo valor <section>.logging.audiences. O valor da + string é composto por códigos de 3 letras delimitados pelo operador de + agregação (+) ou pelo operador de remoção (-). + + Por exemplo, todas as saídas de log de componentes devem incluir + apenas mensagens do programador e do usuário: + + helm install myhpcc ./hpcc --set global.logging.audiences="PRO+USR" + + + + Filtragem de Categoria de Destino + + As categorias de destino disponíveis incluem desastre (DIS), erro + (ERR), informações (INF), aviso (WRN), progresso (PRO), métricas (MET). + O filtro de categoria (ou classe) é controlado pelo valor + <section>.logging.classes, composto por códigos de 3 letras + delimitados pelo operador de agregação (+) ou pelo operador de remoção + (-). + + Por exemplo, a saída do log da instância mydali para incluir todas + as classes, exceto o progresso: + + helm install myhpcc ./hpcc --set dali[0].logging.classes="ALL-PRO" --set dali[0].name="mydali" + + + + Log Detail Level Configuration + + A verbosidade da saída do log pode ser ajustada de "critical + messages only" (1) até "report all messages" (100). O nível de log + padrão é bastante alto (80) e deve ser ajustado de acordo. + + Por exemplo, a verbosidade deve ser média para todos os + componentes: + + helm install myhpcc ./hpcc --set global.logging.detail="50" + + + + Configuração da Coluna de Dados de Registro + + As colunas de dados de log disponíveis incluem messageid(MID), + público(AUD), class(CLS), date(DAT), time(TIM), node(NOD), + militime(MLT), microtime(MCT), nanotime(NNT) , processid(PID), + threadid(TID), job(JOB), use(USE), session(SES), code(COD), + component(COM), quotemessage(QUO), prefix(PFX), all(ALL) e padrão (STD). + A configuração das colunas (ou campos) de dados de log é controlada pelo + valor <section>.logging.fields, composto por códigos de 3 letras + delimitados pelo operador de agregação (+) ou pelo operador de remoção + (-). Por exemplo, todas as saídas de log de componentes devem incluir as + colunas padrão, exceto a coluna de ID do job: + + Por exemplo, todas as saídas de log de componentes devem incluir + as colunas padrão, exceto a coluna de ID do job: + + helm install myhpcc ./hpcc --set global.logging.fields="STD-JOB" + + O ajuste de valores de registro por componente pode exigir a + afirmação de vários valores específicos de componentes, o que pode ser + inconveniente de fazer por meio do parâmetro de linha de comando --set. + Nesses casos, um arquivo de valores personalizados pode ser usado para + definir todos os campos obrigatórios. + + Por exemplo, a instância do componente ESP 'eclwatch' deve gerar + um log mínimo: + + helm install myhpcc ./hpcc --set -f ./examples/logging/esp-eclwatch-low-logging-values.yaml + + + diff --git a/docs/PT_BR/ContainerizedHPCC/ContainerizedMods/CustomConfig.xml b/docs/PT_BR/ContainerizedHPCC/ContainerizedMods/CustomConfig.xml new file mode 100644 index 00000000000..1fea7e3485e --- /dev/null +++ b/docs/PT_BR/ContainerizedHPCC/ContainerizedMods/CustomConfig.xml @@ -0,0 +1,319 @@ + + + + Configurações Customizadas + + + Técnicas de Customização + + Nesta seção, nós vamos abordar a criação de uma configuração + customizada do arquivo YAML e do lançamento de uma plataforma HPCC + Systems® utilizando os configurações padrão mas + as customizações. Depois de entender os conceitos de deste capítulo, você + pode consultar o próximo para uma referência a todos as configurações de + valores de configuração. + + Há várias maneiras de personalizar uma implantação da plataforma. + Nós recomendamos o uso de métodos que permitem que você aproveite melhor o + configuração como práticas de código (CaC). A configuração como código é a + padrão de gerenciamento de arquivos de configuração em um sistema de + controle de versão ou repositório. + + A seguir uma lista de técnicas de customização: + + + + A primeira maneira de substituir uma configuração padrão é + através da linha de comando usando o parâmetro --set. + + Este é o mais fácil, mas o menos compatível com as diretrizes + CaC. Também é mais difícil de rastrear as alterações. + + + + A segunda maneira é modificando os valores padrão e salvando por + meio da seguinte linha de comando: + + helm show values hpcc/hpcc > myvalues.yaml + + Isso pode estar em conformidade com as diretrizes do CaC, se + você colocar esse arquivo sob controle de versão, mas dificulta a + utilização de uma nova configuração padrão quando uma estiver + disponível. + + + + A terceira maneira, é a que normalmente usamos. Usar o padrão + configuração, mais um arquivo YAML de personalização e o parâmetro -f + (ou parâmetro --values) para o comando helm. Isso usa o padrão + configuração e apenas substitui as configurações especificadas no + personalização YAML. Além disso, você pode passar vários arquivos YAML + no mesmo comando, se desejado. + + Para este tutorial, usaremos o terceiro método para levantar um + plataforma com todas as configurações padrão, mas adicionar algumas + personalizações. No primeiro exemplo, em vez de um Roxie, teremos + dois. No segundo exemplo, ele adicionará um segundo 10-way + Thor. + + + + + Criar um Custom Configuration Chart para Dois Roxies + + + + Se você ainda não adicionou o repositório HPCC Systems a sua + lista de repositórios do helm, adicione-o agora. + + helm repo add hpcc https://hpcc-systems.github.io/helm-chart/ + + Se você já adiciou, atualize para os últimos charts: + + helm repo update + + + + Crie um novo arquivo de texto, o nomeie para tworoxies.yaml e abra-o em um editor de + texto. + + Você pode usar qualquer editor de texto. + + + + Salve os valores padrão em um arquivo de texto: + + helm show values hpcc/hpcc > myvalues.yaml + + + + Abra o arquivo salvo (myvalues.yaml) em um editor de + texto. + + + + Copie toda seção roxie: e + cole dentro no novo arquivo tworoxies.yaml. + + + + Copie todo o conteúdo do novo arquivo tworoxies.yaml, exceto a + primeira linha (roxie:), e cole no final do arquivo. + + + + No primeiro bloco, edite o valor para name: e altere o valor para roxie2. + + + + No segundo bloco, edite o valor para prefix: e altere para roxie2. + + + + No segundo bloco, edite o valor para name: abaixo de services: e altere para roxie2. + + + + Salve o arquivo e feche o editor de texto. + + O arquivo tworoxies.yaml file deve se parecer com este + aqui: + + Nota: Os comentários foram + removidos para simplificar o examplo: + + roxie: +- name: roxie + disabled: false + prefix: roxie + services: + - name: roxie + servicePort: 9876 + listenQueue: 200 + numThreads: 30 + visibility: local + replicas: 2 + numChannels: 2 + serverReplicas: 0 + localAgent: false + traceLevel: 1 + topoServer: + replicas: 1 + +- name: roxie2 + disabled: false + prefix: roxie2 + services: + - name: roxie2 + servicePort: 9876 + listenQueue: 200 + numThreads: 30 + visibility: local + replicas: 2 + numChannels: 2 + serverReplicas: 0 + localAgent: false + traceLevel: 1 + topoServer: + replicas: 1 + + + + + Deploy utilizando um novo chart de + configuração personalizado. + + + + Abra uma janela de terminal e navegue para o diretório onde + você salvou o arquivo tworoxies.yaml. + + + + Faça o deploy do seu HPCC Systems Platform, adicionando a nova + configuração ao comando: + + helm install mycluster hpcc/hpcc -f tworoxies.yaml + + + + Após você confirmar que seu deploy está sendo executado, abra + o ECL Watch. + + Você deverá ver dois clusters Roxie disponíveis como Targets + -- roxie e roxie2. + + + + + + Crie um Novo Chart de Configuração para Dois Thors + + Você pode especificar mais de uma configuração de customização + repetindo o parâmetro -f. + + Por exemplo: + + helm install mycluster hpcc/hpcc -f tworoxies.yaml -f twothors.yaml + + Nesta seção, nós vamos adicionar um segundo Thor 10-way. + + + + Se você ainda não adicionou o repositório do HPCC Systems a + lista de repositórios helm, adicione agora.helm repo add hpcc https://hpcc-systems.github.io/helm-chart/ + + Se você já adicionou, atualize os últimos charts: + + helm repo update + + + + Crie um novo arquivo de texto e nomeie-o twothors.yaml, em seguida abra em um editor + de texto. + + Você pode usar qualquer editor de texto. + + + + Em um editor de texto, abra o arquivo de valores padrão que + você salvou anteriormente (myvalues.yaml). + + + + Copie por inteiro a seção thor: e cole no novo arquivo + twothors.yaml. + + + + Copie todo conteúdo para o novo arquivo twothors.yaml, exceto + a primeira linha (thor:), e cole no final do arquivo. + + + + No segundo bloco, edite o valor para name: e altere-o para thor10. + + + + No segundo bloco, edite o valor para prefix: e altere-o para thor10. + + + + No segundo bloco, edite o valor para numWorkers: e altere-o para 10. + + + + Salve o arquivo e feche o editor de texto. + + O resultado do arquivo do twothors.yaml deve se parecer + assim + + Nota: Os comentários foram + removidos para simplificar o exemplo: + + thor: +- name: thor + prefix: thor + numWorkers: 2 + maxJobs: 4 + maxGraphs: 2 +- name: thor10 + prefix: thor10 + numWorkers: 10 + maxJobs: 4 + maxGraphs: 2 + + + + Deploy utilizando o novo chart de + configuração personalizado. + + + + Abra uma janela de terminal e navegue para o diretório onde + você salvou o arquivo twothors.yaml. + + + + Faça o deploy do seu HPCC Systems Platform, adicionando a nova + configuração ao comando: + + # If you have previously stopped your cluster + +helm install mycluster hpcc/hpcc -f tworoxies.yaml -f twothors.yaml + +# To upgrade without stopping + +helm upgrade mycluster hpcc/hpcc -f tworoxies.yaml -f twothors.yaml + + + + + Após você confirmar que seu deploy está sendo executado, abra + o ECL Watch. + + Você deverá ver dois clusters Thor disponíveis como Targets -- + thor and thor10. + + + + + diff --git a/docs/PT_BR/ContainerizedHPCC/ContainerizedMods/LocalDeployment.xml b/docs/PT_BR/ContainerizedHPCC/ContainerizedMods/LocalDeployment.xml index 6af38a333b7..59192f7ea51 100644 --- a/docs/PT_BR/ContainerizedHPCC/ContainerizedMods/LocalDeployment.xml +++ b/docs/PT_BR/ContainerizedHPCC/ContainerizedMods/LocalDeployment.xml @@ -5,63 +5,79 @@ Deploy Local (Desenvolvimento e Teste) Embora haja muitas maneiras de instalar uma plataforma HPCC Systems de - nó único local, esta seção se concentra no uso do Docker Desktop. + nó único local, esta seção se concentra no uso local do Docker + Desktop. Pré-requisitos + + Todas ferramentas de terceiros devem ser 64-bits. - Adicionar repositório + Adicionar Repositório Para usar o helm charts do HPCC Systems, você deve adicioná-lo à lista de repositório do helm, conforme mostrado abaixo: helm repo add hpcc https://hpcc-systems.github.io/helm-chart/ - Reposta esperada: + Expected response: "hpcc" has been added to your repositories - Para atualizar os último charts: + To update to the latest charts: helm repo update - Resposta esperada: + You should update your local repo before any deployment to ensure + you have the latest code available. + + Expected response: - Update Complete. Happy Helming! + Hang tight while we grab the latest from your chart repositories... +...Successfully got an update from the "hpcc" chart repository +Update Complete. Happy Helming! - Iniciar um sistema padrão + Iniciar um Sistema Padrão O helm chart padrão inicia um sistema de teste simples com Dali, - ESP, eclccserver, duas filas eclagent (modo ROXIE e hThor) e uma fila + ESP, ECL CC Server, duas filas ECL Agent (modo ROXIE e hThor) e uma fila Thor. Para iniciar este sistema simples: - helm install mycluster hpcc/hpcc --version=8.2.2 + helm install mycluster hpcc/hpcc --version=8.6.14 - O argumento --version é opcional, mas - recomendado. Ele garante que você saiba a versão que você está instalando. - Se omitido, a última versão não-desenvolvimento será instalada. Este - exemplo usa 8.2.2, mas você deve usar a versão que deseja. - + + + Nota: + + + O argumento --version é opcional, mas recomendado. Ele garante + que você saiba a versão que você está instalando. Se omitido, a + última versão não-desenvolvimento será instalada. Este exemplo usa + 8.6.14, mas você deve usar a versão que deseja. + + + Resposta esperada: NAME: mycluster -LAST DEPLOYED: Tue Mar 23 13:26:55 2021 +LAST DEPLOYED: Tue Apr 5 14:45:08 2022 NAMESPACE: default STATUS: deployed REVISION: 1 TEST SUITE: None NOTES: -Thank you for installing the HPCC chart. +Thank you for installing the HPCC chart version 8.6.14 using image "hpccsystems/platform-core:8.6.14" +**** WARNING: The configuration contains ephemeral planes: [dali sasha dll data mydropzone debug] **** This chart has defined the following HPCC components: dali.mydali @@ -69,6 +85,7 @@ dfuserver.dfuserver eclagent.hthor eclagent.roxie-workunit eclccserver.myeclccserver +eclscheduler.eclscheduler esp.eclwatch esp.eclservices esp.eclqueries @@ -77,11 +94,19 @@ esp.sql2ecl esp.dfs roxie.roxie thor.thor +dali.sasha.coalescer sasha.dfurecovery-archiver sasha.dfuwu-archiver sasha.file-expiry sasha.wu-archiver + Observe o aviso sobre planos efêmeros. Isso ocorre porque essa + implantação criou armazenamento temporário e efêmero para uso. Quando o + cluster for desinstalado, o armazenamento não existirá mais. Isso é útil + para um teste rápido, mas para um trabalho mais complexo, você desejará um + armazenamento mais persistente. Isso é abordado em uma seção + posterior. + Para verificar o status: kubectl get pods @@ -125,7 +150,7 @@ thor-thoragent-56d788869f-7trxk 1/1 Running 0 2m6s< - Uso padrão do sistema + Acesso padrão do sistema Seu sistema agora está pronto para uso. O primeiro passo usual é abrir o ECL Watch. @@ -152,7 +177,8 @@ thor-thoragent-56d788869f-7trxk 1/1 Running 0 2m6s< NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE eclqueries LoadBalancer 10.108.171.35 localhost 8002:31615/TCP 2m6s eclservices ClusterIP 10.107.121.158 <none> 8010/TCP 2m6s -eclwatch LoadBalancer 10.100.81.69 localhost 8010:30173/TCP 2m6s +eclwatch LoadBalancer 10.100.81.69 localhost 8010:30173/TCP 2m6s esdl-sandbox LoadBalancer 10.100.194.33 localhost 8899:30705/TCP 2m6s kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 2m6s mydali ClusterIP 10.102.80.158 <none> 7070/TCP 2m6s @@ -163,12 +189,11 @@ sasha-wu-archiver ClusterIP 10.111.34.240 <none> 8877/TCP sql2ecl LoadBalancer 10.107.177.180 localhost 8510:30054/TCP 2m6s dfs LoadBalancer 10.100.52.9 localhost 8520:30184/TCP 2m6s - Observe que o serviço eclwatch - está sendo executado em localhost:8010. - Use esse endereço em seu navegador para acessar o ECL Watch. + Localize o serviço ECL Watch e identifique o EXTERNAL-IP e PORTA(S) + para eclwatch. Neste caso, é localhost:8010. - Dentro do ECL Watch, pressione o botão ECL e em seguida vá para a - aba Playground. + Abra um navegador e acesse o ECLWatch, pressione o botão ECL e + selecione a aba Playground. A partir daqui, você pode usar o ECL de exemplo ou inserir outras consultas de teste e escolher entre os clusters disponíveis para enviar @@ -176,13 +201,16 @@ dfs LoadBalancer 10.100.52.9 localhost 8520:30184/TCP - Terminar (Descomissionar) o sistema + Encerrar (Descomissionar) o Sistema Para verificar quais helm charts estão instalados atualmente, execute este comando: helm list + Isso exibe os gráficos instalados e seus nomes. Neste exemplo, + mycluster. + Para interromper os pods do HPCC Systems, use o helm para desinstalar: @@ -192,4 +220,260 @@ dfs LoadBalancer 10.100.52.9 localhost 8520:30184/TCP padrão e os volumes persistentes, também exclui o armazenamento usado. + + + Persistent Storage for a Local Deployment + + When running on a single-node test system such as Docker Desktop, + the default storage class normally means that all persistent volume claims + (PVCs) map to temporary local directories on the host machine. These are + typically removed when the cluster is stopped. This is fine for simple + testing but for any real application, you want persistent storage. + + To persist data with a Docker Desktop deployment, the first step is + to make sure the relevant directories exist: + + + + Create data directories using a terminal interface: + + For Windows, use this command: + + mkdir c:\hpccdata +mkdir c:\hpccdata\dalistorage +mkdir c:\hpccdata\hpcc-data +mkdir c:\hpccdata\debug +mkdir c:\hpccdata\queries +mkdir c:\hpccdata\sasha +mkdir c:\hpccdata\dropzone + + For macOS, use this command: + + mkdir -p /Users/myUser/hpccdata/{dalistorage,hpcc-data,debug,queries,sasha,dropzone} + + For Linux, use this command: + + mkdir -p ~/hpccdata/{dalistorage,hpcc-data,debug,queries,sasha,dropzone} + + + + Note: + + + If all of these directories do not exist, your pods may + not start. + + + + + + + Install the hpcc-localfile Helm chart. + + This chart creates persistent volumes based on host directories + you created earlier.# for a WSL2 deployment: +helm install hpcc-localfile hpcc/hpcc-localfile --set common.hostpath=/run/desktop/mnt/host/c/hpccdata + +# for a Hyper-V deployment: +helm install hpcc-localfile hpcc/hpcc-localfile --set common.hostpath=/c/hpccdata + +# for a macOS deployment: +helm install hpcc-localfile hpcc/hpcc-localfile --set common.hostpath=/Users/myUser/hpccdata + +# for a Linux deployment: +helm install hpcc-localfile hpcc/hpcc-localfile --set common.hostpath=~/hpccdata + + The --set common.hostpath= + option specifies the base directory: + + The path /run/desktop/mnt/host/c/hpccdata provides + access to the host file system for WSL2. + + The path /c/hpccdata provides + access to the host file system for Hyper-V. + + The path /Users/myUser/hpccdata + provides access to the host file system for Mac OSX. + + The path ~/hpccdata provides + access to the host file system for Linux. + + + + Note: + + + The value passed to --set common-hostpath is case + sensitive. + + + + + + + Copy the output from the helm install command in the previous + step from the word storage: to the + end, and save it to a text file. + + In this example, we will call the file + mystorage.yaml. The file should look similar to + this: + + storage: + planes: + - name: dali + pvc: dali-hpcc-localfile-pvc + prefix: "/var/lib/HPCCSystems/dalistorage" + category: dali + - name: dll + pvc: dll-hpcc-localfile-pvc + prefix: "/var/lib/HPCCSystems/queries" + category: dll + - name: sasha + pvc: sasha-hpcc-localfile-pvc + prefix: "/var/lib/HPCCSystems/sasha" + category: sasha + - name: debug + pvc: debug-hpcc-localfile-pvc + prefix: "/var/lib/HPCCSystems/debug" + category: debug + - name: data + pvc: data-hpcc-localfile-pvc + prefix: "/var/lib/HPCCSystems/hpcc-data" + category: data + - name: mydropzone + pvc: mydropzone-hpcc-localfile-pvc + prefix: "/var/lib/HPCCSystems/dropzone" + category: lz + +sasha: + wu-archiver: + plane: sasha + dfuwu-archiver: + plane: sasha + + + + If you are using Docker Desktop with Hyper-V, add the shared + data folder (in this example, C:\hpccdata) in Docker Desktop's + settings by pressing the Add button and typing c:\hpccdata. + + This is not needed in a MacOS + or WSL 2 environment. + + + + + + Finally, install the hpcc Helm chart, and provide a yaml file + that provides the storage information created by the previous + step. + + helm install mycluster hpcc/hpcc --version=8.6.14 -f mystorage.yaml + + + + Note: + + + The --version argument is optional, but recommended. It + ensures that you know which version you are installing. If + omitted, the latest non-development version is installed. This + example uses 8.6.14, but you should use the version you + want. + + + + + + + To test, open a browser and access ECLWatch, press the ECL + button, and select the Playground tab, then create some data files and + workunits by submitting to Thor some ECL code like the + following: + + LayoutPerson := RECORD + UNSIGNED1 ID; + STRING15 FirstName; + STRING25 LastName; +END; +allPeople := DATASET([ {1,'Fred','Smith'}, + {2,'Joe','Jones'}, + {3,'Jane','Smith'}],LayoutPerson); +OUTPUT(allPeople,,'MyData::allPeople',THOR,OVERWRITE); + + + + + Use the helm uninstall command to terminate your cluster, then + restart your deployment. + + + + Open ECL Watch and notice your workunits and logical files are + still there. + + + + + + + + Import: Storage Planes and How To Use Them + + Storage planes provide the flexibility to configure where the data + is stored within a deployed HPCC Systems platform, but it doesn't directly + address the question of how to get data onto the platform in the first + place. + + Containerized platforms support importing data in two ways: + + + + Upload a file to a Landing Zone and Import (Spray) + + + + Copy a file to a Storage Plane and access it directly + + + + Beginning with version 7.12.0, new ECL syntax was added to access + files directly from a storage plane. This is similar to the file:: syntax used to directly read files from a + physical machine, typically a landing zone. + + The new syntax is: + + ~plane::<storage-plane-name>::<path>::<filename>Where + the syntax of the path and filename are the same as used with the + file:: syntax. This includes requiring + uppercase letters to be quoted with a ^ symbol. For more details, see the + Landing Zone Files section of the ECL Language + Reference. + + If you have storage planes configured as in the previous section, + and you copy the originalperson file to + C:\hpccdata\hpcc-data\tutorial, you can + then reference the file using this syntax: + + '~plane::data::tutorial::originalperson' + + Note: + + + The originalperson file + is available from the HPCC Systems Web site + (https://cdn.hpccsystems.com/install/docs/3_8_0_8rc_CE/OriginalPerson). + + + + + + + + + + diff --git a/docs/PT_BR/ContainerizedHPCC/ContainerizedMods/PVCStorage.xml b/docs/PT_BR/ContainerizedHPCC/ContainerizedMods/PVCStorage.xml deleted file mode 100644 index 1292c144438..00000000000 --- a/docs/PT_BR/ContainerizedHPCC/ContainerizedMods/PVCStorage.xml +++ /dev/null @@ -1,211 +0,0 @@ - - - - Storage - - - Persistent Storage para um Deploy Local - - Ao executar em um sistema de teste de nó único, como Docker Desktop, - a classe de armazenamento padrão normalmente significa que todas as - declarações de volume persistente (PVCs) são mapeadas para diretórios - locais temporários na máquina host. Normalmente, eles são removidos quando - o cluster é interrompido. Isso é bom para teste, mas para qualquer - aplicativo real, você deseja o persistent storage. - - Para persistir os dados com o Docker Desktop, a primeira etapa é - garantir que os diretórios relevantes existam: - - - - Crie diretórios de dados usando uma interface de - terminal: - - Para Windows, use o seguinte comando: - - mkdir c:\hpccdata -mkdir c:\hpccdata\dalistorage -mkdir c:\hpccdata\queries -mkdir c:\hpccdata\sasha -mkdir c:\hpccdata\hpcc-data -mkdir c:\hpccdata\mydropzone - - Para macOS, use este comando: - - mkdir -p /Users/myUser/hpccdata/{dalistorage,queries,sasha,hpcc-data,mydropzone} - - Note:Se todos esses diretórios - não existirem, o cluster pode não iniciar. - - - - Faça o download do HPCC Platform Helm charts. - - Eles estão disponíveis no repositório do HPCC-Platform do HPCC Systems no GitHub - (https://github.com/hpcc-systems/HPCC-Platform). - - Se você quiser somente o helm charts, use o repositório - helm-chart - (https://github.com/hpcc-systems/helm-chart). - - - - Abra uma janela de prompt e navegue até diretório helm do reposítório que você acabou de fazer o - download. - - - - Instalar o Helm chart do diretório examples/local no seu repositório local. - - Este chart cria volumes persistents baseada nos diretórios host - que você criou anteriormente.# for a WSL2 deployment: -helm install hpcc-localfile examples/local/hpcc-localfile - --set common.hostpath=/run/desktop/mnt/host/c/hpccdata - -# for a Hyper-V deployment: -helm install hpcc-localfile examples/local/hpcc-localfile --set common.hostpath=/c/hpccdata - -# for a macOS deployment: -helm install hpcc-localfile examples/local/hpcc-localfile --set common.hostpath=/Users/myUser/hpccdata - - - A opção --set common.hostpath= - especifica o diretório base: - - O caminho /run/desktop/mnt/host/c/hpccdata fornece acesso - ao arquivo de host do sistema para WSL2. - - O caminho /c/hpccdata fornece - acesso ao arquivo de host do sistema para Hyper-V. - - O caminho /Users/myUser/hpccdata fornece acesso ao - arquivo de host do sistema para Mac OSX. - - Observação: O valor passado - para --set common-hostpath diferencia maiúsculas de minúsculas. - - - - Se você estiver usando o Docker Desktop com Hyper-V, adicione o - diretório dados compartilhado (no exemplo, C:\hpccdata) nas - configurações do Docker Desktop. - - Isto não é necessário em - ambientes macOS ou WSL 2. - - - - - - Por fim, instale o hpcc Helm chart e forneça um arquivo yaml que - fornece informações de armazenamento que usa os PVCs criados na etapa - anterior. - - O diretório de exemplo contém um arquivo yaml de amostra que - pode ser usado neste caso: - - helm install mycluster hpcc/ --version=8.2.2 - -f examples/local/values-localfile.yaml - - O argumento --version é opcional, mas - recomendado. Ele garante que você saiba a versão que você está - instalando. Se omitido, a última versão não-desenvolvimento será - instalada. Este exemplo usa 8.2.2, mas você deve usar a versão que - deseja. - - - - Para testar, crie alguns arquivos de dados e workunitso enviando - ao Thor algum código ECL como o seguinte: - - LayoutPerson := RECORD - UNSIGNED1 ID; - STRING15 FirstName; - STRING25 LastName; -END; -allPeople := DATASET([ {1,'Fred','Smith'}, - {2,'Joe','Jones'}, - {3,'Jane','Smith'}],LayoutPerson); -OUTPUT(allPeople,,'MyData::allPeople',THOR,OVERWRITE); - - - - - Use o comando helm uninstall para encerrar seus clusters e, em - seguida, reinicie. - - - - Abra o ECL Watch e observe que suas workunits e os arquivos - lógicos ainda estão lá. - - - - - - - - Importar: Storage Plans e como usá-los - - Os Storage Plans fornecem a flexibilidade para configurar onde os - dados são armazenados em uma plataforma HPCC Systems, mas não aborda - diretamente a questão de como obter dados para a plataforma em primeiro - lugar. - - As plataformas em contêiner oferecem suporte à importação de dados - de duas maneiras: - - - - Fazendo o upload para uma Landing Zone e o Spray (não - implementado na versão em contêiner) - - - - Copiando o Storage Plane e acessando diretamente - - - - A partir da versão 7.12.0, uma nova sintaxe ECL foi adicionada para - acessar arquivos diretamente de um plano de armazenamento. Isso é - semelhante a sintax file:: sintaxe usada - para ler arquivos diretamente de uma máquina física, normalmente uma - landing zone. - - A nova sintaxe é: - - ~plane::<storage-plane-name>::<path>::<filename>Onde - a sintaxe do caminho e do nome do arquivo são as mesmas usadas com a - sintex file::. Isso inclui exigir que - letras maiúsculas sejam citadas com um símbolo ^. Para maiores detalhes, - veja a seção Arquivos de Landing Zone Files no documento ECL - Language Reference. - - Se você o plano de armazenamento configurado conforme a seção - anterior e você copiou o arquivo originalperson para C:\hpccdata\hpcc-data\tutorial, então - referenciaro arquivo utilizando o sintax - - '~plane::data::tutorial::originalperson' - - Observação: O arquivo originalperson está disponível na página do HPCC - Systems - (https://cdn.hpccsystems.com/install/docs/3_8_0_8rc_CE/OriginalPerson) - - - - - - - - - - diff --git a/docs/PT_BR/DynamicESDL/DESDL-Mods/ESDLESPstruct.xml b/docs/PT_BR/DynamicESDL/DESDL-Mods/ESDLESPstruct.xml index cb8420080ae..3247e718646 100644 --- a/docs/PT_BR/DynamicESDL/DESDL-Mods/ESDLESPstruct.xml +++ b/docs/PT_BR/DynamicESDL/DESDL-Mods/ESDLESPstruct.xml @@ -1,7 +1,7 @@ - + ESP<emphasis role="bold">struct</emphasis> ESPstruct diff --git a/docs/PT_BR/DynamicESDL/DESDL-Mods/ESDLbooleanbool.xml b/docs/PT_BR/DynamicESDL/DESDL-Mods/ESDLbooleanbool.xml index 2ab79de7ffb..43dacc6e9ea 100644 --- a/docs/PT_BR/DynamicESDL/DESDL-Mods/ESDLbooleanbool.xml +++ b/docs/PT_BR/DynamicESDL/DESDL-Mods/ESDLbooleanbool.xml @@ -1,7 +1,7 @@ - + <emphasis role="bold">booleanbool</emphasis> <indexterm> <primary>boolean</primary> </indexterm> diff --git a/docs/PT_BR/ECLLanguageReference/ECLR_mods/BltInFunc-JOIN.xml b/docs/PT_BR/ECLLanguageReference/ECLR_mods/BltInFunc-JOIN.xml index a9775a8794a..e8b4afd2ac4 100644 --- a/docs/PT_BR/ECLLanguageReference/ECLR_mods/BltInFunc-JOIN.xml +++ b/docs/PT_BR/ECLLanguageReference/ECLR_mods/BltInFunc-JOIN.xml @@ -701,8 +701,9 @@ INDEX. Para ambos os tipos de junção KEYED (com chave), qualquer GROUPing - dos conjuntos de registro de base permanece inalterado. Consulte KEYED e - WILD para ler uma discussão sobre a filtragem de INDEX. + e/ou distribuição dos conjuntos de registro de base permanece inalterado. + Consulte KEYED e WILD para ler uma discussão sobre a filtragem de + INDEX. diff --git a/docs/PT_BR/ECLLanguageReference/ECLR_mods/ResrvdKywds-LIKELY.xml b/docs/PT_BR/ECLLanguageReference/ECLR_mods/ResrvdKywds-LIKELY.xml index 536bb62f97d..ed536559635 100644 --- a/docs/PT_BR/ECLLanguageReference/ECLR_mods/ResrvdKywds-LIKELY.xml +++ b/docs/PT_BR/ECLLanguageReference/ECLR_mods/ResrvdKywds-LIKELY.xml @@ -4,15 +4,13 @@ LIKELY e UNLIKELY - [attrname - := ] LIKELY + LIKELY LIKELY (filtercondition, [ likelihood ] ); - [attrname - := ] UNLIKELY + UNLIKELY UNLIKELY (filtercondition); diff --git a/docs/PT_BR/ECLStandardLibraryReference/SLR-Mods/Copy.xml b/docs/PT_BR/ECLStandardLibraryReference/SLR-Mods/Copy.xml index 55e79ca6100..b9e4dd60da3 100644 --- a/docs/PT_BR/ECLStandardLibraryReference/SLR-Mods/Copy.xml +++ b/docs/PT_BR/ECLStandardLibraryReference/SLR-Mods/Copy.xml @@ -214,8 +214,7 @@ A função Copy recebe um arquivo lógico e o copia para outro arquivo lógico. Isso pode ser feito no mesmo - cluster, para outro cluster ou para um cluster em um Dali totalmente - separado. + cluster, para outro. O destino não pode ser um arquivo externo. Exemplo: diff --git a/docs/PT_BR/HPCCClientTools/CT_Mods/CT_Comm_Line_DFU.xml b/docs/PT_BR/HPCCClientTools/CT_Mods/CT_Comm_Line_DFU.xml index 78d49baaf97..28cf87236b2 100644 --- a/docs/PT_BR/HPCCClientTools/CT_Mods/CT_Comm_Line_DFU.xml +++ b/docs/PT_BR/HPCCClientTools/CT_Mods/CT_Comm_Line_DFU.xml @@ -349,7 +349,17 @@ Opcional. O endereço IP da máquina de origem. Se omitido, as informações devem ser fornecidas pelo - parâmetro srcxml + parâmetro srcxml ou pelo parâmetro + srcplane. + + + + srcplane + + Opcional. O plano de armazenamendo da fonte + contendo o arquivo fonte. Observação: + srcplane não deve ser utilizado ao + mesmo tempo que o srcip. @@ -587,7 +597,14 @@ dstcluster=le_thor dstname=LE::imagedb overwrite=1 imageRecord := RECORD STRING filename; DATA image; //first 4 bytes contain the length of the image data -END; +END; +// using srcplane +dfuplus action=spray srcplane=lzstorageplane + srcfile=/var/lib/HPCCSystems/dropzone/largedata1 + dstname=mytest::test:spraytest + dstcluster=mydatastorageplane recordsize=58 overwrite=1 server=127.0.0.1:8010 + + @@ -622,7 +639,16 @@ END; Opcional. O endereço IP do nó do servidor. Se omitido, as informações devem ser fornecidas pelo parâmetro - dstxml . + dstxml ou pelo parâmetro + dstplane . + + + + dstplane + + Opcional. O plano de armazenamento do destino. + Observação: dstplane não deve ser usado + ao mesmo tempo que dstip. @@ -742,6 +768,12 @@ DATA image; //first 4 bytes contain the length of the image data dfuplus action=dspray srcname=le::imagedb dstip=10.150.51.26 dstfile=c:\export\ splitprefix=FILENAME,FILESIZE + +// using dstplane +dfuplus action=despray srcname=mytest::test:spraytest + dstplane=lzstorageplane + dstfile=mydespraytest server=127.0.0.1:8010 + diff --git a/docs/PT_BR/HPCCClientTools/CT_Mods/CT_ECL_CLI.xml b/docs/PT_BR/HPCCClientTools/CT_Mods/CT_ECL_CLI.xml index 2cca7c934e4..f8e052f8a27 100644 --- a/docs/PT_BR/HPCCClientTools/CT_Mods/CT_ECL_CLI.xml +++ b/docs/PT_BR/HPCCClientTools/CT_Mods/CT_ECL_CLI.xml @@ -1668,6 +1668,125 @@ ecl queries list roxie --target=roxie --show=A + + arquivos de consultas ecl + + arquivos de consultas ecl <target> + [<query>] + + The queries files command displays a list of the files currently + in use by the given query. If the query option is + omitted, it returns a list of files for all queries on the specified + target. + + Examplos: + + ecl queries files roxie myquery + + + Examplo de resultado: + + > ecl queries files roxie myquery + ------------------ +Query: myquery +Files used: + jd::subfile1, 100 bytes, 2 part(s) + jd::subfile2, 100 bytes, 2 part(s) + +SuperFiles used: + jd::mysuperfile + > jd::subfile2 + > jd::subfile1 + + + + + + + + + + Opções + + + + target + + Nome do cluster de destino em que a consulta será + publicada + + + + query + + Opcional. Nome da consulta para obter a lista de + arquivos utilizados. Se omitido, uma lista de arquivos para + cada consulta será retornado no destino + especificado. + + + + Opções + + + + -v, --verbose + + Informação adicional de rastreamento do + Output + + + + -s, --server + + Endereço IP ou hostname do servidor ESP executando + serviços ECL Watch + + + + --port + + Porta dos serviços do ECL Watch (Padrão é + 8010) + + + + -ssl + + Utilize SSL para segurar a conexão com o + servidor. + + + + -u, --username + + Nome do usuário (se necessário) + + + + -pw, --password + + Senha (se necessário) + + + + --wait-connect=<Ms> + + Timeout enquanto conecta ao servidor (em + milesegundos) + + + + --wait-read=<Secs> + + Timeout enquanto é feita leitura do socket (em + segundos) + + + + + + ecl queries copy @@ -1836,8 +1955,22 @@ ecl queries copy //192.168.1.10:8010/thor/findperson thor -ssl - Use o protocolo SSL para proteger a conexão com o - servidor. + Use o protocolo SSL para proteger a conexão com o(s) + servidor(es). + + + + --source-ssl + + Use SSL when connecting to the source (default if + --ssl is enabled) + + + + --source-no-ssl + + Não utilize SSL ao se conectar com a fonte (padrão é + --ssl DESabilitado) @@ -2004,8 +2137,22 @@ ecl queries copy-set roxie1 roxie2 --clone-active-state -ssl, --ssl - Use o protocolo SSL para proteger a conexão com o - servidor. + Use o protocolo SSL para proteger a conexão com o(s) + servidor(es). + + + + --source-ssl + + Utilize SSL quando conectar com a fonte (padrão é + --ssl desabilitado) + + + + --source-no-ssl + + Não utilize SSL ao conectar com a fonte (padrão é + --ssl é DESabilitado) @@ -4618,6 +4765,144 @@ ecl packagemap copy //192.168.0.100:8010/roxie/MyPkg roxie2 + + ecl roxie xref + + ecl roxie xref + <cluster> + + The roxie xref command returns + file information for the specified queries on the specified cluster. + If the queryids option is omitted, file + information about all queries is returned. The result is in XML + format. + + Examples: + + ecl roxie xref myroxie +ecl roxie xref myroxie --queryids=myquery.1,myotherquery.1 + + Example result: + + <QueryXrefInfo> + <Endpoint ep="192.168.56.1:9876"> + <Queries reporting="1"> + <Query id="myquery.1"/> + <SuperFile name="jd::mysuperfile"> + <File name="jd::subfile1"/> + <File name="jd::subfile2"/> + <Query id="myotherquery.1"/> + <SuperFile name="jd::myothersuperfile"> + <File name="jd::subfile1"/> + <File name="jd::subfile2"/> + <File name="jd::subfile3"/> + </Queries> + <Status>ok</Status> + </Endpoint> +</QueryXrefInfo> + + + + + + + + + + + ecl roxie xref + + Returns file information for the selected queries in + XML format. + + + + Options + + + + --check-all-nodes + + Gets query file information from all nodes. This can + be slow. + + + + --queryids=<csv list> + + The queries for which to get file information + (default is all queries) + + + + --wait=<ms> + + Max time to wait in milliseconds + + + + -v, --verbose + + Informação adicional de rastreamento do + Output + + + + -s, --server + + O endereço IP ou hostname do servidor ESP executando + os serviços do ECL Watch + + + + --port + + A porta do serviço ECL Watch (Padrão é 8010) + + + + --ssl + + Utilize SSL para segurar a conexão com o + servidor. + + + + --wait=<ms> + + Tempo máx de espera em milesegundos + + + + -u, --username + + O nome do usuário (se necessário) + + + + -pw, --password + + A senha (se necessário) + + + + --wait-connect=<Ms> + + Timeout durante a conexão ao servidor (em + milesegundos) + + + + --wait-read=<Secs> + + Timeout durante a leitura do socket (em + segundos) + + + + + + ecl bundle depends diff --git a/docs/PT_BR/HPCCClientTools/CT_Mods/CT_ECL_IDE.xml b/docs/PT_BR/HPCCClientTools/CT_Mods/CT_ECL_IDE.xml index 1cf2c4e7b14..c15fee365c3 100644 --- a/docs/PT_BR/HPCCClientTools/CT_Mods/CT_ECL_IDE.xml +++ b/docs/PT_BR/HPCCClientTools/CT_Mods/CT_ECL_IDE.xml @@ -1471,7 +1471,7 @@ definir margens, tamanho do papel, orientação etc. list Escolha entre as opções Enviar, Enviar selecionados, - Compilar ou Depura + ou Compilar Submit Envia o código ECL a ser compilado e executado no cluster de diff --git a/docs/PT_BR/HPCCClientTools/CT_Mods/CT_Overview.xml b/docs/PT_BR/HPCCClientTools/CT_Mods/CT_Overview.xml index cc6e9b355dc..d1fcf97ed58 100644 --- a/docs/PT_BR/HPCCClientTools/CT_Mods/CT_Overview.xml +++ b/docs/PT_BR/HPCCClientTools/CT_Mods/CT_Overview.xml @@ -136,6 +136,21 @@ Instale o software de ferramentas do cliente em sua máquina. + + + Nota: + + + Para grandes workunits ECL, o compilador de 32 bits + (eclcc) pode ficar sem memória mais rapidamente do que a + versão de 64 bits. Portanto, na maioria dos casos, você deve + instalar a versão de 64 bits. No entanto, para máquinas com + memória de 4 GB ou menos, você deve usar as ferramentas de + cliente de 32 bits. + + + + Windows: Execute o arquivo executável, p.ex.: @@ -157,8 +172,8 @@ sudo dpkg -i <deb filename> - Após a instalação do pacote, execute o seguinte comando para - atualizar as dependências: + Após instalar o pacote, execute o comando a seguir para + "arrumar" as dependências: sudo apt-get install -f diff --git a/docs/PT_BR/HPCCClientTools/CT_Mods/CT_Overview_withoutIDE.xml b/docs/PT_BR/HPCCClientTools/CT_Mods/CT_Overview_withoutIDE.xml index 1951ab15abf..dc76b2f9915 100644 --- a/docs/PT_BR/HPCCClientTools/CT_Mods/CT_Overview_withoutIDE.xml +++ b/docs/PT_BR/HPCCClientTools/CT_Mods/CT_Overview_withoutIDE.xml @@ -127,6 +127,21 @@ Instale o software de ferramentas do cliente em sua máquina. + + + Nota: + + + Para grandes workunits ECL, o compilador de 32 bits + (eclcc) pode ficar sem memória mais rapidamente do que a + versão de 64 bits. Portanto, na maioria dos casos, você deve + instalar a versão de 64 bits. No entanto, para máquinas com + memória de 4 GB ou menos, você deve usar as ferramentas de + cliente de 32 bits. + + + + Windows: Execute o arquivo executável, p.ex.: @@ -149,7 +164,7 @@ sudo dpkg -i <deb filename> Após instalar o pacote, execute o comando a seguir para - atualizar as dependências: + "arrumar" as dependências: sudo apt-get install -f diff --git a/docs/PT_BR/Installing_and_RunningTheHPCCPlatform/Inst-Mods/UserSecurityMaint.xml b/docs/PT_BR/Installing_and_RunningTheHPCCPlatform/Inst-Mods/UserSecurityMaint.xml index bf9dce90d11..a28d71669a2 100644 --- a/docs/PT_BR/Installing_and_RunningTheHPCCPlatform/Inst-Mods/UserSecurityMaint.xml +++ b/docs/PT_BR/Installing_and_RunningTheHPCCPlatform/Inst-Mods/UserSecurityMaint.xml @@ -1613,6 +1613,15 @@ Completo + + + WsLogAccess + + Habilita a função de leitura de logs dos + componentes + + Leitura + diff --git a/docs/PT_BR/Installing_and_RunningTheHPCCPlatform/Inst-Mods/hpcc_ldap.xml b/docs/PT_BR/Installing_and_RunningTheHPCCPlatform/Inst-Mods/hpcc_ldap.xml index 249cf9e1b94..d4cb1a63b8b 100644 --- a/docs/PT_BR/Installing_and_RunningTheHPCCPlatform/Inst-Mods/hpcc_ldap.xml +++ b/docs/PT_BR/Installing_and_RunningTheHPCCPlatform/Inst-Mods/hpcc_ldap.xml @@ -66,7 +66,7 @@ Verifique se eles não estão mais sendo executados. É possível - usar um comando único, como: sudo /opt/HPCCSystems/sbin/hpcc-run.sh -a hpcc-init status + usar um único comando, como: sudo /opt/HPCCSystems/sbin/hpcc-run.sh status diff --git a/docs/PT_BR/images/CL-Img01-1.jpg b/docs/PT_BR/images/CL-Img01-1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9e35a5ad27f38f88fba6772750fdb94b3eb62ef7 GIT binary patch literal 80166 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 Wy?=|-|6>aMzY_QV&m%#9rvD4}=_5)2 literal 0 HcmV?d00001 diff --git a/docs/PT_BR/images/CL-Img02-1.jpg b/docs/PT_BR/images/CL-Img02-1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ffaf4ddbe32b9abd02b21fa134f9d8f50d4ee237 GIT binary patch literal 127653 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 b&%di>|6MTr?_%lyu2}m2Z%O&>oB96*;7nud literal 0 HcmV?d00001 From cac8e1a6cc17e093e98b6a0aa6acc6d325c89413 Mon Sep 17 00:00:00 2001 From: Shamser Ahmed Date: Thu, 31 Mar 2022 10:17:40 +0100 Subject: [PATCH 06/24] HPCC-27399 Support file stats in CQ Signed-off-by: Shamser Ahmed --- .../activities/diskread/thdiskreadslave.cpp | 2 +- thorlcr/activities/fetch/thfetch.cpp | 24 +-- thorlcr/activities/fetch/thfetchslave.cpp | 33 ++-- thorlcr/activities/fetch/thfetchslave.ipp | 3 +- thorlcr/activities/indexread/thindexread.cpp | 29 ++- .../activities/indexread/thindexreadslave.cpp | 34 ++-- .../keyedjoin/thkeyedjoin-legacy.cpp | 1 - thorlcr/activities/keyedjoin/thkeyedjoin.cpp | 38 ++-- .../activities/keyedjoin/thkeyedjoinslave.cpp | 59 +++--- thorlcr/activities/thdiskbase.cpp | 21 +-- thorlcr/activities/thdiskbase.ipp | 3 +- thorlcr/activities/thdiskbaseslave.cpp | 29 +-- thorlcr/activities/thdiskbaseslave.ipp | 5 +- thorlcr/graph/thgraphmaster.cpp | 177 +++++++++++------- thorlcr/graph/thgraphmaster.ipp | 12 +- thorlcr/graph/thgraphslave.cpp | 12 ++ thorlcr/graph/thgraphslave.hpp | 3 +- 17 files changed, 251 insertions(+), 234 deletions(-) diff --git a/thorlcr/activities/diskread/thdiskreadslave.cpp b/thorlcr/activities/diskread/thdiskreadslave.cpp index 3e0c0df8b5c..3f86495263b 100644 --- a/thorlcr/activities/diskread/thdiskreadslave.cpp +++ b/thorlcr/activities/diskread/thdiskreadslave.cpp @@ -404,7 +404,7 @@ void CDiskRecordPartHandler::close(CRC32 &fileCRC) if (partStream) { closedPartFileStats.mergeStatistic(StNumDiskRowsRead, partStream->queryProgress()); - activity.mergeSubFileStats(partDesc, partStream); + activity.mergeFileStats(partDesc, partStream); partStream->stop(&fileCRC); } } diff --git a/thorlcr/activities/fetch/thfetch.cpp b/thorlcr/activities/fetch/thfetch.cpp index 77e7d8971d1..97f59eac25f 100644 --- a/thorlcr/activities/fetch/thfetch.cpp +++ b/thorlcr/activities/fetch/thfetch.cpp @@ -31,7 +31,7 @@ class CFetchActivityMaster : public CMasterActivity Owned mapping; MemoryBuffer offsetMapMb; SocketEndpoint *endpoints; - std::vector> subFileStats; + unsigned fileStatsTableStart = NotFound; protected: Owned fetchFile; @@ -54,7 +54,7 @@ class CFetchActivityMaster : public CMasterActivity { CMasterActivity::init(); OwnedRoxieString fname(helper->getFileName()); - fetchFile.setown(lookupReadFile(fname, AccessMode::readRandom, false, false, 0 != (helper->getFetchFlags() & FFdatafileoptional))); + fetchFile.setown(lookupReadFile(fname, AccessMode::readRandom, false, false, 0 != (helper->getFetchFlags() & FFdatafileoptional), reInit, diskReadRemoteStatistics, &fileStatsTableStart)); if (fetchFile) { if (isFileKey(fetchFile)) @@ -77,16 +77,6 @@ class CFetchActivityMaster : public CMasterActivity else if (encrypted) throw MakeActivityException(this, 0, "File '%s' was published as encrypted but no encryption key provided", fetchFile->queryLogicalName()); IDistributedSuperFile *super = fetchFile->querySuperFile(); - unsigned numsubs = super?super->numSubFiles(true):0; - - /* JCS->SHAMSER - kludge for now, don't add more than max - * But it means updateFileReadCostStats will not be querying the correct files, - * if the file varies per CQ execution (see other notes in updateFileReadCostStats) - */ - for (unsigned i=subFileStats.size(); iqueryLogicalName(), *fileDesc, container.queryJob().queryUserDescriptor(), container.queryJob().querySlaveGroup(), container.queryLocalOrGrouped(), false, NULL, super)); mapping->serializeFileOffsetMap(offsetMapMb); } @@ -110,16 +100,20 @@ class CFetchActivityMaster : public CMasterActivity } if (!container.queryLocalOrGrouped()) dst.append((int)mpTag); + dst.append(fileStatsTableStart); } virtual void deserializeStats(unsigned node, MemoryBuffer &mb) override { CMasterActivity::deserializeStats(node, mb); - for (auto &stats: subFileStats) - stats->deserialize(node, mb); + unsigned numFilesToRead; + mb.read(numFilesToRead); + assertex(numFilesToRead<=fileStats.size()); + for (unsigned i=0; ideserialize(node, mb); } virtual void done() override { - updateFileReadCostStats(subFileStats); + updateFileReadCostStats(); CMasterActivity::done(); } }; diff --git a/thorlcr/activities/fetch/thfetchslave.cpp b/thorlcr/activities/fetch/thfetchslave.cpp index e64c389c205..f4195dc3d5b 100644 --- a/thorlcr/activities/fetch/thfetchslave.cpp +++ b/thorlcr/activities/fetch/thfetchslave.cpp @@ -262,29 +262,25 @@ class CFetchStream : public IRowStream, implements IStopInput, implements IFetch } return NULL; } - virtual void getFileStats(CRuntimeStatisticCollection & stats) override + virtual void getFileStats(CRuntimeStatisticCollection & stats, std::vector> & fileStats, unsigned fileTableStart) override { for (unsigned f=0; f> & subFileStats) override - { - if (subFileStats.size()>0) + if(!fileStats.empty()) { ISuperFileDescriptor *super = parts.item(0).queryOwner().querySuperFileDescriptor(); - dbgassertex(super); for (unsigned f=0; fmapSubPart(part.queryPartIndex(), subfile, lnum)) - { - IFileIO *file = fPosMultiPartTable[f].file; - mergeStats(*subFileStats[subfile], file); - } + if (super && super->mapSubPart(part.queryPartIndex(), subfile, lnum)) + mergeStats(*fileStats[fileTableStart+subfile], file); + else + mergeStats(*fileStats[fileTableStart], file); } } } @@ -307,7 +303,7 @@ class CFetchSlaveBase : public CSlaveActivity, implements IFetchHandler MemoryBuffer offsetMapBytes; Owned eexp; Owned keyRowAllocator; - std::vector> subFileStats; + unsigned fileTableStart = NotFound; protected: Owned fetchDiskRowIf; @@ -368,6 +364,7 @@ class CFetchSlaveBase : public CSlaveActivity, implements IFetchHandler if (!container.queryLocalOrGrouped()) mptag = container.queryJobChannel().deserializeMPTag(data); + data.read(fileTableStart); files = parts.ordinality(); if (files) { @@ -387,9 +384,7 @@ class CFetchSlaveBase : public CSlaveActivity, implements IFetchHandler } } ISuperFileDescriptor *super = parts.item(0).queryOwner().querySuperFileDescriptor(); - if (super) - for (unsigned i=0; iquerySubFiles():0, diskReadRemoteStatistics); } unsigned encryptedKeyLen; @@ -575,12 +570,10 @@ class CFetchSlaveBase : public CSlaveActivity, implements IFetchHandler virtual void serializeStats(MemoryBuffer &mb) override { if (fetchStream) - { - fetchStream->getFileStats(stats); - fetchStream->getSubFileStats(subFileStats); - } + fetchStream->getFileStats(stats, fileStats, fileTableStart); PARENT::serializeStats(mb); - for (auto &stats: subFileStats) + mb.append((unsigned)fileStats.size()); + for (auto &stats: fileStats) stats->serialize(mb); } virtual void onLimitExceeded() = 0; diff --git a/thorlcr/activities/fetch/thfetchslave.ipp b/thorlcr/activities/fetch/thfetchslave.ipp index 161a08eeb2a..7b801120223 100644 --- a/thorlcr/activities/fetch/thfetchslave.ipp +++ b/thorlcr/activities/fetch/thfetchslave.ipp @@ -37,8 +37,7 @@ interface IFetchStream : extends IInterface virtual IFileIO *getPartIO(unsigned part) = 0; virtual StringBuffer &getPartName(unsigned part, StringBuffer &out) = 0; virtual void abort() = 0; - virtual void getFileStats(CRuntimeStatisticCollection & stats) = 0; - virtual void getSubFileStats(std::vector> & subFileStats) = 0; + virtual void getFileStats(CRuntimeStatisticCollection & stats, std::vector> & fileStats, unsigned fileTableStart) = 0; }; IFetchStream *createFetchStream(CSlaveActivity &owner, IThorRowInterfaces *keyRowIf, IThorRowInterfaces *fetchRowIf, bool &abortSoon, const char *logicalFilename, CPartDescriptorArray &parts, unsigned offsetCount, size32_t offsetMapSz, const void *offsetMap, IFetchHandler *iFetchHandler, mptag_t tag, IExpander *eexp=NULL); diff --git a/thorlcr/activities/indexread/thindexread.cpp b/thorlcr/activities/indexread/thindexread.cpp index e3cdd1fc283..625ae2eb937 100644 --- a/thorlcr/activities/indexread/thindexread.cpp +++ b/thorlcr/activities/indexread/thindexread.cpp @@ -36,7 +36,7 @@ class CIndexReadBase : public CMasterActivity bool localKey = false; bool partitionKey = false; StringBuffer fileName; - std::vector> subIndexFileStats; + unsigned fileStatsTableStart = NotFound; rowcount_t aggregateToLimit() { @@ -222,7 +222,7 @@ class CIndexReadBase : public CMasterActivity StringBuffer expandedFileName; queryThorFileManager().addScope(container.queryJob(), helperFileName, expandedFileName); fileName.set(expandedFileName); - Owned index = lookupReadFile(helperFileName, AccessMode::readRandom, false, false, 0 != (TIRoptional & indexBaseHelper->getFlags())); + Owned index = lookupReadFile(helperFileName, AccessMode::readRandom, false, false, 0 != (TIRoptional & indexBaseHelper->getFlags()), reInit, indexReadActivityStatistics, &fileStatsTableStart); if (index && (0 == index->numParts())) // possible if superfile index.clear(); if (index) @@ -235,26 +235,15 @@ class CIndexReadBase : public CMasterActivity if (container.queryLocalData() && !localKey) throw MakeActivityException(this, 0, "Index Read cannot be LOCAL unless supplied index is local"); + IDistributedSuperFile *super = index->querySuperFile(); nofilter = 0 != (TIRnofilter & indexBaseHelper->getFlags()); if (localKey) nofilter = true; else { - IDistributedSuperFile *super = index->querySuperFile(); IDistributedFile *sub = super ? &super->querySubFile(0,true) : index.get(); if (sub && 1 == sub->numParts()) nofilter = true; - if (super) - { - unsigned numSubFiles = super->numSubFiles(true); - - /* JCS->SHAMSER - kludge for now, don't add more than max - * But it means updateFileReadCostStats will not be querying the correct files, - * if the file varies per CQ execution (see other notes in updateFileReadCostStats) - */ - for (unsigned i=subIndexFileStats.size(); igetDiskFormatCrc(), indexBaseHelper->queryDiskRecordSize(), indexBaseHelper->getProjectedFormatCrc(), indexBaseHelper->queryProjectedDiskRecordSize(), true); @@ -290,7 +279,10 @@ class CIndexReadBase : public CMasterActivity ForEachItemIn(p2, parts) partNumbers.append(parts.item(p2).queryPartIndex()); if (partNumbers.ordinality()) + { fileDesc->serializeParts(dst, partNumbers); + dst.append(fileStatsTableStart); + } } virtual void abort() override { @@ -300,12 +292,15 @@ class CIndexReadBase : public CMasterActivity virtual void deserializeStats(unsigned node, MemoryBuffer &mb) override { CMasterActivity::deserializeStats(node, mb); - for (auto &indexFileStats: subIndexFileStats) - indexFileStats->deserialize(node, mb); + unsigned numFilesToRead; + mb.read(numFilesToRead); + assertex(numFilesToRead<=fileStats.size()); + for (unsigned i=0; ideserialize(node, mb); } virtual void done() override { - updateFileReadCostStats(subIndexFileStats); + updateFileReadCostStats(); CMasterActivity::done(); } }; diff --git a/thorlcr/activities/indexread/thindexreadslave.cpp b/thorlcr/activities/indexread/thindexreadslave.cpp index e7a3c3c64b6..cc620fdc2e3 100644 --- a/thorlcr/activities/indexread/thindexreadslave.cpp +++ b/thorlcr/activities/indexread/thindexreadslave.cpp @@ -76,7 +76,7 @@ class CIndexReadSlaveBase : public CSlaveActivity rowcount_t rowLimit = RCMAX; bool useRemoteStreaming = false; Owned lazyIFileIO; - std::vector> subIndexFileStats; + unsigned fileTableStart = NotFound; template class CCaptureIndexStats @@ -358,15 +358,19 @@ class CIndexReadSlaveBase : public CSlaveActivity else return nullptr; } - void mergeSubFileStats(IPartDescriptor *partDesc, IFileIO *partIO) + void mergeFileStats(IPartDescriptor *partDesc, IFileIO *partIO) { - if (subIndexFileStats.size()>0) + if (fileStats.size()>0) { ISuperFileDescriptor * superFDesc = partDesc->queryOwner().querySuperFileDescriptor(); - dbgassertex(superFDesc); - unsigned subfile, lnum; - if(superFDesc->mapSubPart(partDesc->queryPartIndex(), subfile, lnum)) - mergeStats(*subIndexFileStats[subfile], partIO); + if (superFDesc) + { + unsigned subfile, lnum; + if(superFDesc->mapSubPart(partDesc->queryPartIndex(), subfile, lnum)) + mergeStats(*fileStats[fileTableStart+subfile], partIO); + } + else + mergeStats(*fileStats[fileTableStart], partIO); } } void updateStats() @@ -375,7 +379,7 @@ class CIndexReadSlaveBase : public CSlaveActivity { mergeStats(stats, lazyIFileIO); if (currentPartquerySubFiles(); - for (unsigned i=0; igetFlags() & TIRusesblob)) && !localMerge) { if (!inChildQuery()) @@ -692,6 +691,8 @@ class CIndexReadSlaveBase : public CSlaveActivity } } } + data.read(fileTableStart); + setupSpace4FileStats(fileTableStart, reInit, super!=nullptr, super?super->querySubFiles():0, indexReadActivityStatistics); } } // IThorDataLink @@ -727,8 +728,9 @@ class CIndexReadSlaveBase : public CSlaveActivity { stats.setStatistic(StNumRowsProcessed, progress); PARENT::serializeStats(mb); - for (auto &indexFileStats: subIndexFileStats) - indexFileStats->serialize(mb); + mb.append((unsigned)fileStats.size()); + for (auto &stats: fileStats) + stats->serialize(mb); } virtual void done() override { diff --git a/thorlcr/activities/keyedjoin/thkeyedjoin-legacy.cpp b/thorlcr/activities/keyedjoin/thkeyedjoin-legacy.cpp index 5b1ae8fd7d0..7b1292e0916 100644 --- a/thorlcr/activities/keyedjoin/thkeyedjoin-legacy.cpp +++ b/thorlcr/activities/keyedjoin/thkeyedjoin-legacy.cpp @@ -64,7 +64,6 @@ class CKeyedJoinMaster : public CMasterActivity unsigned keyReadWidth = (unsigned)container.queryJob().getWorkUnitValueInt("KJKRR", 0); if (!keyReadWidth || keyReadWidth>container.queryJob().querySlaves()) keyReadWidth = container.queryJob().querySlaves(); - initMb.clear(); initMb.append(indexFileName.get()); diff --git a/thorlcr/activities/keyedjoin/thkeyedjoin.cpp b/thorlcr/activities/keyedjoin/thkeyedjoin.cpp index 4b8cf31ec2e..f32b26d9b37 100644 --- a/thorlcr/activities/keyedjoin/thkeyedjoin.cpp +++ b/thorlcr/activities/keyedjoin/thkeyedjoin.cpp @@ -41,7 +41,8 @@ class CKeyedJoinMaster : public CMasterActivity bool remoteKeyedFetch = false; bool assumePrimary = false; unsigned totalIndexParts = 0; - std::vector> fileStats; + unsigned indexFileStatsTableEntry = NotFound; + unsigned dataFileStatsTableEntry = NotFound; // CMap contains mappings and lists of parts for each slave class CMap @@ -303,28 +304,20 @@ class CKeyedJoinMaster : public CMasterActivity totalIndexParts = 0; Owned dataFile; - Owned indexFile = lookupReadFile(indexFileName, AccessMode::readRandom, false, false, 0 != (helper->getJoinFlags() & JFindexoptional)); + Owned indexFile = lookupReadFile(indexFileName, AccessMode::readRandom, false, false, 0 != (helper->getJoinFlags() & JFindexoptional), true, indexReadActivityStatistics, &indexFileStatsTableEntry); if (indexFile) { if (!isFileKey(indexFile)) throw MakeActivityException(this, TE_FileTypeMismatch, "Attempting to read flat file as an index: %s", indexFileName.get()); IDistributedSuperFile *superIndex = indexFile->querySuperFile(); - // One entry for each subfile (unless it is not a superfile => then add one entry for index data file stats) - unsigned numSuperIndexSubs = superIndex?superIndex->numSubFiles(true):1; - - /* JCS->SHAMSER - kludge for now, don't add more than max - * But it means updateFileReadCostStats will not be querying the correct files, - * if the file varies per CQ execution (see other notes in updateFileReadCostStats) - */ - for (unsigned i=fileStats.size(); inumSubFiles(true):1; if (helper->diskAccessRequired()) { OwnedRoxieString fetchFilename(helper->getFileName()); if (fetchFilename) { - dataFile.setown(lookupReadFile(fetchFilename, AccessMode::readRandom, false, false, 0 != (helper->getFetchFlags() & FFdatafileoptional))); + dataFile.setown(lookupReadFile(fetchFilename, AccessMode::readRandom, false, false, 0 != (helper->getFetchFlags() & FFdatafileoptional), true, diskReadRemoteStatistics, &dataFileStatsTableEntry)); if (dataFile) { if (isFileKey(dataFile)) @@ -362,16 +355,6 @@ class CKeyedJoinMaster : public CMasterActivity remoteKeyedFetch = false; } dataMap.map(*this, dataFile, false, getOptBool("allLocalFetchParts")); - IDistributedSuperFile *super = dataFile->querySuperFile(); - // One entry for each subfile (unless it is not a superfile => then have 1 entry for data file stats) - unsigned numsubs = super?super->numSubFiles(true):1; - - /* JCS->SHAMSER - kludge for now, don't add more than max - * But it means updateFileReadCostStats will not be querying the correct files, - * if the file varies per CQ execution (see other notes in updateFileReadCostStats) - */ - for (unsigned i=fileStats.size(); ideserialize(node, mb); + unsigned numFilesToRead; + mb.read(numFilesToRead); + assertex(fileStats.size()>=numFilesToRead); + for (unsigned i=0; ideserialize(node, mb); } virtual void done() override { - updateFileReadCostStats(fileStats); + updateFileReadCostStats(); CMasterActivity::done(); } }; diff --git a/thorlcr/activities/keyedjoin/thkeyedjoinslave.cpp b/thorlcr/activities/keyedjoin/thkeyedjoinslave.cpp index c761f878b98..af7608bf440 100644 --- a/thorlcr/activities/keyedjoin/thkeyedjoinslave.cpp +++ b/thorlcr/activities/keyedjoin/thkeyedjoinslave.cpp @@ -1243,7 +1243,7 @@ class CKeyedJoinSlave : public CSlaveActivity, implements IJoinProcessor, implem if (limiter) limiter->dec(); // unblocks any requests to start lookup threads } - virtual void getSubFileStats(std::vector> & subFileStats) = 0; + virtual void getFileStats(std::vector> & fileStats, unsigned startOffset) = 0; }; class CKeyLookupLocalBase : public CLookupHandler @@ -1392,7 +1392,7 @@ class CKeyedJoinSlave : public CSlaveActivity, implements IJoinProcessor, implem } processRows(processing, partNo, keyManager); } - virtual void getSubFileStats(std::vector> & subFileStats) override + virtual void getFileStats(std::vector> & fileStats, unsigned startOffset) override { for (size_t i=0; imergeStats(*subFileStats[subfile]); + keyManager->mergeStats(*fileStats[startOffset+subfile]); } else - keyManager->mergeStats(*subFileStats[0]); + keyManager->mergeStats(*fileStats[startOffset]); } } }; @@ -1442,7 +1442,7 @@ class CKeyedJoinSlave : public CSlaveActivity, implements IJoinProcessor, implem } processRows(processing, 0, keyManager); } - virtual void getSubFileStats(std::vector> & subFileStats) override + virtual void getFileStats(std::vector> & fileStats, unsigned startOffset) override { if (keyManager) { @@ -1451,10 +1451,10 @@ class CKeyedJoinSlave : public CSlaveActivity, implements IJoinProcessor, implem if (isSuper) { unsigned subfile = subFileNum[i]; - (*keyManager).mergeStats(*subFileStats[subfile]); + (*keyManager).mergeStats(*fileStats[startOffset+subfile]); } else - (*keyManager).mergeStats(*subFileStats[0]); + (*keyManager).mergeStats(*fileStats[startOffset]); } } } @@ -1547,7 +1547,7 @@ class CKeyedJoinSlave : public CSlaveActivity, implements IJoinProcessor, implem for (auto &h: handles) h = 0; } - virtual void getSubFileStats(std::vector> & subFileStats) override + virtual void getFileStats(std::vector> & fileStats, unsigned startOffset) override { /* Note: currently, stats from remote file not tracked */ } @@ -1871,7 +1871,7 @@ class CKeyedJoinSlave : public CSlaveActivity, implements IJoinProcessor, implem diskSeeks++; } } - virtual void getSubFileStats(std::vector> & subFileStats) override + virtual void getFileStats(std::vector> & fileStats, unsigned startOffset) override { for (size_t i=0; iend(); } } - void getSubFileStats(std::vector> & subFileStats) + void getFileStats(std::vector> & fileStats, unsigned startOffset) { ForEachItemIn(h, handlers) - handlers.item(h)->getSubFileStats(subFileStats); + handlers.item(h)->getFileStats(fileStats, startOffset); } }; @@ -2297,8 +2297,8 @@ class CKeyedJoinSlave : public CSlaveActivity, implements IJoinProcessor, implem CriticalSection fetchFileCrit; std::vector openFetchParts; - std::vector> dataFileStats; - std::vector> indexFileStats; + unsigned indexFileStatsTableEntry = NotFound; + unsigned dataFileStatsTableEntry = NotFound; PartIO getFetchPartIO(unsigned partNo, unsigned copy, bool compressed, bool encrypted) { @@ -3044,8 +3044,6 @@ class CKeyedJoinSlave : public CSlaveActivity, implements IJoinProcessor, implem dataPartToSlaveMap.clear(); tlkKeyManagers.kill(); partitionKey = false; - dataFileStats.clear(); - indexFileStats.clear(); } // decode data from master. NB: can be resent and differ if in global loop data.read(indexName); @@ -3117,14 +3115,11 @@ class CKeyedJoinSlave : public CSlaveActivity, implements IJoinProcessor, implem } else if (container.queryLocalData()) totalIndexParts = numIndexParts; // will be same unless local data only + data.read(indexFileStatsTableEntry); } ISuperFileDescriptor *superFdesc = numIndexParts?allIndexParts.item(0).queryOwner().querySuperFileDescriptor():nullptr; - // One entry in indexFileStats vector for each index file's subfile - // Unless it is not a superfile => then the indexFileStats[0] will be used for the index file's stats - unsigned numIndexSubFiles = superFdesc?superFdesc->querySubFiles():1; - for (unsigned n=0; nquerySubFiles():0; + setupSpace4FileStats(indexFileStatsTableEntry, true, superFdesc!=nullptr, numIndexSubFiles, indexReadActivityStatistics); setupLookupHandlers(keyLookupHandlers, totalIndexParts, superFdesc, localIndexParts, indexPartToSlaveMap, localKey, forceRemoteKeyedLookup ? ht_remotekeylookup : ht_localkeylookup, ht_remotekeylookup); data.read(totalDataParts); if (totalDataParts) @@ -3160,11 +3155,6 @@ class CKeyedJoinSlave : public CSlaveActivity, implements IJoinProcessor, implem e->index = f; } std::sort(globalFPosToSlaveMap.begin(), globalFPosToSlaveMap.end(), [](const FPosTableEntry &a, const FPosTableEntry &b) { return a.base < b.base; }); - // One entry in dataFileStats vector for each data file's subfile - // Unless it is not a superfile => then the dataFileStats[0] will be used for the data file's stats - unsigned numDataSubFiles = superFdesc?superFdesc->querySubFiles():1; - for (unsigned n=0; nquerySubFiles():0; + setupSpace4FileStats(dataFileStatsTableEntry, true, superFdesc!=nullptr, numDataSubFiles, diskReadRemoteStatistics); } } ActPrintLog("Remote Keyed Lookups = %s (forced = %s), remote fetch = %s (forced = %s)", boolToStr(remoteKeyedLookup), boolToStr(forceRemoteKeyedLookup), boolToStr(remoteKeyedFetch), boolToStr(forceRemoteKeyedFetch)); @@ -3799,14 +3792,12 @@ class CKeyedJoinSlave : public CSlaveActivity, implements IJoinProcessor, implem } virtual void serializeStats(MemoryBuffer &mb) override { - keyLookupHandlers.getSubFileStats(indexFileStats); - if (dataFileStats.size()>0) - fetchLookupHandlers.getSubFileStats(dataFileStats); + keyLookupHandlers.getFileStats(fileStats, indexFileStatsTableEntry); + fetchLookupHandlers.getFileStats(fileStats, dataFileStatsTableEntry); PARENT::serializeStats(mb); - for (auto &stats: indexFileStats) - stats->serialize(mb); - for (auto &stats: dataFileStats) + mb.append((unsigned)fileStats.size()); + for (auto &stats: fileStats) stats->serialize(mb); } }; diff --git a/thorlcr/activities/thdiskbase.cpp b/thorlcr/activities/thdiskbase.cpp index d09e9376535..ef14ed48763 100644 --- a/thorlcr/activities/thdiskbase.cpp +++ b/thorlcr/activities/thdiskbase.cpp @@ -52,7 +52,7 @@ void CDiskReadMasterBase::init() bool temp = 0 != (TDXtemporary & helper->getFlags()); bool jobTemp = 0 != (TDXjobtemp & helper->getFlags()); bool opt = 0 != (TDRoptional & helper->getFlags()); - file.setown(lookupReadFile(helperFileName, AccessMode::readSequential, jobTemp, temp, opt)); + file.setown(lookupReadFile(helperFileName, AccessMode::readSequential, jobTemp, temp, opt, reInit, diskReadRemoteStatistics, &fileStatsTableStart)); if (file) { if (file->isExternal() && (helper->getFlags() & TDXcompress)) @@ -79,15 +79,6 @@ void CDiskReadMasterBase::init() subfileLogicalFilenames.append(subfile.queryLogicalName()); } } - if (0==(helper->getFlags() & TDXtemporary)) - { - /* JCS->SHAMSER - kludge for now, don't add more than max - * But it means updateFileReadCostStats will not be querying the correct files, - * if the file varies per CQ execution (see other notes in updateFileReadCostStats) - */ - for (unsigned i=subFileStats.size(); igetEncryptKey(ekeylen,ekey); @@ -123,6 +114,7 @@ void CDiskReadMasterBase::serializeSlaveData(MemoryBuffer &dst, unsigned slave) ForEachItemIn(s, subfileLogicalFilenames) dst.append(subfileLogicalFilenames.item(s)); } + dst.append(fileStatsTableStart); if (mapping) mapping->serializeMap(slave, dst); else @@ -131,7 +123,7 @@ void CDiskReadMasterBase::serializeSlaveData(MemoryBuffer &dst, unsigned slave) void CDiskReadMasterBase::done() { - updateFileReadCostStats(subFileStats); + updateFileReadCostStats(); CMasterActivity::done(); } @@ -140,8 +132,11 @@ void CDiskReadMasterBase::deserializeStats(unsigned node, MemoryBuffer &mb) CMasterActivity::deserializeStats(node, mb); if (mapping && (mapping->queryMapWidth(node)>0)) // there won't be any subfile stats. if worker was sent 0 parts { - for (auto &stats: subFileStats) - stats->deserialize(node, mb); + unsigned numFilesToRead; + mb.read(numFilesToRead); + assertex(numFilesToRead<=fileStats.size()); + for (unsigned i=0; ideserialize(node, mb); } } ///////////////// diff --git a/thorlcr/activities/thdiskbase.ipp b/thorlcr/activities/thdiskbase.ipp index 470d1f49447..d3dad2be550 100644 --- a/thorlcr/activities/thdiskbase.ipp +++ b/thorlcr/activities/thdiskbase.ipp @@ -33,8 +33,7 @@ protected: Owned mapping; IHash *hash; StringAttr fileName; - std::vector> subFileStats; - + unsigned fileStatsTableStart = NotFound; public: CDiskReadMasterBase(CMasterGraphElement *info); virtual void init() override; diff --git a/thorlcr/activities/thdiskbaseslave.cpp b/thorlcr/activities/thdiskbaseslave.cpp index 343f5ad29b5..24998488ce2 100644 --- a/thorlcr/activities/thdiskbaseslave.cpp +++ b/thorlcr/activities/thdiskbaseslave.cpp @@ -228,6 +228,7 @@ void CDiskReadSlaveActivityBase::init(MemoryBuffer &data, MemoryBuffer &slaveDat data.read(subfile); subfileLogicalFilenames.append(subfile); } + data.read(fileTableStart); unsigned parts; data.read(parts); if (parts) @@ -242,28 +243,31 @@ void CDiskReadSlaveActivityBase::init(MemoryBuffer &data, MemoryBuffer &slaveDat } else { - ISuperFileDescriptor *super = partDescs.item(0).queryOwner().querySuperFileDescriptor(); - if (super) + if ((TDXjobtemp & helper->getFlags())==0) { - unsigned numSubFiles = super->querySubFiles(); - for (unsigned i=0; iquerySubFiles():0, diskReadRemoteStatistics); } } } gotMeta = false; // if variable filename and inside loop, need to invalidate cached meta } -void CDiskReadSlaveActivityBase::mergeSubFileStats(IPartDescriptor *partDesc, IExtRowStream *partStream) +void CDiskReadSlaveActivityBase::mergeFileStats(IPartDescriptor *partDesc, IExtRowStream *partStream) { - if (subFileStats.size()>0) + if (fileStats.size()>0) { ISuperFileDescriptor * superFDesc = partDesc->queryOwner().querySuperFileDescriptor(); - dbgassertex(superFDesc); - unsigned subfile, lnum; - if(superFDesc->mapSubPart(partDesc->queryPartIndex(), subfile, lnum)) - mergeStats(*subFileStats[subfile], partStream); + if (superFDesc) + { + unsigned subfile, lnum; + if(superFDesc->mapSubPart(partDesc->queryPartIndex(), subfile, lnum)) + mergeStats(*fileStats[subfile+fileTableStart], partStream); + } + else + mergeStats(*fileStats[fileTableStart], partStream); } } + const char *CDiskReadSlaveActivityBase::queryLogicalFilename(unsigned index) { return subfileLogicalFilenames.item(index); @@ -320,7 +324,8 @@ void CDiskReadSlaveActivityBase::serializeStats(MemoryBuffer &mb) stats.set(activePartStats); // replace disk read stats } PARENT::serializeStats(mb); - for (auto &stats: subFileStats) + mb.append((unsigned)fileStats.size()); + for (auto &stats: fileStats) stats->serialize(mb); } diff --git a/thorlcr/activities/thdiskbaseslave.ipp b/thorlcr/activities/thdiskbaseslave.ipp index ed4aad9cef7..5416c1e7235 100644 --- a/thorlcr/activities/thdiskbaseslave.ipp +++ b/thorlcr/activities/thdiskbaseslave.ipp @@ -91,13 +91,12 @@ protected: mutable ThorDataLinkMetaInfo cachedMetaInfo; Owned partHandler; Owned eexp; - std::vector> subFileStats; - + unsigned fileTableStart = NotFound; public: CDiskReadSlaveActivityBase(CGraphElementBase *_container, IHThorArg *_helper); const char *queryLogicalFilename(unsigned index); IThorRowInterfaces * queryProjectedDiskRowInterfaces(); - void mergeSubFileStats(IPartDescriptor *partDesc, IExtRowStream *partStream); + void mergeFileStats(IPartDescriptor *partDesc, IExtRowStream *partStream); virtual void start() override; // IThorSlaveActivity diff --git a/thorlcr/graph/thgraphmaster.cpp b/thorlcr/graph/thgraphmaster.cpp index ede01af823b..c3009c41970 100644 --- a/thorlcr/graph/thgraphmaster.cpp +++ b/thorlcr/graph/thgraphmaster.cpp @@ -390,35 +390,91 @@ CMasterActivity::~CMasterActivity() IDistributedFile *CMasterActivity::queryReadFile(unsigned f) { - if (f>=readFiles.ordinality()) - return NULL; - return &readFiles.item(f); + if (f>=readFiles.size()) + return nullptr; + return readFiles[f]; } -IDistributedFile *CMasterActivity::findReadFile(const char *lfnName) +unsigned CMasterActivity::queryReadFileId(const char *normalizedFileName) { - auto it = readFilesMap.find(lfnName); + auto it = readFilesMap.find(normalizedFileName); if (it != readFilesMap.end()) - return LINK(it->second); + return (int)(it->second); + return NotFound; +} + +IDistributedFile *CMasterActivity::findReadFile(const char *normalizedFileName) +{ + auto it = readFilesMap.find(normalizedFileName); + if (it != readFilesMap.end()) + return LINK(readFiles[it->second]); return nullptr; } -IDistributedFile *CMasterActivity::lookupReadFile(const char *lfnName, AccessMode mode, bool jobTemp, bool temp, bool opt) +IDistributedFile *CMasterActivity::lookupReadFile(const char *lfnName, AccessMode mode, bool jobTemp, bool temp, bool opt, bool statsForMultipleFiles, const StatisticsMapping & statsMapping, unsigned * fileStatsStartEntry) { StringBuffer normalizedFileName; queryThorFileManager().addScope(container.queryJob(), lfnName, normalizedFileName, jobTemp|temp); - Owned file = findReadFile(normalizedFileName); - if (!file) + Owned file; + unsigned fileId = queryReadFileId(normalizedFileName.str()); + if (fileId==NotFound) { file.setown(queryThorFileManager().lookup(container.queryJob(), lfnName, mode, jobTemp|temp, opt, true, container.activityIsCodeSigned())); if (file) { - readFiles.append(*LINK(file)); - readFilesMap[normalizedFileName.str()] = file; + fileId = readFiles.size(); + readFiles.push_back(LINK(file)); + readFilesMap[normalizedFileName.str()] = fileId; if (!temp) // NB: Temps not listed in workunit queryThorFileManager().noteFileRead(container.queryJob(), file); + if (!jobTemp && !temp && fileStatsStartEntry!=nullptr) + { + unsigned fileStatsSizeExtend = 0; // how many entries need to be added to fileStats vector + IDistributedSuperFile *super = file->querySuperFile(); + bool isSuper = super!=nullptr; + unsigned numSubs = isSuper ? super->numSubFiles(true) : 0; + if (statsForMultipleFiles) + { + // fileStats[] will need to track stats from multiple files. To do this, fileStatsTable[] vector + // will be used to map from file id to the starting point of the stats in fileStats. + // e.g. + // Given that file id for 'child::super' is 0 + // file id for 'child::f1' is 1 + // file id for 'child::f2' is 2 + // and child::super has 2 subfiles + // + // Then the following table will be generated (through multiple calls to this function) + // fileStatsTable[0] = 0; + // fileStatsTable[1] = 2; + // fileStatsTable[2] = 3; + // + // fileStats[0] = stats for 'child::super' - stats for child::super and subfile 1 + // fileStats[1] = stats for 'child::super' - stats for child::super and subfile 2 + // fileStats[2] = stats for 'child::f1' - stats for child::f1 + // fileStats[3] = stats for 'child::f2' - stats for child::f2 + *fileStatsStartEntry = (unsigned) fileStats.size(); // index of the first stats entry for this file + fileStatsTable.push_back(*fileStatsStartEntry); + assertex(fileId==fileStatsTable.size()-1); + fileStatsSizeExtend = isSuper?numSubs:1; + } + else // Use entire fileStats to track stats for a superfile (fileStats is not used if not a superfile) + { + assertex(fileStats.size() == 0); + *fileStatsStartEntry = 0; + // Super files will use fileStats to track stats. Non-super file stats tracked in graph stats + fileStatsSizeExtend = isSuper?numSubs:0; + } + for (unsigned i=0; i tmpFiles; - ForEachItemIn(f, readFiles) + for (IDistributedFile * file: readFiles) { - IDistributedFile &file = readFiles.item(f); - IDistributedSuperFile *super = file.querySuperFile(); + IDistributedSuperFile *super = file->querySuperFile(); if (super) { getSuperFileSubs(super, tmpFiles, true); - tmpFiles.append(*LINK(&file)); + tmpFiles.append(*LINK(file)); } else - tmpFiles.append(*LINK(&file)); + tmpFiles.append(*LINK(file)); } ForEachItemIn(s, tmpFiles) { @@ -579,61 +636,47 @@ void CMasterActivity::done() } } -void CMasterActivity::updateFileReadCostStats(std::vector> & subFileStats) -{ - /* JCSMORE->SHAMSER: (separate JIRA needed) - * there can be >1 read file if this act. is in a child query/loop, it could be processing a different file per iteration, - * meaning there could be an arbitrary number of readfiles, in that case activity::init would be called multiple times. - * - * I hit an assert during testing reading a super in a CQ: - libjlib.so!raiseAssertException(const char * assertion, const char * file, unsigned int line) (\home\jsmith\git\HPCC-Platform\system\jlib\jexcept.cpp:660) - libjlib.so!MemoryBuffer::read(MemoryBuffer * const this, unsigned char & value) (\home\jsmith\git\HPCC-Platform\system\jlib\jbuff.cpp:693) - libjlib.so!MemoryBuffer::readPacked(MemoryBuffer * const this) (\home\jsmith\git\HPCC-Platform\system\jlib\jbuff.cpp:813) - libjlib.so!MemoryBuffer::readPacked(MemoryBuffer * const this, unsigned int & value) (\home\jsmith\git\HPCC-Platform\system\jlib\jbuff.cpp:824) - libjlib.so!CRuntimeStatisticCollection::deserialize(CRuntimeStatisticCollection * const this, MemoryBuffer & in) (\home\jsmith\git\HPCC-Platform\system\jlib\jstats.cpp:2524) - libactivitymasters_lcr.so!CThorStatsCollection::deserialize(CThorStatsCollection * const this, unsigned int node, MemoryBuffer & mb) (\home\jsmith\git\HPCC-Platform\thorlcr\graph\thgraphmaster.ipp:53) - libactivitymasters_lcr.so!CDiskReadMasterBase::deserializeStats(CDiskReadMasterBase * const this, unsigned int node, MemoryBuffer & mb) (\home\jsmith\git\HPCC-Platform\thorlcr\activities\thdiskbase.cpp:139) - libgraphmaster_lcr.so!CMasterGraph::deserializeStats(CMasterGraph * const this, unsigned int node, MemoryBuffer & mb) (\home\jsmith\git\HPCC-Platform\thorlcr\graph\thgraphmaster.cpp:2781) - * - * (would be a crash in a Release build) - * it's because the diskread init is adding new CThorStatsCollection per init (per execution of the CQ), - * which means when deserializing there are too many. The code assumes there is only 1 file being read. - * - * I've temporarily changed the code where subFileStats's are added, to prevent more being added per iteration, - * but it needs re-thinking to handle the workers potentially dealing with different logical files - * (+ index read to handle super files, and case where act. is reading >1 file, i.e. KJ) - * I've changed this code rely on the 1st readFiles for now (not the # of subFileStats, which may be more) - * NB: also changed when the expansion of readFiles (from supers to subfiles) happens, a new super was being added - * each CQ iteration and re-expanded, meaing readFiles kept growing. - * - * Also, superkey1.ecl hits a dbgasserex whilst deserializing stats (before and after these PR changes), - * but is caught/ignored. I haven't investigated further. - */ - - IDistributedFile *file = queryReadFile(0); - if (file) - { - IDistributedSuperFile *super = file->querySuperFile(); - if (super) - { - unsigned numSubFiles = super->numSubFiles(true); //subFileStats.size(); - if (subFileStats.size()) +void CMasterActivity::updateFileReadCostStats() +{ + if (fileStats.size()>0) + { + unsigned fileIndex = 0; + for (unsigned i=0; iquerySuperFile(); + if (super) { - IDistributedFile &subFile = super->querySubFile(i, true); - const char *subName = subFile.queryLogicalName(); - PROGLOG("subName = %s", subName); - stat_type numDiskReads = subFileStats[i]->getStatisticSum(StNumDiskReads); + unsigned numSubFiles = super->numSubFiles(true); + for (unsigned i=0; iquerySubFile(i, true); + stat_type numDiskReads = fileStats[fileIndex]->getStatisticSum(StNumDiskReads); + StringBuffer clusterName; + subFile.getClusterName(0, clusterName); + diskAccessCost += money2cost_type(calcFileAccessCost(clusterName, 0, numDiskReads)); + subFile.addAttrValue("@numDiskReads", numDiskReads); + fileIndex++; + } + } + else + { + stat_type numDiskReads = fileStats[fileIndex]->getStatisticSum(StNumDiskReads); StringBuffer clusterName; - subFile.getClusterName(0, clusterName); + file->getClusterName(0, clusterName); diskAccessCost += money2cost_type(calcFileAccessCost(clusterName, 0, numDiskReads)); - subFile.addAttrValue("@numDiskReads", numDiskReads); + file->addAttrValue("@numDiskReads", numDiskReads); + fileIndex++; } } } - else + } + else + { + IDistributedFile *file = queryReadFile(0); + if (file) { stat_type numDiskReads = statsCollection.getStatisticSum(StNumDiskReads); StringBuffer clusterName; diff --git a/thorlcr/graph/thgraphmaster.ipp b/thorlcr/graph/thgraphmaster.ipp index a4065c67af9..6fca98e8b77 100644 --- a/thorlcr/graph/thgraphmaster.ipp +++ b/thorlcr/graph/thgraphmaster.ipp @@ -239,19 +239,21 @@ class graphmaster_decl CMasterActivity : public CActivityBase, implements IThrea bool asyncStart; MemoryBuffer *data; CriticalSection progressCrit; - IArrayOf readFiles; - std::unordered_map readFilesMap; // NB: IDistributedFile pointers are owned by readFiles - + std::vector> readFiles; + std::unordered_map readFilesMap; // NB: IDistributedFile pointers are owned by readFiles + std::vector fileStatsTable; protected: std::vector> edgeStatsVector; CThorStatsCollection statsCollection; IBitSet *notedWarnings; cost_type diskAccessCost = 0; + std::vector> fileStats; IDistributedFile *queryReadFile(unsigned f); + unsigned queryReadFileId(const char *lfnName); IDistributedFile *findReadFile(const char *lfnName); - IDistributedFile *lookupReadFile(const char *lfnName, AccessMode mode, bool jobTemp, bool temp, bool opt); - void updateFileReadCostStats(std::vector> & subFileStats); + IDistributedFile *lookupReadFile(const char *lfnName, AccessMode mode, bool jobTemp, bool temp, bool opt, bool statsForMultipleFiles=false, const StatisticsMapping &statsMapping=diskReadRemoteStatistics, unsigned * fileStatsStartEntry=nullptr); + void updateFileReadCostStats(); void updateFileWriteCostStats(IFileDescriptor & fileDesc, IPropertyTree &props, stat_type numDiskWrites); virtual void process() { } public: diff --git a/thorlcr/graph/thgraphslave.cpp b/thorlcr/graph/thgraphslave.cpp index 81087e33494..4c1d256370a 100644 --- a/thorlcr/graph/thgraphslave.cpp +++ b/thorlcr/graph/thgraphslave.cpp @@ -233,6 +233,18 @@ bool CSlaveActivity::isLookAheadActive(unsigned index) const return _input.isLookAheadActive(); } +void CSlaveActivity::setupSpace4FileStats(unsigned where, bool statsForMultipleFiles, bool isSuper, unsigned numSubs, const StatisticsMapping & statsMapping) +{ + unsigned slotsNeeded = 0; + if (statsForMultipleFiles) // for variable filenames: filestats will track stats from multiple files + slotsNeeded = isSuper ? numSubs : 1; + else // filename fixed so only use need filestats for super files as activity stats can track file stats + // (n.b. where == 0 when single file stats tracking) + slotsNeeded = isSuper ? numSubs : 0; + for (unsigned i=fileStats.size(); i> fileStats; protected: unsigned __int64 queryLocalCycles() const; @@ -217,7 +218,7 @@ class graphslave_decl CSlaveActivity : public CActivityBase, public CEdgeProgres void setLookAhead(unsigned index, IStartableEngineRowStream *lookAhead, bool persistent); void startLookAhead(unsigned index); bool isLookAheadActive(unsigned index) const; - + void setupSpace4FileStats(unsigned where, bool statsForMultipleFiles, bool isSuper, unsigned numSubs, const StatisticsMapping & statsMapping); public: IMPLEMENT_IINTERFACE_USING(CActivityBase) From 217f402c86fbbf68cc51917f8b3474e517f69a66 Mon Sep 17 00:00:00 2001 From: Gavin Halliday Date: Thu, 5 May 2022 11:17:53 +0100 Subject: [PATCH 07/24] HPCC-27606 Avoid linking source when merging stats Signed-off-by: Gavin Halliday --- system/jlib/jstats.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/jlib/jstats.h b/system/jlib/jstats.h index 05ce5d2e441..268c98b113c 100644 --- a/system/jlib/jstats.h +++ b/system/jlib/jstats.h @@ -797,13 +797,13 @@ void mergeStats(CRuntimeStatisticCollection & stats, INTERFACE * source, const S } template -void mergeStats(CRuntimeStatisticCollection & stats, Shared source, const StatisticsMapping & mapping) { mergeStats(stats, source.get(), mapping); } +void mergeStats(CRuntimeStatisticCollection & stats, const Shared & source, const StatisticsMapping & mapping) { mergeStats(stats, source.get(), mapping); } template void mergeStats(CRuntimeStatisticCollection & stats, INTERFACE * source) { mergeStats(stats, source, stats.queryMapping()); } template -void mergeStats(CRuntimeStatisticCollection & stats, Shared source) { mergeStats(stats, source.get(), stats.queryMapping()); } +void mergeStats(CRuntimeStatisticCollection & stats, const Shared & source) { mergeStats(stats, source.get(), stats.queryMapping()); } template void mergeStat(CRuntimeStatisticCollection & stats, INTERFACE * source, StatisticKind kind) @@ -813,7 +813,7 @@ void mergeStat(CRuntimeStatisticCollection & stats, INTERFACE * source, Statisti } template -void mergeStat(CRuntimeStatisticCollection & stats, Shared source, StatisticKind kind) { mergeStat(stats, source.get(), kind); } +void mergeStat(CRuntimeStatisticCollection & stats, const Shared & source, StatisticKind kind) { mergeStat(stats, source.get(), kind); } //--------------------------------------------------------------------------------------------------------------------- From 9e876ca405ff6efa4701e29372dfaa011bd62c2b Mon Sep 17 00:00:00 2001 From: wangkx Date: Tue, 15 Mar 2022 16:49:39 -0400 Subject: [PATCH 08/24] HPCC-24078 Retrieve Component Logs for K8 job 1. Report Component Logs in WUInfo response. 2. Send the WUFile request from ECLWatch to ESP. 3. When the WUFile request has a zip option, use the existing readWorkunitComponentLogs() to read the Component Logs into a file, zip the file, and send it out. 4. When the WUFile request has no zip option, use the IRemoteLogAccessStream to read Component Logs into new CFlushingHttpResponseBuffer and send the Logs chuck by chuck. Signed-off-by: wangkx --- .../http/platform/httpresponsebuffer.ipp | 69 ++++ esp/bindings/http/platform/httptransport.cpp | 17 + esp/bindings/http/platform/httptransport.ipp | 1 + esp/scm/ws_workunits.ecm | 2 +- esp/scm/ws_workunits_req_resp.ecm | 1 + esp/scm/ws_workunits_struct.ecm | 8 + esp/services/ws_sql/CMakeLists.txt | 3 + esp/services/ws_workunits/CMakeLists.txt | 1 + .../ws_workunits/ws_workunitsAuditLogs.cpp | 13 + .../ws_workunits/ws_workunitsHelpers.cpp | 338 ++++++++++++++++-- .../ws_workunits/ws_workunitsHelpers.hpp | 27 +- esp/src/eclwatch/HelpersWidget.js | 3 + esp/src/src-react/components/Helpers.tsx | 3 + 13 files changed, 445 insertions(+), 41 deletions(-) create mode 100644 esp/bindings/http/platform/httpresponsebuffer.ipp diff --git a/esp/bindings/http/platform/httpresponsebuffer.ipp b/esp/bindings/http/platform/httpresponsebuffer.ipp new file mode 100644 index 00000000000..e93cb0c23db --- /dev/null +++ b/esp/bindings/http/platform/httpresponsebuffer.ipp @@ -0,0 +1,69 @@ +/*############################################################################## + + HPCC SYSTEMS software Copyright (C) 2022 HPCC Systems®. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +############################################################################## */ + +#ifndef _HTTPRESPONSEBUFFER_IPP__ +#define _HTTPRESPONSEBUFFER_IPP__ + +#include "http/platform/httptransport.ipp" +#include "rtlformat.hpp" + +static constexpr unsigned defaultResponseFlushThresholdBytes = 8000; +static constexpr unsigned closingResponseFlushThresholdBytes = 1; + +class CFlushingHttpResponseBuffer : public CInterfaceOf +{ + CHttpResponse* response = nullptr; + const size32_t responseFlushThreshold = defaultResponseFlushThresholdBytes; + StringBuffer responseBuffer; + +public: + CFlushingHttpResponseBuffer(CHttpResponse* _response, size32_t _flushThreshold) : + response(_response), responseFlushThreshold(_flushThreshold) + { + if (!response) + throw makeStringException(-1, "HttpResponse not specified for CFlushingWUFileBuffer"); + }; + ~CFlushingHttpResponseBuffer() + { + try + { + if (!responseBuffer.isEmpty()) + response->sendChunk(responseBuffer); + } + catch (IException* e) + { + // Ignore any socket errors that we get at termination - nothing we can do about them anyway... + EXCLOG(e, "In ~CFlushingHttpResponseBuffer()"); + e->Release(); + } + } + void flushXML(StringBuffer& current, bool closing) + { + responseBuffer.append(current); + current.clear(); + + //When closing, no flushing is needed if responseBuffer.length() < closingResponseFlushThresholdBytes. + const size32_t threshold = closing ? closingResponseFlushThresholdBytes : responseFlushThreshold; + if (responseBuffer.length() < threshold) + return; + + response->sendChunk(responseBuffer); + responseBuffer.clear(); + } +}; + +#endif diff --git a/esp/bindings/http/platform/httptransport.cpp b/esp/bindings/http/platform/httptransport.cpp index 85c586ebf0d..5d5c5eb8f98 100644 --- a/esp/bindings/http/platform/httptransport.cpp +++ b/esp/bindings/http/platform/httptransport.cpp @@ -501,6 +501,23 @@ IProperties *CHttpMessage::queryParameters() return m_queryparams.get(); } +int CHttpMessage::getParameterInt(const char* name, int defaultValue) +{ + StringBuffer value; + getParameter(name, value); + if (value.isEmpty()) + return defaultValue; + + const char* finger = value.str(); + while (*finger) + { + if (!isdigit(*finger)) + return defaultValue; + ++finger; + } + return atoi(value.str()); +} + IProperties *CHttpMessage::getParameters() { if (!m_queryparams) diff --git a/esp/bindings/http/platform/httptransport.ipp b/esp/bindings/http/platform/httptransport.ipp index 6758ba1e60b..31b9d4db408 100644 --- a/esp/bindings/http/platform/httptransport.ipp +++ b/esp/bindings/http/platform/httptransport.ipp @@ -174,6 +174,7 @@ public: virtual int getAttachmentCount(){return m_attachCount;} virtual IProperties *queryParameters(); virtual IProperties *getParameters(); + virtual int getParameterInt(const char* name, int defaultValue); virtual MapStrToBuf *queryAttachments() { return &m_attachments; diff --git a/esp/scm/ws_workunits.ecm b/esp/scm/ws_workunits.ecm index 3a9151a0199..30d22c08cd2 100644 --- a/esp/scm/ws_workunits.ecm +++ b/esp/scm/ws_workunits.ecm @@ -25,7 +25,7 @@ EspInclude(ws_workunits_queryset_req_resp); ESPservice [ auth_feature("DEFERRED"), //This declares that the method logic handles feature level authorization - version("1.89"), default_client_version("1.89"), cache_group("ESPWsWUs"), + version("1.90"), default_client_version("1.90"), cache_group("ESPWsWUs"), noforms,exceptions_inline("./smc_xslt/exceptions.xslt"),use_method_name] WsWorkunits { ESPmethod [cache_seconds(60), resp_xsl_default("/esp/xslt/workunits.xslt")] WUQuery(WUQueryRequest, WUQueryResponse); diff --git a/esp/scm/ws_workunits_req_resp.ecm b/esp/scm/ws_workunits_req_resp.ecm index b37c5bcad28..7947ba281c2 100644 --- a/esp/scm/ws_workunits_req_resp.ecm +++ b/esp/scm/ws_workunits_req_resp.ecm @@ -403,6 +403,7 @@ ESPrequest WULogFileRequest [min_ver("1.55")] int64 SizeLimit(0); [min_ver("1.77")] ESPenum ErrorMessageFormat ErrorMessageFormat; string PlainText; + [min_ver("1.90")] ESPStruct WUFileOption FileOptions; }; ESPresponse [exceptions_inline] WULogFileResponse diff --git a/esp/scm/ws_workunits_struct.ecm b/esp/scm/ws_workunits_struct.ecm index 1d9b8036c00..40e429db186 100644 --- a/esp/scm/ws_workunits_struct.ecm +++ b/esp/scm/ws_workunits_struct.ecm @@ -16,6 +16,8 @@ ############################################################################## */ #include "xslprocessor.hpp" +EspInclude(ws_logaccess); + // =========================================================================== EspInclude(common); @@ -64,6 +66,7 @@ ESPenum WUFileType : string ThorSlaveLog("ThorSlaveLog"), EclAgentLog("EclAgentLog"), ArchiveQuery("ArchiveQuery"), + ComponentLog("ComponentLog"), }; ESPenum WUFileDownloadOption : int @@ -522,6 +525,11 @@ ESPStruct WUFileOption string PlainText; //XML: optional int SlaveNumber(1); //ThorSlaveLog: optional int64 SizeLimit(0); + [min_ver("1.90"), nil_remove] unsigned MaxLogRecords; + [min_ver("1.90"), nil_remove] ESPenum LogSelectColumnMode LogSelectColumnMode; + [min_ver("1.90"), nil_remove] ESPenum LogAccessLogFormat LogFormat; + [min_ver("1.90"), nil_remove] unsigned LogSearchTimeBuffSecs; + [min_ver("1.90"), nil_remove] ESParray LogColumns; }; ESPStruct [nil_remove] ScheduledWU diff --git a/esp/services/ws_sql/CMakeLists.txt b/esp/services/ws_sql/CMakeLists.txt index 521d2cd80aa..a0f093e3dc0 100644 --- a/esp/services/ws_sql/CMakeLists.txt +++ b/esp/services/ws_sql/CMakeLists.txt @@ -96,6 +96,9 @@ if(WSSQL_SERVICE AND USE_JAVA) ${ESPSCM_GENERATED_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/SQL2ECL ${HPCC_SOURCE_DIR}/common/thorhelper + ${HPCC_SOURCE_DIR}/esp/bindings/http/platform + ${HPCC_SOURCE_DIR}/rtl/eclrtl + ${HPCC_SOURCE_DIR}/rtl/include ) if (CMAKE_COMPILER_IS_CLANG) diff --git a/esp/services/ws_workunits/CMakeLists.txt b/esp/services/ws_workunits/CMakeLists.txt index 0ca353d611f..0b2b5e70cba 100644 --- a/esp/services/ws_workunits/CMakeLists.txt +++ b/esp/services/ws_workunits/CMakeLists.txt @@ -88,6 +88,7 @@ include_directories ( ./../../bindings/http/client ./../../smc/SMCLib ./../../bindings/SOAP/xpp + ${HPCC_SOURCE_DIR}/esp/bindings/http/platform ${HPCC_SOURCE_DIR}/dali/dfu ${HPCC_SOURCE_DIR}/dali/ft ./../../../testing/unittests diff --git a/esp/services/ws_workunits/ws_workunitsAuditLogs.cpp b/esp/services/ws_workunits/ws_workunitsAuditLogs.cpp index 0e242db7557..95ee9d598fb 100644 --- a/esp/services/ws_workunits/ws_workunitsAuditLogs.cpp +++ b/esp/services/ws_workunits/ws_workunitsAuditLogs.cpp @@ -1622,6 +1622,19 @@ int CWsWorkunitsSoapBindingEx::onGet(CHttpRequest* request, CHttpResponse* respo return CWsWorkunitsSoapBinding::onGet(request,response); return 0; } +#ifdef _CONTAINERIZED + else if (!strnicmp(path.str(), "/WsWorkunits/WUFile", 19)) + { + StringBuffer type; + request->getParameter("Type", type); + if (strieq(type.str(), File_ComponentLog)) + { + CWsWuFileHelper helper(nullptr); + helper.sendWUComponentLogStreaming(request, response); + return 0; + } + } +#endif else if (!strnicmp(path.str(), REQPATH_CREATEANDDOWNLOADZAP, sizeof(REQPATH_CREATEANDDOWNLOADZAP) - 1)) { createAndDownloadWUZAPFile(*ctx, request, response); diff --git a/esp/services/ws_workunits/ws_workunitsHelpers.cpp b/esp/services/ws_workunits/ws_workunitsHelpers.cpp index 00257e2d05e..2abeccf4d76 100644 --- a/esp/services/ws_workunits/ws_workunitsHelpers.cpp +++ b/esp/services/ws_workunits/ws_workunitsHelpers.cpp @@ -206,8 +206,8 @@ WsWUExceptions::WsWUExceptions(IConstWorkUnit& wu): numerr(0), numwrn(0), numinf } } -void WsWuInfo::readWorkunitComponentLogs(const char* outFile, unsigned maxLogRecords, - LogAccessReturnColsMode retColsMode, LogAccessLogFormat logFormat, unsigned wuLogSearchTimeBuffSecs) +void WsWuInfo::readWorkunitComponentLogs(const char* outFile, unsigned maxLogRecords, const LogAccessReturnColsMode retColsMode, + const LogAccessLogFormat logFormat, unsigned wuLogSearchTimeBuffSecs) { if (!m_remoteLogAccessor) throw makeStringException(ECLWATCH_LOGACCESS_UNAVAILABLE, "WsWuInfo: Remote Log Access plug-in not available!"); @@ -216,41 +216,10 @@ void WsWuInfo::readWorkunitComponentLogs(const char* outFile, unsigned maxLogRec throw makeStringException(ECLWATCH_INVALID_FILE_NAME, "WsWuInfo: Target filename not provided!"); LogAccessConditions logFetchOptions; - logFetchOptions.setFilter(getJobIDLogAccessFilter(wuid.str())); - - struct LogAccessTimeRange range; - stat_type timeStamp; - if (cw->getStatistic(timeStamp, "", StWhenCreated)) - { - CDateTime startt; - startt.set(timeStamp / microSecsToSecDivisor); - - startt.adjustTimeSecs(-wuLogSearchTimeBuffSecs); - range.setStart(startt); - - StringBuffer newstart; - startt.getString(newstart); - DBGLOG("Searching for WUID '%s' log entries starting time: '%s'", wuid.str(), newstart.str()); - } - else - throw makeStringExceptionV(ECLWATCH_WU_START_NOT_AVAILABLE, "WsWuInfo: Cound not determine WUID '%s' start time", wuid.str()); - - if (cw->getStatistic(timeStamp, "", StWhenFinished)) - { - CDateTime endt; - endt.set(timeStamp / microSecsToSecDivisor); - endt.adjustTimeSecs(wuLogSearchTimeBuffSecs); - - range.setEnd(endt); - StringBuffer newend; - endt.getString(newend); - DBGLOG("Searching for WUID '%s' log entries ending time: '%s'", wuid.str(), newend.str()); //end + time buffer - } - else - WARNLOG("WsWuInfo: Fetching log entries for '%s' without a 'finished'", wuid.str()); + logFetchOptions.setFilter(getJobIDLogAccessFilter(wuid)); + setLogTimeRange(logFetchOptions, wuLogSearchTimeBuffSecs); logFetchOptions.setReturnColsMode(retColsMode); - logFetchOptions.setTimeRange(range); logFetchOptions.setLimit(maxLogRecords); Owned outIOS; @@ -260,7 +229,7 @@ void WsWuInfo::readWorkunitComponentLogs(const char* outFile, unsigned maxLogRec if(!outIOS) throw makeStringException(ECLWATCH_CANNOT_OPEN_FILE, "WsWuInfo: Could not create target log file!"); - Owned logreader = m_remoteLogAccessor->getLogReader(logFetchOptions, logFormat); + Owned logReader = m_remoteLogAccessor->getLogReader(logFetchOptions, logFormat); StringBuffer logcontent; unsigned totalRecsRead = 0; @@ -271,7 +240,7 @@ void WsWuInfo::readWorkunitComponentLogs(const char* outFile, unsigned maxLogRec else if (logFormat == LOGACCESS_LOGFORMAT_xml) writeStringToStream(*outIOS, ""); - while (logreader->readLogEntries(logcontent.clear(), recsRead)) + while (logReader->readLogEntries(logcontent.clear(), recsRead)) { if (logFormat == LOGACCESS_LOGFORMAT_json && totalRecsRead > 0 && recsRead > 0) { @@ -310,6 +279,141 @@ void WsWuInfo::readWorkunitComponentLogs(const char* outFile, unsigned maxLogRec } } +void WsWuInfo::setLogTimeRange(LogAccessConditions& logFetchOptions, unsigned wuLogSearchTimeBuffSecs) +{ + struct LogAccessTimeRange range; + stat_type timeStamp; + if (cw->getStatistic(timeStamp, "", StWhenCreated)) + { + CDateTime startt; + startt.set(timeStamp / microSecsToSecDivisor); + + startt.adjustTimeSecs(-wuLogSearchTimeBuffSecs); + range.setStart(startt); + + StringBuffer newstart; + startt.getString(newstart); + DBGLOG("Searching for WUID '%s' log entries starting time: '%s'", wuid.get(), newstart.str()); + } + else + throw makeStringExceptionV(ECLWATCH_WU_START_NOT_AVAILABLE, "WsWuInfo: Cound not determine WUID '%s' start time", wuid.get()); + + if (cw->getStatistic(timeStamp, "", StWhenFinished)) + { + CDateTime endt; + endt.set(timeStamp / microSecsToSecDivisor); + endt.adjustTimeSecs(wuLogSearchTimeBuffSecs); + + range.setEnd(endt); + StringBuffer newend; + endt.getString(newend); + DBGLOG("Searching for WUID '%s' log entries ending time: '%s'", wuid.get(), newend.str()); //end + time buffer + } + else + WARNLOG("WsWuInfo: Fetching log entries for '%s' without a 'finished' statistics entry", wuid.get()); + + logFetchOptions.setTimeRange(range); +} + +#ifdef _CONTAINERIZED +void WsWuInfo::sendWorkunitComponentLogs(IEspContext* context, CHttpResponse* response, WUComponentLogOptions& options) +{ + if (!m_remoteLogAccessor) + throw makeStringException(ECLWATCH_LOGACCESS_UNAVAILABLE, "WsWuInfo: Remote Log Access plug-in not available!"); + + setLogTimeRange(options.logFetchOptions, options.wuLogSearchTimeBuffSecs); + options.logFetchOptions.setFilter(getJobIDLogAccessFilter(wuid)); + + response->setStatus(HTTP_STATUS_OK); + response->startSend(); + + Owned flusher = new CFlushingHttpResponseBuffer(response, defaultResponseFlushThresholdBytes); //No need to use a different ResponseFlushThreshold. + Owned logReader = m_remoteLogAccessor->getLogReader(options.logFetchOptions, options.logFormat); + if (options.logFormat == LOGACCESS_LOGFORMAT_csv) + sendComponentLogCSV(context, logReader, flusher, options); + else if (options.logFormat == LOGACCESS_LOGFORMAT_json) + sendComponentLogJSON(context, logReader, flusher, options); + else + sendComponentLogXML(context, logReader, flusher, options); +} + +void WsWuInfo::sendComponentLogCSV(IEspContext* context, IRemoteLogAccessStream* logReader, IXmlStreamFlusher* flusher, const WUComponentLogOptions& options) +{ + unsigned totalRecsRead = sendComponentLogContent(context, logReader, flusher, options); + if (totalRecsRead == options.logFetchOptions.getLimit()) //Warn of possible truncation + { + VStringBuffer truncationWarnmessage("Log query reached record limit (%u). Log report could be incomplete.", options.logFetchOptions.getLimit()); + LOG(MCuserInfo, "WsWuInfo: %s", truncationWarnmessage.str()); + flusher->flushXML(truncationWarnmessage, false); + } +} + +void WsWuInfo::sendComponentLogJSON(IEspContext* context, IRemoteLogAccessStream* logReader, IXmlStreamFlusher* flusher, const WUComponentLogOptions& options) +{ + StringBuffer s("{\"lines\": ["); + flusher->flushXML(s, false); + unsigned totalRecsRead = sendComponentLogContent(context, logReader, flusher, options); + if (totalRecsRead == options.logFetchOptions.getLimit()) //Warn of possible truncation + { + VStringBuffer truncationWarnmessage("Log query reached record limit (%u). Log report could be incomplete.", options.logFetchOptions.getLimit()); + LOG(MCuserInfo, "WsWuInfo: %s", truncationWarnmessage.str()); + VStringBuffer jsonMessage("], \"Message\": \"%s\"}", truncationWarnmessage.str()); //close lines array, append Message object + flusher->flushXML(jsonMessage, true); + } + else + { + s.set("]}"); + flusher->flushXML(s, true); + } +} + +void WsWuInfo::sendComponentLogXML(IEspContext* context, IRemoteLogAccessStream* logReader, IXmlStreamFlusher* flusher, const WUComponentLogOptions& options) +{ + StringBuffer s(""); + flusher->flushXML(s, false); + unsigned totalRecsRead = sendComponentLogContent(context, logReader, flusher, options); + if (totalRecsRead == options.logFetchOptions.getLimit()) //Warn of possible truncation + { + VStringBuffer truncationWarnmessage("Log query reached record limit (%u). Log report could be incomplete.", options.logFetchOptions.getLimit()); + LOG(MCuserInfo, "WsWuInfo: %s", truncationWarnmessage.str()); + VStringBuffer xmlMessage("\n", truncationWarnmessage.str()); //close lines element, append comment message + flusher->flushXML(xmlMessage, true); + } + else + { + s.set(""); + flusher->flushXML(s, true); + } +} + +unsigned WsWuInfo::sendComponentLogContent(IEspContext* context, IRemoteLogAccessStream* logReader, IXmlStreamFlusher* flusher, const WUComponentLogOptions& options) +{ + CESPAbortRequestCallback abortCallback(context); + + StringBuffer logContent; + unsigned totalRecsRead = 0; + unsigned recsRead = 0; + while (logReader->readLogEntries(logContent.clear(), recsRead)) + { + if (options.logFormat == LOGACCESS_LOGFORMAT_json && totalRecsRead > 0 && recsRead > 0) + { + StringBuffer s(','); + flusher->flushXML(s, false); + } + + totalRecsRead += recsRead; + flusher->flushXML(logContent, false); + + if (abortCallback.abortRequested()) + { + LOG(MCdebugInfo, unknownJob, "WsWuInfo::sendComponentLogContent(): abort requested via callback"); + break; + } + } + return totalRecsRead; +} +#endif + void WsWuInfo::getSourceFiles(IEspECLWorkunit &info, unsigned long flags) { if (!(flags & WUINFO_IncludeSourceFiles)) @@ -655,7 +759,16 @@ void WsWuInfo::getHelpers(IEspECLWorkunit &info, unsigned long flags) for (unsigned i = 0; i < FileTypeSize; i++) getHelpFiles(query, (WUFileType) i, helpers, flags, helpersCount); } -#ifndef _CONTAINERIZED +#ifdef _CONTAINERIZED + helpersCount++; + if ((flags & WUINFO_IncludeHelpers)) + { + Owned h = createECLHelpFile(); + h->setName(File_ComponentLog); + h->setType(File_ComponentLog); + helpers.append(*h.getLink()); + } +#else getWorkunitThorLogInfo(helpers, info, flags, helpersCount); if (cw->getWuidVersion() > 0) @@ -3922,6 +4035,127 @@ void CWsWuFileHelper::createWULogFile(IConstWorkUnit *cwu, WsWuInfo &winfo, cons } } +#ifdef _CONTAINERIZED +void CWsWuFileHelper::sendWUComponentLogStreaming(CHttpRequest* request, CHttpResponse* response) +{ + StringBuffer wuid, fileName; + request->getParameter("Wuid", wuid); + if (wuid.isEmpty()) + throw makeStringException(ECLWATCH_INVALID_INPUT, "Empty Wuid detected"); + request->getParameter("Name", fileName); + if (fileName.isEmpty()) + throw makeStringException(ECLWATCH_INVALID_INPUT, "Empty Name detected"); + + IEspContext* ctx = request->queryContext(); + Owned espRequest = new CWULogFileRequest(ctx, "WsWorkunits", request->queryParameters(), request->queryAttachments()); + + WUComponentLogOptions options; + readWUComponentLogOptionsReq(espRequest->getFileOptions(), options); + + WsWuInfo winfo(*ctx, wuid.str()); + int option = request->getParameterInt("Option", CWUFileDownloadOption_OriginalText); //0: original content; 1: content as attachment; 2: zip; 3: gzip. + if ((option < CWUFileDownloadOption_OriginalText) || (option > CWUFileDownloadOption_GZIP)) + throw makeStringExceptionV(ECLWATCH_INVALID_INPUT, "Invalid WU File Download option detected: '%d'", option); + + CWUFileDownloadOption opt = (CWUFileDownloadOption) option; + if ((opt == CWUFileDownloadOption_OriginalText) || (opt == CWUFileDownloadOption_Attachment)) + { + if (opt == CWUFileDownloadOption_OriginalText) + { + if (options.logFormat == LOGACCESS_LOGFORMAT_csv) + ctx->setResponseFormat(ESPSerializationCSV); + else if (options.logFormat == LOGACCESS_LOGFORMAT_json) + ctx->setResponseFormat(ESPSerializationJSON); + else + ctx->setResponseFormat(ESPSerializationXML); + } + else + { + VStringBuffer headerStr("attachment;filename=%s", fileName.str()); + if (options.logFormat == LOGACCESS_LOGFORMAT_csv) + headerStr.append(".csv"); + else if (options.logFormat == LOGACCESS_LOGFORMAT_json) + headerStr.append(".json"); + else + headerStr.append(".xml"); + ctx->addCustomerHeader("Content-disposition", headerStr.str()); + } + + winfo.sendWorkunitComponentLogs(ctx, response, options); + } + else + { //zip or gzip the WUComponentLog + StringBuffer workingFolder, resultFileNameWithPath; + + unsigned currentTime = msTick(); + workingFolder.setf("%s%sT%xAT%x", TEMPZIPDIR, PATHSEPSTR, (unsigned)(memsize_t)GetCurrentThreadId(), currentTime); + resultFileNameWithPath.setf("%s%s%s", workingFolder.str(), PATHSEPSTR, fileName.str()); + recursiveCreateDirectoryForFile(resultFileNameWithPath); + //Read the WUComponentLog into a file + winfo.readWorkunitComponentLogs(resultFileNameWithPath, options.logFetchOptions.getLimit(), options.logFetchOptions.getReturnColsMode(), options.logFormat, options.wuLogSearchTimeBuffSecs); + + VStringBuffer headerStr("attachment;filename=%s", fileName.str()); + if (opt == CWUFileDownloadOption_GZIP) + headerStr.append(".gz"); + else + headerStr.append(".zip"); + ctx->addCustomerHeader("Content-disposition", headerStr.str()); + + //zip or gzip + StringBuffer zipFileNameWithPath(resultFileNameWithPath); + zipFileNameWithPath.append((opt == CWUFileDownloadOption_GZIP) ? ".gz" : ".zip"); + StringBuffer zipCommand; + if (opt == CWUFileDownloadOption_GZIP) + zipCommand.setf("tar -czf %s -C %s %s", zipFileNameWithPath.str(), workingFolder.str(), fileName.str()); + else + zipCommand.setf("zip -j %s %s", zipFileNameWithPath.str(), resultFileNameWithPath.str()); + if (system(zipCommand) != 0) + throw makeStringExceptionV(ECLWATCH_CANNOT_COMPRESS_DATA, "Failed to execute system command '%s'. Please make sure that zip utility is installed.", + (opt == CWUFileDownloadOption_GZIP) ? "tar" : "zip"); + + //Send the zipped file and clean the workingFolder. + response->setContent(createIOStreamWithFileName(zipFileNameWithPath, IFOread)); + if (opt == CWUFileDownloadOption_GZIP) + response->setContentType("application/x-gzip"); + else + response->setContentType("application/zip"); + response->send(); + recursiveRemoveDirectory(workingFolder); + } +} + +void CWsWuFileHelper::readWUComponentLogOptionsReq(IConstWUFileOption& logOptionReq, WUComponentLogOptions& options) +{ + //If MaxLogRecords is in logOptionReq, use it; otherwise, default to 100 in LogAccessConditions. + if (!logOptionReq.getMaxLogRecords_isNull()) + options.logFetchOptions.setLimit(logOptionReq.getMaxLogRecords()); + //If LogSearchTimeBuffSecs is in logOptionReq, use it; otherwise, default to defaultWULogSearchTimeBufferSecs. + if (!logOptionReq.getLogSearchTimeBuffSecs_isNull()) + options.wuLogSearchTimeBuffSecs = logOptionReq.getLogSearchTimeBuffSecs(); + CLogAccessLogFormat logFormatSetting = logOptionReq.getLogFormat(); + if (logFormatSetting != LogAccessLogFormat_Undefined) + options.logFormat = (LogAccessLogFormat) logFormatSetting; + else + options.logFormat = LOGACCESS_LOGFORMAT_csv; + switch (logOptionReq.getLogSelectColumnMode()) + { + case CLogSelectColumnMode_MIN: + options.logFetchOptions.setReturnColsMode(RETURNCOLS_MODE_min); + break; + case CLogSelectColumnMode_ALL: + options.logFetchOptions.setReturnColsMode(RETURNCOLS_MODE_all); + break; + case CLogSelectColumnMode_CUSTOM: + options.logFetchOptions.setReturnColsMode(RETURNCOLS_MODE_custom); + options.logFetchOptions.copyLogFieldNames(logOptionReq.getLogColumns()); + break; + default: + options.logFetchOptions.setReturnColsMode(RETURNCOLS_MODE_default); + break; + } +} +#endif + void CWsWuFileHelper::createZAPWUGraphProgressFile(const char* wuid, const char* pathNameStr) { Owned graphProgress = getWUGraphProgress(wuid, true); @@ -4332,6 +4566,34 @@ void CWsWuFileHelper::readWUFile(const char* wuid, const char* workingFolder, Ws winfo.getWorkunitEclAgentLog(nullptr, file, item.getProcess(), mb, fileNameWithPath.str()); break; } +#else + case CWUFileType_ComponentLog: + { + WUComponentLogOptions options; + readWUComponentLogOptionsReq(item, options); + + fileName.set(File_ComponentLog); + if (options.logFormat == LOGACCESS_LOGFORMAT_csv) + { + fileMimeType.set(HTTP_TYPE_TEXT_PLAIN); + fileName.append(".csv"); + } + else if (options.logFormat == LOGACCESS_LOGFORMAT_json) + { + fileMimeType.set(HTTP_TYPE_JSON); + fileName.append(".json"); + } + else + { + fileMimeType.set(HTTP_TYPE_APPLICATION_XML); + fileName.append(".xml"); + } + fileNameWithPath.set(workingFolder).append(PATHSEPCHAR).append(fileName.str()); + + winfo.readWorkunitComponentLogs(fileNameWithPath, options.logFetchOptions.getLimit(), options.logFetchOptions.getReturnColsMode(), + options.logFormat, options.wuLogSearchTimeBuffSecs); + break; + } #endif case CWUFileType_XML: { diff --git a/esp/services/ws_workunits/ws_workunitsHelpers.hpp b/esp/services/ws_workunits/ws_workunitsHelpers.hpp index 578a92c095c..aa950f7bd48 100644 --- a/esp/services/ws_workunits/ws_workunitsHelpers.hpp +++ b/esp/services/ws_workunits/ws_workunitsHelpers.hpp @@ -26,6 +26,7 @@ #include "hqlerror.hpp" #include "dllserver.hpp" #include "mpbase.hpp" +#include "httpresponsebuffer.ipp" #include #include @@ -43,6 +44,8 @@ namespace ws_workunits { #define File_ThorLog "ThorLog" #define File_ThorSlaveLog "ThorSlaveLog" #define File_EclAgentLog "EclAgentLog" +#else +#define File_ComponentLog "ComponentLog" #endif #define File_XML "XML" @@ -146,6 +149,13 @@ static constexpr unsigned defaultMaxLogRecords = 10000; static constexpr unsigned defaultWULogSearchTimeBufferSecs = 600; static constexpr unsigned microSecsToSecDivisor = 1000000; +struct WUComponentLogOptions +{ + LogAccessConditions logFetchOptions; + LogAccessLogFormat logFormat = LOGACCESS_LOGFORMAT_csv; + unsigned wuLogSearchTimeBuffSecs = defaultWULogSearchTimeBufferSecs; +}; + class WsWuInfo { IEspWUArchiveFile* readArchiveFileAttr(IPropertyTree& fileTree, const char* path); @@ -157,12 +167,19 @@ class WsWuInfo bool parseLogLine(const char* line, const char* endWUID, unsigned& processID, const unsigned columnNumPID); void readWorkunitThorLog(const char* processName, const char* logSpec, const char* slaveIPAddress, unsigned slaveNum, MemoryBuffer& buf, const char* outFile); void readWorkunitThorLogOneDay(IFile* ios, unsigned& processID, MemoryBuffer& buf, IFileIOStream* outIOS); +#else + unsigned sendComponentLogContent(IEspContext* context, IRemoteLogAccessStream* logreader, IXmlStreamFlusher* flusher, const WUComponentLogOptions& options); + void sendComponentLogCSV(IEspContext* context, IRemoteLogAccessStream* logreader, IXmlStreamFlusher* flusher, const WUComponentLogOptions& options); + void sendComponentLogJSON(IEspContext* context, IRemoteLogAccessStream* logreader, IXmlStreamFlusher* flusher, const WUComponentLogOptions& options); + void sendComponentLogXML(IEspContext* context, IRemoteLogAccessStream* logreader, IXmlStreamFlusher* flusher, const WUComponentLogOptions& options); #endif void readFileContent(const char* sourceFileName, const char* sourceIPAddress, const char* sourceAlias, MemoryBuffer &mb, bool forDownload); void copyContentFromRemoteFile(const char* sourceFileName, const char* sourceIPAddress, const char* sourceAlias, const char *outFileName); void initWULogReader(); + void setLogTimeRange(LogAccessConditions& logFetchOptions, unsigned wuLogSearchTimeBuffSecs); + public: /* @@ -173,8 +190,9 @@ class WsWuInfo * LogAccessLogFormat logFormat - Declares the log report format * unsigned wuLogSearchTimeBuffSecs - Defines the query time-window before wu creation and after wu end */ - void readWorkunitComponentLogs(const char* outFile, unsigned maxLogRecords, LogAccessReturnColsMode retColsMode, - LogAccessLogFormat logFormat, unsigned wuLogSearchTimeBuffSecs); + void readWorkunitComponentLogs(const char* outFile, unsigned maxLogRecords, const LogAccessReturnColsMode retColsMode, + const LogAccessLogFormat logFormat, unsigned wuLogSearchTimeBuffSecs); + void sendWorkunitComponentLogs(IEspContext* context, CHttpResponse* response, WUComponentLogOptions& options); WsWuInfo(IEspContext &ctx, IConstWorkUnit *cw_) : context(ctx), cw(cw_) @@ -665,6 +683,8 @@ class CWsWuFileHelper #ifndef _CONTAINERIZED void createProcessLogfile(IConstWorkUnit *cwu, WsWuInfo &winfo, const char *process, const char *path); void createThorSlaveLogfile(IConstWorkUnit *cwu, WsWuInfo &winfo, const char *path); +#else + void readWUComponentLogOptionsReq(IConstWUFileOption &logOptionReq, WUComponentLogOptions &options); #endif void createWULogFile(IConstWorkUnit *cwu, WsWuInfo &winfo, const char *path, unsigned maxLogRecords, LogAccessReturnColsMode retColsMode, LogAccessLogFormat logFormat, unsigned wuLogSearchTimeBuffSecs); void writeZAPWUInfoToIOStream(IFileIOStream *outFile, const char *name, SCMStringBuffer &value); @@ -683,6 +703,9 @@ class CWsWuFileHelper IFileIOStream* createIOStreamWithFileName(const char *fileNameWithPath, IFOmode mode); void validateFilePath(const char *file, WsWuInfo &winfo, CWUFileType wuFileType, bool UNCFileName, const char *fileType, const char *compType, const char *compName); bool validateWUFile(const char *file, WsWuInfo &winfo, CWUFileType wuFileType); +#ifdef _CONTAINERIZED + void sendWUComponentLogStreaming(CHttpRequest *request, CHttpResponse *response); +#endif }; class CWsWuEmailHelper diff --git a/esp/src/eclwatch/HelpersWidget.js b/esp/src/eclwatch/HelpersWidget.js index baaafaef858..8f9dbdebd88 100644 --- a/esp/src/eclwatch/HelpersWidget.js +++ b/esp/src/eclwatch/HelpersWidget.js @@ -49,6 +49,9 @@ define([ case "res": params = "/WUFile/res.txt?Wuid=" + this.wu.Wuid + "&Type=" + item.Orig.Type; break; + case "ComponentLog": + params = "/WUFile/" + item.Type + "?Wuid=" + this.wu.Wuid + "&Name=" + item.Orig.Name + "&Type=" + item.Orig.Type; + break; case "EclAgentLog": params = "/WUFile/" + item.Type + "?Wuid=" + this.wu.Wuid + "&Process=" + item.Orig.PID + "&Name=" + item.Orig.Name + "&Type=" + item.Orig.Type; break; diff --git a/esp/src/src-react/components/Helpers.tsx b/esp/src/src-react/components/Helpers.tsx index 7ea832b4387..73de7fac50c 100644 --- a/esp/src/src-react/components/Helpers.tsx +++ b/esp/src/src-react/components/Helpers.tsx @@ -30,6 +30,9 @@ function getURL(item: HelperRow, option) { case "res": params = "/WUFile/res.txt?Wuid=" + item.workunit.Wuid + "&Type=" + item.Orig.Type; break; + case "ComponentLog": + params = "/WUFile/" + item.Type + "?Wuid=" + item.workunit.Wuid + "&Name=" + item.Orig.Name + "&Type=" + item.Orig.Type; + break; case "EclAgentLog": params = "/WUFile/" + item.Type + "?Wuid=" + item.workunit.Wuid + "&Process=" + item.Orig.PID + "&Name=" + item.Orig.Name + "&Type=" + item.Orig.Type; break; From 9727aad93db217c03eb46a80cc8952120c4489c3 Mon Sep 17 00:00:00 2001 From: Jake Smith Date: Wed, 4 May 2022 11:55:03 +0100 Subject: [PATCH 09/24] HPCC-27584 NP selector should be on dynamic thor-eclagent label value Signed-off-by: Jake Smith --- helm/hpcc/templates/thor.yaml | 43 ++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/helm/hpcc/templates/thor.yaml b/helm/hpcc/templates/thor.yaml index 9b4e43a40c8..f82781294ed 100644 --- a/helm/hpcc/templates/thor.yaml +++ b/helm/hpcc/templates/thor.yaml @@ -144,11 +144,13 @@ data: template: metadata: labels: - app: "_HPCC_JOBNAME_" - component: "thormanager" accessDali: "yes" accessEsp: "yes" + app: "thor" + component: "thormanager" helmVersion: 8.6.25-closedown0 + instance: "_HPCC_JOBNAME_" + job: "_HPCC_JOBNAME_" {{- if hasKey $thorScope "labels" }} {{ toYaml $thorScope.labels | indent 12 }} {{- end }} @@ -210,10 +212,12 @@ data: template: metadata: labels: - app: "_HPCC_JOBNAME_" - component: "thorworker" accessEsp: "true" + app: "thor" + component: "thorworker" helmVersion: 8.6.25-closedown0 + instance: "_HPCC_JOBNAME_" + job: "_HPCC_JOBNAME_" {{- if hasKey $thorScope "labels" }} {{ toYaml $thorScope.labels | indent 12 }} {{- end }} @@ -275,18 +279,20 @@ data: spec: podSelector: matchLabels: - app: "_HPCC_JOBNAME_" + app: thor component: thormanager + job: "_HPCC_JOBNAME_" ingress: - from: - podSelector: matchExpressions: - - {key: app, operator: In, values: ["thor-eclagent", "_HPCC_JOBNAME_"]} + - {key: instance, operator: In, values: ["{{.eclAgentName}}", "_HPCC_JOBNAME_"]} egress: - to: - podSelector: matchLabels: - app: "_HPCC_JOBNAME_" + app: thor + job: "_HPCC_JOBNAME_" thorworker-networkspec.yaml: apiVersion: networking.k8s.io/v1 @@ -296,18 +302,21 @@ data: spec: podSelector: matchLabels: - app: "_HPCC_JOBNAME_" + app: thor component: thorworker + job: "_HPCC_JOBNAME_" ingress: - from: - podSelector: matchLabels: - app: "_HPCC_JOBNAME_" + app: thor + job: "_HPCC_JOBNAME_" egress: - to: - podSelector: matchLabels: - app: "_HPCC_JOBNAME_" + app: thor + job: "_HPCC_JOBNAME_" {{- end -}} {{- $local := dict "first" true }} @@ -331,16 +340,16 @@ spec: replicas: {{ $commonCtx.eclAgentReplicas }} selector: matchLabels: - run: {{ $commonCtx.eclAgentName | quote }} - app: thor-eclagent + instance: {{ $commonCtx.eclAgentName | quote }} template: metadata: labels: - run: {{ $commonCtx.eclAgentName | quote }} - app: thor-eclagent accessDali: "yes" accessEsp: {{ $commonCtx.eclAgentUseChildProcesses | ternary "yes" "no" | quote }} + app: "thor" + component: "thor-eclagent" helmVersion: 8.6.25-closedown0 + instance: {{ $commonCtx.eclAgentName | quote }} {{- if hasKey $commonCtx.me "labels" }} {{ toYaml $commonCtx.me.labels | indent 8 }} {{- end }} @@ -392,14 +401,16 @@ spec: replicas: {{ $commonCtx.thorAgentReplicas }} selector: matchLabels: - run: {{ $commonCtx.thorAgentName | quote }} + instance: {{ $commonCtx.thorAgentName | quote }} template: metadata: labels: - run: {{ $commonCtx.thorAgentName | quote }} accessDali: "yes" accessEsp: "no" + app: "thor" + component: "thor-thoragent" helmVersion: 8.6.25-closedown0 + instance: {{ $commonCtx.thorAgentName | quote }} {{- if hasKey $commonCtx.me "labels" }} {{ toYaml $commonCtx.me.labels | indent 8 }} {{- end }} From bf9603251749fe2a80a9af7a61d38727d99652c0 Mon Sep 17 00:00:00 2001 From: Russ Whitehead Date: Tue, 26 Apr 2022 15:41:39 -0400 Subject: [PATCH 10/24] HPCC-27572 Remove unsupported characters from generated password Certain password characters are not accepted on the kubectl create secret command line. This PR removes those characters from the set available to be generated. Also replaces the unsafe rand() function with Mersenne Twister (std::my19937) Signed-off-by: Russ Whitehead --- system/jlib/jutil.cpp | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/system/jlib/jutil.cpp b/system/jlib/jutil.cpp index e3405eaaceb..e0412deb33b 100644 --- a/system/jlib/jutil.cpp +++ b/system/jlib/jutil.cpp @@ -56,6 +56,8 @@ #include "portlist.h" +#include + static NonReentrantSpinLock * cvtLock; #ifdef _WIN32 @@ -3349,33 +3351,36 @@ const char * generatePassword(StringBuffer &pwd, int pwdLen) const char alphaUC[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const char alphaLC[] = "abcdefghijklmnopqrstuvwxyz"; const char numeric[] = "0123456789"; - const char symbol[] = "~@#$%^*()_-+={[}]:,.?"; + const char symbol[] = "~@#%^*_-+{[}]:,.?"; + + std::random_device seedGen;//uniformly-distributed integer random number generator that produces non-deterministic random numbers + std::mt19937 mtEngine(seedGen());//generates 32-bit pseudo-random numbers using Mersenne twister algorithm //Ensures each character group used at least once - srand(time(0)); - pwd.append(alphaUC[rand() % (sizeof(alphaUC) - 1)]); - pwd.append(alphaLC[rand() % (sizeof(alphaLC) - 1)]); - pwd.append(numeric[rand() % (sizeof(numeric) - 1)]); - pwd.append(symbol[rand() % (sizeof(symbol) - 1)]); + pwd.append(alphaUC[mtEngine() % (sizeof(alphaUC) - 1)]); + pwd.append(alphaLC[mtEngine() % (sizeof(alphaLC) - 1)]); + pwd.append(numeric[mtEngine() % (sizeof(numeric) - 1)]); + pwd.append(symbol[mtEngine() % (sizeof(symbol) - 1)]); for (int i = 4; i < pwdLen; i++) { - switch(rand() % NUM_GROUPS)//select a random character group + switch(mtEngine() % NUM_GROUPS)//select a random character group { case 0: - pwd.append(alphaUC[rand() % (sizeof(alphaUC) - 1)]); break; + pwd.append(alphaUC[mtEngine() % (sizeof(alphaUC) - 1)]); + break; case 1: - pwd.append(alphaLC[rand() % (sizeof(alphaLC) - 1)]); break; + pwd.append(alphaLC[mtEngine() % (sizeof(alphaLC) - 1)]); + break; case 2: - pwd.append(numeric[rand() % (sizeof(numeric) - 1)]); break; + pwd.append(numeric[mtEngine() % (sizeof(numeric) - 1)]); + break; case 3: - pwd.append(symbol[rand() % (sizeof(symbol) - 1)]); break; + pwd.append(symbol[mtEngine() % (sizeof(symbol) - 1)]); + break; } } -#ifdef _DEBUG - if (pwd.length() != (size32_t)pwdLen) - throw makeStringException(0, "Generated Passwords wrong length!"); -#endif + return pwd.str(); } From 7462ee7fe8fb8583cd3ae915b4b0d58d1a779a3d Mon Sep 17 00:00:00 2001 From: Rodrigo Pastrana Date: Mon, 25 Apr 2022 18:00:31 +0100 Subject: [PATCH 11/24] HPCC-26876 Add LogAccess Compound Filter support - Adds support for binary AND/OR operations - Changes Elastic Search query to query_string - Adds support for user defined column based filter Signed-off-by: Rodrigo Pastrana --- esp/scm/ws_logaccess.ecm | 84 ++++-- .../ws_logaccess/WsLogAccessService.cpp | 142 +++++++-- system/jlib/jlog.cpp | 12 +- system/jlib/jlog.hpp | 26 +- system/jlib/jlog.ipp | 55 +++- .../ElasticStack/ElasticStackLogAccess.cpp | 275 ++++++++++-------- .../ElasticStack/ElasticStackLogAccess.hpp | 2 + 7 files changed, 416 insertions(+), 180 deletions(-) diff --git a/esp/scm/ws_logaccess.ecm b/esp/scm/ws_logaccess.ecm index 89739dddc58..86716001de2 100644 --- a/esp/scm/ws_logaccess.ecm +++ b/esp/scm/ws_logaccess.ecm @@ -23,7 +23,8 @@ ESPenum LogAccessType : int ByLogType(3, "ByLogType"), ByTargetAudience(4, "ByTargetAudience"), BySourceInstance(5, "BySourceInstance"), - BySourceNode(6, "BySourceNode") + BySourceNode(6, "BySourceNode"), + ByFieldName(7, "ByFieldName") }; ESPenum LogAccessLogFormat : int @@ -56,45 +57,78 @@ ESPResponse GetLogAccessInfoResponse string RemoteLogManagerType; string RemoteLogManagerConnectionString; }; + +ESPenum LogAccessFilterOperator : int +{ + NONE(0, "NONE"), + AND(1, "AND"), + OR(2, "OR") +}; + /* * Provides mechanism to query log entries * -* Caller can query by JobId, component, log event type, or target audience by providing the appropriate +* Caller can query by JobId, component, log event type, target audience, source instance, or host by providing the appropriate * enumerated value in the LogCategory field, as well as the targeted value in the SearchByValue field. +* Caller can also query by field name directly by choosing the 'ByFieldName' option and providing the field name, and desired filter value. * * SearchbyValue is optional if LogCategory == ALL * +* SearchbyValue contains value used to identify target log entries. +* Limited to values associated with the the LogCategory choice. +* +* If searching by "ByJobIdID", the SearchByValue should contain the jobid of interest +* If searching by "ByComponent", the SearchByValue should contain the component of interest +* If searching by "ByLogType", the SearchByValue should contain the 3 letter code associated with the log type of interest. +* valid values at time of writing are: +* DIS - Disaster +* ERR - Error +* WRN - Warning +* INF - Information +* PRO - Progress +* MET - Metric +* +*If searching by "ByTargetAudience", the SearchByValue should contain the 3 letter code associated with the target audience of interest. +* valid values at time of writing are: +* OPR - Operator +* USR - User +* PRO - Programmer +* ADT - Audit +*If searching by "BySourceInstance", the SearchByValue should contain the instance of interest +*If searching by "BySourceNode", the SearchByValue should contain the node of interest +*If searching by "ByFieldName", the SearchByValue should contain the field value of interest, and the SearchField +* should be populated. * Caller should restrict the query to target a specific time range specified in the Range field. -* By default, the first 100 log entries encountered are returned. Caller to pagenize using the +* By default, the first 100 log entries encountered are returned. Caller to paginate using the * LogLineStartFrom field (specifies as zero-indexed start index) and the LogLineLimit (specifies the maximum * number of log entries to be returned) * Caller can specify which log columns should be reported via the Columns field, all available columns returned by default. * * The report format can be specified via the Format field: JSON|XML|CSV */ -ESPRequest GetLogsRequest +ESPStruct LogFilter { ESPenum LogAccessType LogCategory; - string SearchByValue; //Value used to identify target log entries. - //Limited to values associated with the the LogCategory choice. - // - //If searching by "ByJobIdID", the SearchByValue should contain the jobid of interest - //If searching by "ByComponent", the SearchByValue should contain the component of interest - //If searching by "ByLogType", the SearchByValue should contain the 3 letter code associated with the log type of interest. - // valid values at time of writing are: - // DIS - Disaster - // ERR - Error - // WRN - Warning - // INF - Information - // PRO - Progress - // MET - Metric - // - //If searching by "ByTargetAudience", the SearchByValue should contain the 3 letter code associated with the target audience of interest. - // valid values at time of writing are: - // OPR - Operator - // USR - User - // PRO - Programmer - // ADT - Audit + string SearchByValue; + string SearchField; +}; + +ESPStruct BinaryLogFilter +{ + ESPStruct LogFilter leftFilter; + ESParray leftBinaryFilter; + + ESPenum LogAccessFilterOperator Operator; + + ESPStruct LogFilter rightFilter; + ESParray rightBinaryFilter; +}; + +ESPRequest GetLogsRequest +{ + [min_ver("1.02")] ESPStruct BinaryLogFilter Filter; + [depr_ver("1.02")] ESPenum LogAccessType LogCategory; + [depr_ver("1.02")] string SearchByValue; //Value used to identify target log entries. ESPStruct TimeRange Range; unsigned LogLineLimit(100); int64 LogLineStartFrom(0); @@ -108,7 +142,7 @@ ESPResponse GetLogsResponse string LogLines; }; -ESPservice [auth_feature("WsLogAccess:READ"), version("1.01"), default_client_version("1.01"), exceptions_inline("xslt/exceptions.xslt")] ws_logaccess +ESPservice [auth_feature("WsLogAccess:READ"), version("1.02"), default_client_version("1.02"), exceptions_inline("xslt/exceptions.xslt")] ws_logaccess { ESPmethod GetLogAccessInfo(GetLogAccessInfoRequest, GetLogAccessInfoResponse); ESPmethod GetLogs(GetLogsRequest, GetLogsResponse); diff --git a/esp/services/ws_logaccess/WsLogAccessService.cpp b/esp/services/ws_logaccess/WsLogAccessService.cpp index a571b1452a8..2e07daa45dd 100644 --- a/esp/services/ws_logaccess/WsLogAccessService.cpp +++ b/esp/services/ws_logaccess/WsLogAccessService.cpp @@ -52,36 +52,42 @@ LogAccessTimeRange requestedRangeToLARange(IConstTimeRange & reqrange) return range; } -bool Cws_logaccessEx::onGetLogs(IEspContext &context, IEspGetLogsRequest &req, IEspGetLogsResponse & resp) +LogAccessFilterType cLogAccessFilterOperator2LogAccessFilterType(CLogAccessFilterOperator cLogAccessFilterOperator) { - if (!m_remoteLogAccessor) - throw makeStringException(-1, "WsLogAccess: Remote Log Access plug-in not available!"); + switch (cLogAccessFilterOperator) + { + case CLogAccessFilterOperator_NONE: + return LOGACCESS_FILTER_unknown; + case CLogAccessFilterOperator_AND: + return LOGACCESS_FILTER_and; + case CLogAccessFilterOperator_OR: + return LOGACCESS_FILTER_or; + case LogAccessFilterOperator_Undefined: + default: + throw makeStringException(-1, "WsLogAccess: Cannot convert log filter operator!"); + } +} - CLogAccessType searchByCategory = req.getLogCategory(); - const char * searchByValue = req.getSearchByValue(); - if (searchByCategory != CLogAccessType_All && isEmptyString(searchByValue)) - throw makeStringException(-1, "WsLogAccess::onGetLogs: Must provide log category"); +ILogAccessFilter * buildLogFilterByFields(CLogAccessType searchByCategory, const char * searchByValue, const char * serchField) +{ + if (isEmptyString(searchByValue) && searchByCategory != CLogAccessType_All) + throw makeStringException(-1, "WsLogAccess: Empty searchByValue detected"); - LogAccessConditions logFetchOptions; switch (searchByCategory) { case CLogAccessType_All: - logFetchOptions.setFilter(getWildCardLogAccessFilter()); - break; + return getWildCardLogAccessFilter(); case CLogAccessType_ByJobIdID: - logFetchOptions.setFilter(getJobIDLogAccessFilter(searchByValue)); - break; + return getJobIDLogAccessFilter(searchByValue); case CLogAccessType_ByComponent: - logFetchOptions.setFilter(getComponentLogAccessFilter(searchByValue)); - break; + return getComponentLogAccessFilter(searchByValue); case CLogAccessType_ByLogType: { LogMsgClass logType = LogMsgClassFromAbbrev(searchByValue); if (logType == MSGCLS_unknown) throw makeStringExceptionV(-1, "Invalid Log Type 3-letter code encountered: '%s' - Available values: 'DIS,ERR,WRN,INF,PRO,MET'", searchByValue); - logFetchOptions.setFilter(getClassLogAccessFilter(logType)); - break; + return getClassLogAccessFilter(logType); } case CLogAccessType_ByTargetAudience: { @@ -89,26 +95,118 @@ bool Cws_logaccessEx::onGetLogs(IEspContext &context, IEspGetLogsRequest &req, I if (targetAud == MSGAUD_unknown || targetAud == MSGAUD_all) throw makeStringExceptionV(-1, "Invalid Target Audience 3-letter code encountered: '%s' - Available values: 'OPR,USR,PRO,ADT'", searchByValue); - logFetchOptions.setFilter(getAudienceLogAccessFilter(targetAud)); - break; + return getAudienceLogAccessFilter(targetAud); } case CLogAccessType_BySourceInstance: { - logFetchOptions.setFilter(getInstanceLogAccessFilter(searchByValue)); - break; + return getInstanceLogAccessFilter(searchByValue); } case CLogAccessType_BySourceNode: { - logFetchOptions.setFilter(getHostLogAccessFilter(searchByValue)); + return getHostLogAccessFilter(searchByValue); break; } + case CLogAccessType_ByFieldName: + { + return getColumnLogAccessFilter(serchField, searchByValue); + } case LogAccessType_Undefined: default: throw makeStringException(-1, "Invalid remote log access request type"); } +} + +ILogAccessFilter * buildLogFilter(IConstLogFilter * logFilter) +{ + if (logFilter) + return buildLogFilterByFields(logFilter->getLogCategory(), logFilter->getSearchByValue(), logFilter->getSearchField()); + else + return nullptr; +} + +bool isLogFilterEmpty(IConstLogFilter * logFilter) +{ + if (!logFilter) + return true; + + return isEmptyString(logFilter->getSearchByValue()) && logFilter->getLogCategory() == LogAccessType_Undefined && isEmptyString(logFilter->getSearchField()); +} + +ILogAccessFilter * buildBinaryLogFilter(IConstBinaryLogFilter * binaryfilter) +{ + if (!binaryfilter) + return nullptr; + + ILogAccessFilter * leftFilter = nullptr; + if (binaryfilter->getLeftBinaryFilter().ordinality() == 0) + { + leftFilter = buildLogFilter(&binaryfilter->getLeftFilter()); + } + else + { + if (binaryfilter->getLeftBinaryFilter().ordinality() > 1) + throw makeStringException(-1, "WsLogAccess: LeftBinaryFilter cannot contain multiple entries!"); + + if (!isLogFilterEmpty(&binaryfilter->getLeftFilter())) + throw makeStringException(-1, "WsLogAccess: Cannot submit leftFilter and leftBinaryFilter!"); + + leftFilter = buildBinaryLogFilter(&binaryfilter->getLeftBinaryFilter().item(0)); + } + + if (!leftFilter) + throw makeStringExceptionV(-1, "WsLogAccess: Empty LEFT filter encountered"); + + switch (binaryfilter->getOperator()) + { + case CLogAccessFilterOperator_NONE: + case LogAccessFilterOperator_Undefined: //no operator found + //if (rightFilter != nullptr) + // WARNLOG("right FILTER ENCOUNTERED but no valid operator"); + return leftFilter; + case CLogAccessFilterOperator_AND: + case CLogAccessFilterOperator_OR: + { + ILogAccessFilter * rightFilter = nullptr; + if (binaryfilter->getRightBinaryFilter().ordinality() == 0) + { + rightFilter = buildLogFilter(&binaryfilter->getRightFilter()); + } + else + { + if (binaryfilter->getRightBinaryFilter().ordinality() > 1) + throw makeStringException(-1, "WsLogAccess: RightBinaryFilter cannot contain multiple entries!"); + + if (!isLogFilterEmpty(&binaryfilter->getRightFilter())) + throw makeStringException(-1, "WsLogAccess: Cannot submit rightFilter and rightBinaryFilter!"); + + rightFilter = buildBinaryLogFilter(&binaryfilter->getRightBinaryFilter().item(0)); + } + + if (!rightFilter) + throw makeStringExceptionV(-1, "WsLogAccess: Empty RIGHT filter encountered"); + + return getBinaryLogAccessFilterOwn(leftFilter, rightFilter, cLogAccessFilterOperator2LogAccessFilterType(binaryfilter->getOperator())); + } + + default: + throw makeStringExceptionV(-1, "WsLogAccess: Invalid log access filter operator encountered '%d'", binaryfilter->getOperator()); + } +} + +bool Cws_logaccessEx::onGetLogs(IEspContext &context, IEspGetLogsRequest &req, IEspGetLogsResponse & resp) +{ + if (!m_remoteLogAccessor) + throw makeStringException(-1, "WsLogAccess: Remote Log Access plug-in not available!"); - LogAccessTimeRange range = requestedRangeToLARange(req.getRange()); double version = context.getClientVersion(); + LogAccessConditions logFetchOptions; + if (version > 1.01) + logFetchOptions.setFilter(buildBinaryLogFilter(&req.getFilter())); + else + logFetchOptions.setFilter(buildLogFilterByFields(req.getLogCategory(), req.getSearchByValue(), nullptr)); + + LogAccessTimeRange range = requestedRangeToLARange(req.getRange()); + if (version > 1.0) { switch (req.getSelectColumnMode()) diff --git a/system/jlib/jlog.cpp b/system/jlib/jlog.cpp index 73c84d61937..5c76e31eb68 100644 --- a/system/jlib/jlog.cpp +++ b/system/jlib/jlog.cpp @@ -3426,6 +3426,11 @@ ILogAccessFilter * getJobIDLogAccessFilter(const char * jobId) return new FieldLogAccessFilter(jobId, LOGACCESS_FILTER_jobid); } +ILogAccessFilter * getColumnLogAccessFilter(const char * columnName, const char * value) +{ + return new ColumnLogAccessFilter(columnName, value, LOGACCESS_FILTER_column); +} + ILogAccessFilter * getComponentLogAccessFilter(const char * component) { return new FieldLogAccessFilter(component, LOGACCESS_FILTER_component); @@ -3449,12 +3454,13 @@ ILogAccessFilter * getBinaryLogAccessFilter(ILogAccessFilter * arg1, ILogAccessF ILogAccessFilter * getBinaryLogAccessFilterOwn(ILogAccessFilter * arg1, ILogAccessFilter * arg2, LogAccessFilterType type) { ILogAccessFilter * ret = new BinaryLogAccessFilter(arg1, arg2, type); - arg1->Release(); - arg2->Release(); + if (arg1) + arg1->Release(); + if (arg2) + arg2->Release(); return ret; } - // LOG ACCESS HELPER METHODS // Fetches log entries - based on provided filter, via provided IRemoteLogAccess instance diff --git a/system/jlib/jlog.hpp b/system/jlib/jlog.hpp index 83cd98cf2ae..e8519583331 100644 --- a/system/jlib/jlog.hpp +++ b/system/jlib/jlog.hpp @@ -1397,6 +1397,7 @@ typedef enum LOGACCESS_FILTER_wildcard, LOGACCESS_FILTER_instance, LOGACCESS_FILTER_host, + LOGACCESS_FILTER_column, LOGACCESS_FILTER_unknown } LogAccessFilterType; @@ -1417,9 +1418,9 @@ inline const char * logAccessFilterTypeToString(LogAccessFilterType field) case LOGACCESS_FILTER_host: return "host"; case LOGACCESS_FILTER_or: - return "or"; + return "OR"; case LOGACCESS_FILTER_and: - return "and"; + return "AND"; case LOGACCESS_FILTER_wildcard: return "*" ; default: @@ -1444,9 +1445,9 @@ inline unsigned logAccessFilterTypeFromName(char const * name) return LOGACCESS_FILTER_instance; if(strieq(name, "host")) return LOGACCESS_FILTER_host; - if(strieq(name, "or")) + if(strieq(name, "OR")) return LOGACCESS_FILTER_or; - if(strieq(name, "and")) + if(strieq(name, "AND")) return LOGACCESS_FILTER_and; return LOGACCESS_FILTER_unknown; } @@ -1457,6 +1458,21 @@ interface jlib_decl ILogAccessFilter : public IInterface virtual void addToPTree(IPropertyTree * tree) const = 0; virtual void toString(StringBuffer & out) const = 0; virtual LogAccessFilterType filterType() const = 0; + virtual ILogAccessFilter * leftFilterClause() const + { + return nullptr; + } + + virtual ILogAccessFilter * rightFilterClause() const + { + return nullptr; + } + + virtual const char * getFieldName() const + { + return nullptr; + } + }; enum LogAccessReturnColsMode @@ -1503,6 +1519,7 @@ struct LogAccessConditions { return filter.get(); } + void setFilter(ILogAccessFilter * _filter) { filter.setown(_filter); @@ -1618,6 +1635,7 @@ extern jlib_decl ILogAccessFilter * getClassLogAccessFilter(LogMsgClass logclass extern jlib_decl ILogAccessFilter * getBinaryLogAccessFilter(ILogAccessFilter * arg1, ILogAccessFilter * arg2, LogAccessFilterType type); extern jlib_decl ILogAccessFilter * getBinaryLogAccessFilterOwn(ILogAccessFilter * arg1, ILogAccessFilter * arg2, LogAccessFilterType type); extern jlib_decl ILogAccessFilter * getWildCardLogAccessFilter(); +extern jlib_decl ILogAccessFilter * getColumnLogAccessFilter(const char * columnName, const char * value); // Helper functions to actuate log access query extern jlib_decl bool fetchLog(StringBuffer & returnbuf, IRemoteLogAccess & logAccess, ILogAccessFilter * filter, LogAccessTimeRange timeRange, const StringArray & cols, LogAccessLogFormat format); diff --git a/system/jlib/jlog.ipp b/system/jlib/jlog.ipp index 0e8689e2751..c5a0ba150dc 100644 --- a/system/jlib/jlog.ipp +++ b/system/jlib/jlog.ipp @@ -863,7 +863,7 @@ class CLogAccessFilter : public CInterfaceOf {}; class FieldLogAccessFilter : public CLogAccessFilter { public: - FieldLogAccessFilter(const char * _value, LogAccessFilterType _filterType) : value(_value), type(_filterType) {} + FieldLogAccessFilter(const char * _value, LogAccessFilterType _filterType) : value(_value), type(_filterType) {fprintf(stderr, "-----Creating FieldLogAccessFilter '%s'", value.str());} FieldLogAccessFilter(IPropertyTree * tree, LogAccessFilterType _filterType) { type = _filterType; @@ -894,11 +894,54 @@ protected: LogAccessFilterType type; }; +class ColumnLogAccessFilter : public CLogAccessFilter +{ +public: + ColumnLogAccessFilter(const char * _column, const char * _value, LogAccessFilterType _filterType) : value(_value), fieldName(_column), type(_filterType) {fprintf(stderr, "-----CREATING ColumnLogAccessFilter");} + ColumnLogAccessFilter(IPropertyTree * tree, LogAccessFilterType _filterType) + { + type = _filterType; + VStringBuffer xpath("@%s", logAccessFilterTypeToString(type)); + value.set(tree->queryProp(xpath.str())); + } + + void addToPTree(IPropertyTree * tree) const + { + IPropertyTree * filterTree = createPTree(ipt_caseInsensitive); + filterTree->setProp("@type", logAccessFilterTypeToString(type)); + filterTree->setProp("@value", value); + tree->addPropTree("filter", filterTree); + } + + void toString(StringBuffer & out) const + { + out.set(value); + } + + LogAccessFilterType filterType() const + { + return type; + } + + const char * getFieldName() const + { + return fieldName.str(); + } + +protected: + StringAttr value; + StringAttr fieldName; + LogAccessFilterType type; +}; + class BinaryLogAccessFilter : public CLogAccessFilter { public: BinaryLogAccessFilter(ILogAccessFilter * _arg1, ILogAccessFilter * _arg2, LogAccessFilterType _type) : arg1(_arg1), arg2(_arg2) { + if (!arg1 || !arg2) + throw makeStringException(-1, "Binary Log Access Filter encountered empty operand"); + setType(_type); } @@ -942,6 +985,16 @@ public: return type; } + ILogAccessFilter * leftFilterClause() const + { + return arg1; + } + + ILogAccessFilter * rightFilterClause() const + { + return arg2; + } + private: Linked arg1; Linked arg2; diff --git a/system/logaccess/ElasticStack/ElasticStackLogAccess.cpp b/system/logaccess/ElasticStack/ElasticStackLogAccess.cpp index 2a365f885aa..affe3590465 100644 --- a/system/logaccess/ElasticStack/ElasticStackLogAccess.cpp +++ b/system/logaccess/ElasticStack/ElasticStackLogAccess.cpp @@ -378,7 +378,7 @@ void esTimestampQueryRangeString(std::string & range, const char * timestampfiel } /* - * Constructs ElasticSearch match clause + * Constructs ElasticSearch term clause * Use for exact term matches such as a price, a product ID, or a username. */ void esTermQueryString(std::string & search, const char *searchval, const char *searchfield) @@ -471,154 +471,182 @@ void ElasticStackLogAccess::esSearchMetaData(std::string & search, const LogAcce search += ", "; } -void ElasticStackLogAccess::populateQueryStringAndQueryIndex(std::string & queryString, std::string & queryIndex, const LogAccessConditions & options) +/* + * Constructs ElasticSearch querystring clause + * Use for exact term matches based on operators such as AND or OR + */ +void ElasticStackLogAccess::populateESQueryQueryString(std::string & queryString, std::string & queryIndex, const ILogAccessFilter * filter) { - try + //https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html + //Returns documents based on a provided query string, using a parser with a strict syntax. + + //This query uses a syntax to parse and split the provided query string based on operators, + //such as AND or NOT. The query then analyzes each split text independently before returning matching documents. + + if (filter == nullptr) + throw makeStringExceptionV(-1, "%s: Null filter detected while creating Elastic Stack query string", COMPONENT_NAME); + + + StringBuffer queryValue; + std::string queryField = m_globalSearchColName.str(); + + filter->toString(queryValue); + switch (filter->filterType()) { - StringBuffer queryValue; - std::string queryField = m_globalSearchColName.str(); - queryIndex = m_globalIndexSearchPattern.str(); + case LOGACCESS_FILTER_jobid: + { + if (m_workunitSearchColName.isEmpty()) + throw makeStringExceptionV(-1, "%s: 'JobID' log entry field not configured", COMPONENT_NAME); - bool fullTextSearch = true; - bool wildCardSearch = false; + queryField = m_workunitSearchColName.str(); - options.queryFilter()->toString(queryValue); - switch (options.queryFilter()->filterType()) - { - case LOGACCESS_FILTER_jobid: + if (!m_workunitIndexSearchPattern.isEmpty()) { - if (!m_workunitSearchColName.isEmpty()) - { - queryField = m_workunitSearchColName.str(); - fullTextSearch = false; //found dedicated components column - } + if (!queryIndex.empty() && queryIndex != m_workunitIndexSearchPattern.str()) + throw makeStringExceptionV(-1, "%s: Multi-index query not supported: '%s' - '%s'", COMPONENT_NAME, queryIndex.c_str(), m_workunitIndexSearchPattern.str()); + queryIndex = m_workunitIndexSearchPattern; + } - if (!m_workunitIndexSearchPattern.isEmpty()) - { - queryIndex = m_workunitIndexSearchPattern.str(); - } + DBGLOG("%s: Searching log entries by jobid: '%s'...", COMPONENT_NAME, queryValue.str() ); + break; + } + case LOGACCESS_FILTER_class: + { + if (m_classSearchColName.isEmpty()) + throw makeStringExceptionV(-1, "%s: 'Class' log entry field not configured", COMPONENT_NAME); - DBGLOG("%s: Searching log entries by jobid: '%s'...", COMPONENT_NAME, queryValue.str() ); - break; - } - case LOGACCESS_FILTER_class: + queryField = m_classSearchColName.str(); + + if (!m_classIndexSearchPattern.isEmpty()) { - if (!m_classSearchColName.isEmpty()) - { - queryField = m_classSearchColName.str(); - fullTextSearch = false; //found dedicated components column - } + if (!queryIndex.empty() && queryIndex != m_classIndexSearchPattern.str()) + throw makeStringExceptionV(-1, "%s: Multi-index query not supported: '%s' - '%s'", COMPONENT_NAME, queryIndex.c_str(), m_classIndexSearchPattern.str()); + queryIndex = m_classIndexSearchPattern.str(); + } - if (!m_classIndexSearchPattern.isEmpty()) - { - queryIndex = m_classIndexSearchPattern.str(); - } + DBGLOG("%s: Searching log entries by class: '%s'...", COMPONENT_NAME, queryValue.str() ); + break; + } + case LOGACCESS_FILTER_audience: + { + if (m_audienceSearchColName.isEmpty()) + throw makeStringExceptionV(-1, "%s: 'Audience' log entry field not configured", COMPONENT_NAME); + + queryField = m_audienceSearchColName.str(); - DBGLOG("%s: Searching log entries by class: '%s'...", COMPONENT_NAME, queryValue.str() ); - break; - } - case LOGACCESS_FILTER_audience: + if (!m_audienceIndexSearchPattern.isEmpty()) { - if (!m_audienceSearchColName.isEmpty()) - { - queryField = m_audienceSearchColName.str(); - fullTextSearch = false; //found dedicated components column - } - - if (!m_audienceIndexSearchPattern.isEmpty()) - { - queryIndex = m_audienceIndexSearchPattern.str(); - } + if (!queryIndex.empty() && queryIndex != m_audienceIndexSearchPattern.str()) + throw makeStringExceptionV(-1, "%s: Multi-index query not supported: '%s' - '%s'", COMPONENT_NAME, queryIndex.c_str(), m_audienceIndexSearchPattern.str()); - DBGLOG("%s: Searching log entries by target audience: '%s'...", COMPONENT_NAME, queryValue.str() ); - break; + queryIndex = m_audienceIndexSearchPattern.str(); } - case LOGACCESS_FILTER_component: - { - if (!m_componentsSearchColName.isEmpty()) - { - queryField = m_componentsSearchColName.str(); - fullTextSearch = false; //found dedicated components column - } - if (!m_componentsIndexSearchPattern.isEmpty()) - { - queryIndex = m_componentsIndexSearchPattern.str(); - } + DBGLOG("%s: Searching log entries by target audience: '%s'...", COMPONENT_NAME, queryValue.str() ); + break; + } + case LOGACCESS_FILTER_component: + { + if (m_componentsSearchColName.isEmpty()) + throw makeStringExceptionV(-1, "%s: 'Host' log entry field not configured", COMPONENT_NAME); - DBGLOG("%s: Searching '%s' component log entries...", COMPONENT_NAME, queryValue.str() ); - break; - } - case LOGACCESS_FILTER_host: - { - if (!m_hostSearchColName.isEmpty()) - { - queryField = m_hostSearchColName.str(); - fullTextSearch = false; //found dedicated components column - } + queryField = m_componentsSearchColName.str(); - if (!m_hostIndexSearchPattern.isEmpty()) - { - queryIndex = m_hostIndexSearchPattern.str(); - } + if (!m_componentsIndexSearchPattern.isEmpty()) + { + if (!queryIndex.empty() && queryIndex != m_componentsIndexSearchPattern.str()) + throw makeStringExceptionV(-1, "%s: Multi-index query not supported: '%s' - '%s'", COMPONENT_NAME, queryIndex.c_str(), m_componentsIndexSearchPattern.str()); - DBGLOG("%s: Searching log entries by host: '%s'", COMPONENT_NAME, queryValue.str() ); - break; + queryIndex = m_componentsIndexSearchPattern.str(); } - case LOGACCESS_FILTER_instance: - { - if (!m_instanceSearchColName.isEmpty()) - { - queryField = m_instanceSearchColName.str(); - fullTextSearch = false; //found dedicated components column - } - if (!m_instanceIndexSearchPattern.isEmpty()) - { - queryIndex = m_instanceIndexSearchPattern.str(); - } + DBGLOG("%s: Searching '%s' component log entries...", COMPONENT_NAME, queryValue.str() ); + break; + } + case LOGACCESS_FILTER_host: + { + if (m_hostSearchColName.isEmpty()) + throw makeStringExceptionV(-1, "%s: 'Host' log entry field not configured", COMPONENT_NAME); - DBGLOG("%s: Searching log entries by HPCC component instance: '%s'", COMPONENT_NAME, queryValue.str() ); - break; - } - case LOGACCESS_FILTER_wildcard: + queryField = m_hostSearchColName.str(); + + if (!m_hostIndexSearchPattern.isEmpty()) { - wildCardSearch = true; - DBGLOG("%s: Performing wildcard log entry search...", COMPONENT_NAME); - break; - } - case LOGACCESS_FILTER_or: - throw makeStringExceptionV(-1, "%s: Compound query criteria not currently supported: '%s'", COMPONENT_NAME, queryValue.str()); - //"query":{"bool":{"must":[{"match":{"kubernetes.container.name.keyword":{"query":"eclwatch","operator":"or"}}},{"match":{"container.image.name.keyword":"hpccsystems\\core"}}]} } - case LOGACCESS_FILTER_and: - throw makeStringExceptionV(-1, "%s: Compound query criteria not currently supported: '%s'", COMPONENT_NAME, queryValue.str()); - //"query":{"bool":{"must":[{"match":{"kubernetes.container.name.keyword":{"query":"eclwatch","operator":"and"}}},{"match":{"created_ts":"2021-08-25T20:23:04.923Z"}}]} } - default: - throw makeStringExceptionV(-1, "%s: Unknown query criteria type encountered: '%s'", COMPONENT_NAME, queryValue.str()); + if (!queryIndex.empty() && queryIndex != m_hostIndexSearchPattern.str()) + throw makeStringExceptionV(-1, "%s: Multi-index query not supported: '%s' - '%s'", COMPONENT_NAME, queryIndex.c_str(), m_hostIndexSearchPattern.str()); + + queryIndex = m_hostIndexSearchPattern.str(); } - queryString = "{"; - esSearchMetaData(queryString, options.getReturnColsMode(), options.getLogFieldNames(), options.getLimit(), options.getStartFrom()); + DBGLOG("%s: Searching log entries by host: '%s'", COMPONENT_NAME, queryValue.str() ); + break; + } + case LOGACCESS_FILTER_instance: + { + if (m_instanceSearchColName.isEmpty()) + throw makeStringExceptionV(-1, "%s: 'Instance' log entry field not configured", COMPONENT_NAME); - queryString += "\"query\": { \"bool\": {"; + queryField = m_instanceSearchColName.str(); - if(!wildCardSearch) + if (!m_instanceIndexSearchPattern.isEmpty()) { - queryString += " \"must\": { "; + if (!queryIndex.empty() && queryIndex != m_instanceIndexSearchPattern.str()) + throw makeStringExceptionV(-1, "%s: Multi-index query not supported: '%s' - '%s'", COMPONENT_NAME, queryIndex.c_str(), m_instanceIndexSearchPattern.str()); - std::string criteria; - if (fullTextSearch) //are we performing a query on a blob, or exact term match? - esMatchQueryString(criteria, queryValue.str(), queryField.c_str()); - else - esTermQueryString(criteria, queryValue.str(), queryField.c_str()); + queryIndex = m_instanceIndexSearchPattern.str(); + } - queryString += criteria; - queryString += "}, "; //end must, expect filter to follow + DBGLOG("%s: Searching log entries by HPCC component instance: '%s'", COMPONENT_NAME, queryValue.str() ); + break; + } + case LOGACCESS_FILTER_wildcard: + throw makeStringExceptionV(-1, "%s: Wild Card filter detected within exact term filter!", COMPONENT_NAME); + case LOGACCESS_FILTER_or: + case LOGACCESS_FILTER_and: + queryString += " ( "; + populateESQueryQueryString(queryString, queryIndex, filter->leftFilterClause()); + queryString.append(" "); + queryString += logAccessFilterTypeToString(filter->filterType()); + queryString.append(" "); + + populateESQueryQueryString(queryString, queryIndex, filter->rightFilterClause()); + queryString += " ) "; + return; // queryString populated, need to break out + case LOGACCESS_FILTER_column: + if (filter->getFieldName() == nullptr) + throw makeStringExceptionV(-1, "%s: empty field name detected in filter by column!", COMPONENT_NAME); + queryField = filter->getFieldName(); + break; + default: + throw makeStringExceptionV(-1, "%s: Unknown query criteria type encountered: '%s'", COMPONENT_NAME, queryValue.str()); + } + + queryString += queryField + ":" + queryValue.str(); + + if (queryIndex.empty()) + queryIndex = m_globalIndexSearchPattern.str(); +} + +void ElasticStackLogAccess::populateQueryStringAndQueryIndex(std::string & queryString, std::string & queryIndex, const LogAccessConditions & options) +{ + try + { + queryString = "{"; + esSearchMetaData(queryString, options.getReturnColsMode(), options.getLogFieldNames(), options.getLimit(), options.getStartFrom()); + + queryString += "\"query\": { \"bool\": { \"filter\": [ "; + if (options.queryFilter()->filterType() == LOGACCESS_FILTER_wildcard) // No filter + { + queryIndex = m_globalIndexSearchPattern.str(); + } + else + { + queryString += "{ \"query_string\": { \"query\": \""; + populateESQueryQueryString(queryString, queryIndex, options.queryFilter()); + queryString += "\" } },"; } - std::string filter = "\"filter\": {"; std::string range; - const LogAccessTimeRange & trange = options.getTimeRange(); //Bail out earlier? if (trange.getStartt().isNull()) @@ -626,25 +654,22 @@ void ElasticStackLogAccess::populateQueryStringAndQueryIndex(std::string & query esTimestampQueryRangeString(range, m_globalIndexTimestampField.str(), trange.getStartt().getSimple(),trange.getEndt().isNull() ? -1 : trange.getEndt().getSimple()); - filter += range; - filter += "}"; //end filter - - queryString += filter; - queryString += "}}}"; //end bool and query + queryString += "{ " + range; + queryString += "}]}}}"; //end range, filter array, bool, query, and request DBGLOG("%s: Search string '%s'", COMPONENT_NAME, queryString.c_str()); } catch (std::runtime_error &e) { const char * wha = e.what(); - throw makeStringExceptionV(-1, "%s: fetchLog: Error searching doc: %s", COMPONENT_NAME, wha); + throw makeStringExceptionV(-1, "%s: Error populating ES search string: %s", COMPONENT_NAME, wha); } catch (IException * e) { StringBuffer mess; e->errorMessage(mess); e->Release(); - throw makeStringExceptionV(-1, "%s: fetchLog: Error searching doc: %s", COMPONENT_NAME, mess.str()); + throw makeStringExceptionV(-1, "%s: Error populating ES search string: %s", COMPONENT_NAME, mess.str()); } } diff --git a/system/logaccess/ElasticStack/ElasticStackLogAccess.hpp b/system/logaccess/ElasticStack/ElasticStackLogAccess.hpp index 0f9f0939a6b..aae5d29ad90 100644 --- a/system/logaccess/ElasticStack/ElasticStackLogAccess.hpp +++ b/system/logaccess/ElasticStack/ElasticStackLogAccess.hpp @@ -14,6 +14,7 @@ #pragma once #include "jlog.hpp" +#include "jlog.ipp" #include "jptree.hpp" #include "jstring.hpp" #include "jfile.ipp" @@ -84,6 +85,7 @@ class ELASTICSTACKLOGACCESS_API ElasticStackLogAccess : public CInterfaceOf &hostUrlList, IPropertyTree & logAccessPluginConfig); virtual ~ElasticStackLogAccess() override = default; From 010164c1bfcd1c05bfa2c407225203835ac903d7 Mon Sep 17 00:00:00 2001 From: Jake Smith Date: Thu, 5 May 2022 12:32:24 +0100 Subject: [PATCH 12/24] HPCC-27608 Fix leaks and poss. crash in fetch if in CQ Signed-off-by: Jake Smith --- thorlcr/activities/fetch/thfetchslave.cpp | 28 ++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/thorlcr/activities/fetch/thfetchslave.cpp b/thorlcr/activities/fetch/thfetchslave.cpp index f4195dc3d5b..8e2b5da33b0 100644 --- a/thorlcr/activities/fetch/thfetchslave.cpp +++ b/thorlcr/activities/fetch/thfetchslave.cpp @@ -307,11 +307,12 @@ class CFetchSlaveBase : public CSlaveActivity, implements IFetchHandler protected: Owned fetchDiskRowIf; - IFetchStream *fetchStream = nullptr; + Owned fetchStream; + CriticalSection fetchStreamCS; IHThorFetchBaseArg *fetchBaseHelper; unsigned files = 0; CPartDescriptorArray parts; - IRowStream *keyIn = nullptr; + Owned keyIn; bool indexRowExtractNeeded = false; mptag_t mptag = TAG_NULL; @@ -328,11 +329,6 @@ class CFetchSlaveBase : public CSlaveActivity, implements IFetchHandler reInit = 0 != (fetchBaseHelper->getFetchFlags() & (FFvarfilename|FFdynamicfilename)); appendOutputLinked(this); } - ~CFetchSlaveBase() - { - ::Release(keyIn); - ::Release(fetchStream); - } virtual void init(MemoryBuffer &data, MemoryBuffer &slaveData) override { @@ -471,12 +467,12 @@ class CFetchSlaveBase : public CSlaveActivity, implements IFetchHandler if (fetchBaseHelper->extractAllJoinFields()) { - keyIn = LINK(inputStream); + keyIn.set(inputStream); keyInMeta.set(input->queryFromActivity()->queryRowMetaData()); } else { - keyIn = new CKeyFieldExtract(this, *inputStream, *fetchBaseHelper); + keyIn.setown(new CKeyFieldExtract(this, *inputStream, *fetchBaseHelper)); keyInMeta.set(QUERYINTERFACE(fetchBaseHelper->queryExtractedSize(), IOutputMetaData)); } keyInIf.setown(createRowInterfaces(keyInMeta)); @@ -509,12 +505,15 @@ class CFetchSlaveBase : public CSlaveActivity, implements IFetchHandler }; Owned fmeta = createFixedSizeMetaData(sizeof(offset_t)); // should be provided by Gavin? keyInIf.setown(createRowInterfaces(fmeta)); - keyIn = new CKeyFPosExtract(keyInIf, this, *inputStream, *fetchBaseHelper); + keyIn.setown(new CKeyFPosExtract(keyInIf, this, *inputStream, *fetchBaseHelper)); } Owned rowIf = createRowInterfaces(queryRowMetaData()); OwnedRoxieString fileName = fetchBaseHelper->getFileName(); - fetchStream = createFetchStream(*this, keyInIf, rowIf, abortSoon, fileName, parts, offsetCount, offsetMapSz, offsetMapBytes.toByteArray(), this, mptag, eexp); + { + CriticalBlock b(fetchStreamCS); + fetchStream.setown(createFetchStream(*this, keyInIf, rowIf, abortSoon, fileName, parts, offsetCount, offsetMapSz, offsetMapBytes.toByteArray(), this, mptag, eexp)); + } fetchStreamOut = fetchStream->queryOutput(); fetchStream->start(keyIn); initializeFileParts(); @@ -569,8 +568,11 @@ class CFetchSlaveBase : public CSlaveActivity, implements IFetchHandler } virtual void serializeStats(MemoryBuffer &mb) override { - if (fetchStream) - fetchStream->getFileStats(stats, fileStats, fileTableStart); + { + CriticalBlock b(fetchStreamCS); + if (fetchStream) + fetchStream->getFileStats(stats, fileStats, fileTableStart); + } PARENT::serializeStats(mb); mb.append((unsigned)fileStats.size()); for (auto &stats: fileStats) From d07da2b9fc3546159e032f73be3e5d1d46547e31 Mon Sep 17 00:00:00 2001 From: Jake Smith Date: Tue, 19 Apr 2022 12:13:49 +0100 Subject: [PATCH 13/24] HPCC-27539 Ensure defaultCopies in containerized mode = 1 Signed-off-by: Jake Smith --- dali/base/dafdesc.cpp | 15 ++++++++++++++- dali/base/dafdesc.hpp | 5 +++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/dali/base/dafdesc.cpp b/dali/base/dafdesc.cpp index 3148f5e92be..d62b01d5f8b 100644 --- a/dali/base/dafdesc.cpp +++ b/dali/base/dafdesc.cpp @@ -101,7 +101,10 @@ void ClusterPartDiskMapSpec::setRoxie (unsigned redundancy, unsigned channelsPer { flags = 0; replicateOffset = _replicateOffset?_replicateOffset:1; - defaultCopies = redundancy+1; +#ifdef _CONTAINERIZED + // NB: in containerized mode the cluster spec. redundancy isn't used. + redundancy = 0; +#endif if ((channelsPerNode>1)&&(redundancy==0)) { flags |= CPDMSF_wrapToNextDrv; flags |= CPDMSF_overloadedConfig; @@ -223,6 +226,11 @@ void ClusterPartDiskMapSpec::fromProp(IPropertyTree *tree) } replicateOffset = getPropDef(tree,"@replicateOffset",1); defaultCopies = getPropDef(tree,"@redundancy",defrep)+1; +#ifdef _CONTAINERIZED + // NB: in containerized mode the cluster spec. redundancy isn't used. + // Force number of copies to 1 here (on deserialization) to avoid code paths that would look at redundant copies. + defaultCopies = 1; +#endif maxDrvs = (byte)getPropDef(tree,"@maxDrvs",2); startDrv = (byte)getPropDef(tree,"@startDrv",defrep?0:getPathDrive(dir.str())); interleave = getPropDef(tree,"@interleave",0); @@ -256,6 +264,11 @@ void ClusterPartDiskMapSpec::deserialize(MemoryBuffer &mb) mb.read(flags); mb.read(replicateOffset); mb.read(defaultCopies); +#ifdef _CONTAINERIZED + // NB: in containerized mode the cluster spec. redundancy isn't used. + // Force number of copies to 1 here (on deserialization) to avoid code paths that would look at redundant copies. + defaultCopies = 1; +#endif mb.read(startDrv); mb.read(maxDrvs); mb.read(interleave); diff --git a/dali/base/dafdesc.hpp b/dali/base/dafdesc.hpp index b9290c7496a..958ba8187fe 100644 --- a/dali/base/dafdesc.hpp +++ b/dali/base/dafdesc.hpp @@ -53,7 +53,12 @@ enum DFD_OS enum DFD_Replicate { DFD_NoCopies = 1, +#ifdef _CONTAINERIZED + // NB: in containerized mode, each plane has only 1 copy. + DFD_DefaultCopies = 1 +#else DFD_DefaultCopies = 2 +#endif }; enum GroupType { grp_thor, grp_thorspares, grp_roxie, grp_hthor, grp_unknown, __grp_size }; From fa481f7b3e0fe36ade2aeaed23747182cdb9ea61 Mon Sep 17 00:00:00 2001 From: Gavin Halliday Date: Thu, 28 Apr 2022 15:25:05 +0100 Subject: [PATCH 14/24] HPCC-27587 Avoid dynamic initialiser for defaultJobInfo Signed-off-by: Gavin Halliday --- system/jlib/jlog.cpp | 2 +- system/jlib/jlog.hpp | 2 +- system/jlib/jthread.cpp | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/system/jlib/jlog.cpp b/system/jlib/jlog.cpp index 73c84d61937..f4f1f6f6360 100644 --- a/system/jlib/jlog.cpp +++ b/system/jlib/jlog.cpp @@ -259,7 +259,7 @@ void LogMsgJobInfo::deserialize(MemoryBuffer & in) } static LogMsgJobInfo globalDefaultJobInfo(UnknownJob, UnknownUser); -static thread_local LogMsgJobInfo defaultJobInfo = globalDefaultJobInfo; +static thread_local LogMsgJobInfo defaultJobInfo; const LogMsgJobInfo unknownJob(UnknownJob, UnknownUser); diff --git a/system/jlib/jlog.hpp b/system/jlib/jlog.hpp index 83cd98cf2ae..4c21b7f0202 100644 --- a/system/jlib/jlog.hpp +++ b/system/jlib/jlog.hpp @@ -557,7 +557,7 @@ class jlib_decl LogMsgSysInfo class jlib_decl LogMsgJobInfo { public: - LogMsgJobInfo(LogMsgJobId _job = UnknownJob, LogMsgUserId _user = UnknownUser) : jobID(_job), userID(_user) {} + constexpr LogMsgJobInfo(LogMsgJobId _job = UnknownJob, LogMsgUserId _user = UnknownUser) : jobID(_job), userID(_user) {} ~LogMsgJobInfo(); LogMsgJobId queryJobID() const; const char * queryJobIDStr() const; diff --git a/system/jlib/jthread.cpp b/system/jlib/jthread.cpp index 1f5f3bc4645..fb9adee3e7a 100644 --- a/system/jlib/jthread.cpp +++ b/system/jlib/jthread.cpp @@ -134,7 +134,7 @@ unsigned WINAPI Thread::_threadmain(LPVOID v) void *Thread::_threadmain(void *v) #endif { - resetThreadLogging(); // Not strictly needed if everything handled via thread_local variable's constructors... + resetThreadLogging(); Thread * t = (Thread *)v; #ifdef _WIN32 if (SEHHandling) @@ -607,6 +607,7 @@ void CThreadedPersistent::threadmain() break; try { + resetThreadLogging(); owner->threadmain(); // Note we do NOT call the thread reset hook here - these threads are expected to be able to preserve state, I think } @@ -957,13 +958,13 @@ class CPooledThreadWrapper: public Thread { do { - resetThreadLogging(); sem.wait(); { CriticalBlock block(parent.crit); // to synchronize if (parent.stopall) break; } + resetThreadLogging(); parent.notifyStarted(this); try { From c1e29c39a11642aaf80bfaacd31e99d2d8718775 Mon Sep 17 00:00:00 2001 From: Gavin Halliday Date: Tue, 19 Apr 2022 10:15:55 +0100 Subject: [PATCH 15/24] HPCC-27176 Fix incorrect warning about udp configuration on startup Signed-off-by: Gavin Halliday --- roxie/ccd/ccdmain.cpp | 3 ++- roxie/udplib/udpsha.cpp | 7 ++++--- roxie/udplib/udpsha.hpp | 2 +- roxie/udplib/udpsim.cpp | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/roxie/ccd/ccdmain.cpp b/roxie/ccd/ccdmain.cpp index fcb0c52d72a..63b2a5c50bf 100644 --- a/roxie/ccd/ccdmain.cpp +++ b/roxie/ccd/ccdmain.cpp @@ -1012,7 +1012,8 @@ int CCD_API roxie_main(int argc, const char *argv[], const char * defaultYaml) unsigned __int64 defaultNetworkSpeed = 10 * U64C(0x40000000); // 10Gb/s unsigned __int64 networkSpeed = topology->getPropInt64("@udpNetworkSpeed", defaultNetworkSpeed); // only used to sanity check the different udp options unsigned udpQueueSize = topology->getPropInt("@udpQueueSize", UDP_QUEUE_SIZE); - sanityCheckUdpSettings(udpQueueSize, numChannels, networkSpeed); + unsigned udpSendQueueSize = topology->getPropInt("@udpSendQueueSize", UDP_SEND_QUEUE_SIZE); + sanityCheckUdpSettings(udpQueueSize, udpSendQueueSize, numChannels, networkSpeed); int ttlTmp = topology->getPropInt("@multicastTTL", 1); if (ttlTmp < 0) diff --git a/roxie/udplib/udpsha.cpp b/roxie/udplib/udpsha.cpp index a27996d2d30..3170eb4c685 100644 --- a/roxie/udplib/udpsha.cpp +++ b/roxie/udplib/udpsha.cpp @@ -483,7 +483,7 @@ void PacketTracker::dump() const //--------------------------------------------------------------------------------------------------------------------- -void sanityCheckUdpSettings(unsigned receiveQueueSize, unsigned numSenders, __uint64 networkSpeedBitsPerSecond) +void sanityCheckUdpSettings(unsigned receiveQueueSize, unsigned sendQueueSize, unsigned numSenders, __uint64 networkSpeedBitsPerSecond) { unsigned maxDataPacketSize = 0x2000; // assume jumbo frames roxiemem::DATA_ALIGNMENT_SIZE; __uint64 bytesPerSecond = networkSpeedBitsPerSecond / 10; @@ -516,10 +516,11 @@ void sanityCheckUdpSettings(unsigned receiveQueueSize, unsigned numSenders, __ui trace("udpRequestTimeout", udpRequestTimeout, (2 * minLatencyNs + minTimeForPermitPackets) * 2 / 5, 10); trace("udpResendDelay", udpResendDelay, minTimeForAllPackets, 10); DBGLOG("udpMaxPendingPermits: %u [%u..%u]", udpMaxPendingPermits, udpMaxPendingPermits, udpMaxPendingPermits); - DBGLOG("udpMaxClientPercent: %u [%u..%u]", udpMaxClientPercent, 100, 500); + DBGLOG("udpMaxClientPercent: %u [%u..%u]", udpMaxClientPercent, 100, udpMaxPendingPermits * 100); DBGLOG("udpMaxPermitDeadTimeouts: %u [%u..%u]", udpMaxPermitDeadTimeouts, 2, 10); DBGLOG("udpRequestDeadTimeout: %u [%u..%u]", udpRequestDeadTimeout, 10000, 120000); DBGLOG("udpMinSlotsPerSender: %u [%u..%u]", udpMinSlotsPerSender, 1, 5); + DBGLOG("Queue sizes: send(%u) receive(%u)", sendQueueSize, receiveQueueSize); } // Some sanity checks @@ -558,7 +559,7 @@ void sanityCheckUdpSettings(unsigned receiveQueueSize, unsigned numSenders, __ui WARNLOG("udpMaxPendingPermits=1: only one sender can send at a time"); if (udpMaxClientPercent < 100) ERRLOG("udpMaxClientPercent should be >= 100"); - else if (maxSlotsPerClient * udpMaxClientPercent / 100 > receiveQueueSize) + else if (maxSlotsPerClient > receiveQueueSize) ERRLOG("maxSlotsPerClient * udpMaxClientPercent exceeds the queue size => all slots will be initially allocated to the first sender"); if (udpMinSlotsPerSender > 10) ERRLOG("udpMinSlotsPerSender of %u is higher than recommended", udpMinSlotsPerSender); diff --git a/roxie/udplib/udpsha.hpp b/roxie/udplib/udpsha.hpp index d837ff5bebb..cec36a36031 100644 --- a/roxie/udplib/udpsha.hpp +++ b/roxie/udplib/udpsha.hpp @@ -278,7 +278,7 @@ inline bool checkTraceLevel(unsigned category, unsigned level) return (udpTraceLevel >= level); } -extern UDPLIB_API void sanityCheckUdpSettings(unsigned receiveQueueSize, unsigned numSenders, __uint64 networkSpeedBitsPerSecond); +extern UDPLIB_API void sanityCheckUdpSettings(unsigned receiveQueueSize, unsigned sendQueueSize, unsigned numSenders, __uint64 networkSpeedBitsPerSecond); #define SOCKET_SIMULATION diff --git a/roxie/udplib/udpsim.cpp b/roxie/udplib/udpsim.cpp index 78df327cd81..e152ff78606 100644 --- a/roxie/udplib/udpsim.cpp +++ b/roxie/udplib/udpsim.cpp @@ -226,7 +226,7 @@ void initOptions(int argc, const char **argv) if (options->getPropBool("sanityCheckUdpSettings", true)) { unsigned __int64 networkSpeed = options->getPropInt64("@udpNetworkSpeed", 10 * U64C(0x40000000)); - sanityCheckUdpSettings(numReceiveSlots, numThreads, networkSpeed); + sanityCheckUdpSettings(numReceiveSlots, 100, numThreads, networkSpeed); } } From bad552d8ff7c57e40b91ac17c1e0a4bd6748580a Mon Sep 17 00:00:00 2001 From: Richard Chapman Date: Fri, 6 May 2022 10:46:45 +0100 Subject: [PATCH 16/24] HPCC-27580 Revisit the ThreadList Signed-off-by: Richard Chapman --- dali/base/dadiags.cpp | 5 +--- system/jlib/jdebug.cpp | 6 ++--- system/jlib/jexcept.cpp | 2 -- system/jlib/jthread.cpp | 54 +++-------------------------------------- system/jlib/jthread.hpp | 2 -- 5 files changed, 7 insertions(+), 62 deletions(-) diff --git a/dali/base/dadiags.cpp b/dali/base/dadiags.cpp index bbc5acd5340..9f8edb6275b 100644 --- a/dali/base/dadiags.cpp +++ b/dali/base/dadiags.cpp @@ -115,10 +115,7 @@ class CDaliDiagnosticsServer: public IDaliServer, public Thread StringAttr id; StringBuffer buf; params.read(id); - if (0 == stricmp(id,"threads")) { - mb.append(getThreadList(buf).str()); - } - else if (0 == stricmp(id, "mpqueue")) { + if (0 == stricmp(id, "mpqueue")) { mb.append(getReceiveQueueDetails(buf).str()); } else if (0 == stricmp(id, "locks")) { // Legacy - newer diag clients should use querySDS().getLocks() directly diff --git a/system/jlib/jdebug.cpp b/system/jlib/jdebug.cpp index 61577391b0f..11d0eee0f4e 100644 --- a/system/jlib/jdebug.cpp +++ b/system/jlib/jdebug.cpp @@ -1753,14 +1753,12 @@ class CProcessMonitor return; assertex(n); processes.sort(compare); - StringBuffer name; ForEachItemIn(i1,processes) { CProcInfo &pi = processes.item(i1); if ((pi.delta.system==0)&&(pi.delta.user==0)) break; - getThreadName(pi.pid(),0,name.clear()); - str.appendf("\n TT: PI=%d PN=%s PC=%d ST=%d UT=%d%s%s", - pi.pid(),pi.info.cmd,(pi.delta.system+pi.delta.user)*100/tot_time,pi.delta.system,pi.delta.user,name.length()?" TN=":"",name.str()); + str.appendf("\n TT: PI=%d PN=%s PC=%d ST=%d UT=%d", + pi.pid(),pi.info.cmd,(pi.delta.system+pi.delta.user)*100/tot_time,pi.delta.system,pi.delta.user); if (--n==0) break; } diff --git a/system/jlib/jexcept.cpp b/system/jlib/jexcept.cpp index b51e00ce6c7..a033b7f3593 100644 --- a/system/jlib/jexcept.cpp +++ b/system/jlib/jexcept.cpp @@ -1349,8 +1349,6 @@ NO_SANITIZE("alignment") void excsighandler(int signum, siginfo_t *info, void *e #endif - StringBuffer threadlist; - PROGLOG( "ThreadList:\n%s",getThreadList(threadlist).str()); queryLogMsgManager()->flushQueue(10*1000); // MCK - really should not return after recv'ing any of these signals diff --git a/system/jlib/jthread.cpp b/system/jlib/jthread.cpp index d1ee29f2dd4..bf85dc63e3f 100644 --- a/system/jlib/jthread.cpp +++ b/system/jlib/jthread.cpp @@ -117,8 +117,7 @@ void enableThreadSEH() { SEHHandling=true; } void disableThreadSEH() { SEHHandling=false; } // only prevents new threads from having SEH handler, no mech. for turning off existing threads SEH handling. -static ICopyArrayOf ThreadList; -static CriticalSection ThreadListSem; +static std::atomic threadCount; static size32_t defaultThreadStackSize=0; static ICopyArrayOf ThreadDestroyList; static CriticalSection ThreadDestroyListLock; @@ -436,9 +435,6 @@ void Thread::startRelease() IERRLOG("pthread_create returns %d",status); PrintStackReport(); PrintMemoryReport(); - StringBuffer s; - getThreadList(s); - IERRLOG("Running threads:\n %s",s.str()); throw makeOsException(status); } unsigned retryCount = 10; @@ -454,12 +450,7 @@ void Thread::startRelease() alive = true; if (prioritydelta) adjustPriority(prioritydelta); - - { - CriticalBlock block(ThreadListSem); - ThreadList.zap(*this); // just in case restarting - ThreadList.append(*this); - } + threadCount++; #ifdef _WIN32 DWORD count = ResumeThread(hThread); assertex(count == 1); @@ -531,54 +522,17 @@ Thread::~Thread() } #endif Link(); - + threadCount--; // DBGLOG("Thread %x (%s) destroyed\n", threadid, threadname); - { - CriticalBlock block(ThreadListSem); - ThreadList.zap(*this); - } free(cthreadname.threadname); cthreadname.threadname = NULL; } unsigned getThreadCount() { - CriticalBlock block(ThreadListSem); - return ThreadList.ordinality(); -} - -StringBuffer & getThreadList(StringBuffer &str) -{ - CriticalBlock block(ThreadListSem); - ForEachItemIn(i,ThreadList) { - Thread &item=ThreadList.item(i); - item.getInfo(str).append("\n"); - } - return str; + return threadCount; } -StringBuffer &getThreadName(int thandle,unsigned tid,StringBuffer &name) -{ - CriticalBlock block(ThreadListSem); - bool found=false; - ForEachItemIn(i,ThreadList) { - Thread &item=ThreadList.item(i); - int h; - unsigned t; - const char *s = item.getLogInfo(h,t); - if (s&&*s&&((thandle==0)||(h==thandle))&&((tid==0)||(t==tid))) { - if (found) { - name.clear(); - break; // only return if unambiguous - } - name.append(s); - found = true; - } - } - return name; -} - - // CThreadedPersistent CThreadedPersistent::CThreadedPersistent(const char *name, IThreaded *_owner) : athread(*this, name), owner(_owner), state(s_ready) diff --git a/system/jlib/jthread.hpp b/system/jlib/jthread.hpp index 85e4c73218e..74d04fa48c2 100644 --- a/system/jlib/jthread.hpp +++ b/system/jlib/jthread.hpp @@ -291,9 +291,7 @@ extern jlib_decl IThreadPool *createThreadPool( unsigned targetpoolsize=0 // target maximum size of pool (default same as defaultmax) ); -extern jlib_decl StringBuffer &getThreadList(StringBuffer &str); extern jlib_decl unsigned getThreadCount(); -extern jlib_decl StringBuffer &getThreadName(int thandle,unsigned logtid,StringBuffer &name); // either thandle or tid should be 0 // Simple pipe process support interface ISimpleReadStream; From 9bec5f9248149dd54bba975425ef0e84193c73bb Mon Sep 17 00:00:00 2001 From: Richard Chapman Date: Fri, 6 May 2022 10:59:51 +0100 Subject: [PATCH 17/24] HPCC-27580 Clean up thread name processing Signed-off-by: Richard Chapman --- system/jlib/jstring.cpp | 7 +++++++ system/jlib/jstring.hpp | 2 +- system/jlib/jthread.cpp | 39 +++++++++------------------------------ system/jlib/jthread.hpp | 14 ++------------ 4 files changed, 19 insertions(+), 43 deletions(-) diff --git a/system/jlib/jstring.cpp b/system/jlib/jstring.cpp index a4cee48257f..7225a90fd6d 100644 --- a/system/jlib/jstring.cpp +++ b/system/jlib/jstring.cpp @@ -1436,6 +1436,13 @@ void StringAttr::set(const char * _text) free(oldtext); } +void StringAttr::swapWith(StringAttr & other) +{ + char * temp = text; + text = other.text; + other.text = temp; +} + void StringAttr::set(const char * _text, size_t _len) { char * oldtext = text; diff --git a/system/jlib/jstring.hpp b/system/jlib/jstring.hpp index 8191f0c85d4..2602fe15595 100644 --- a/system/jlib/jstring.hpp +++ b/system/jlib/jstring.hpp @@ -276,7 +276,7 @@ class jlib_decl StringAttr void setown(StringBuffer & source); void toLowerCase(); void toUpperCase(); - + void swapWith(StringAttr & other); private: char * text; }; diff --git a/system/jlib/jthread.cpp b/system/jlib/jthread.cpp index bf85dc63e3f..9398e2bbef7 100644 --- a/system/jlib/jthread.cpp +++ b/system/jlib/jthread.cpp @@ -346,8 +346,7 @@ void Thread::init(const char *_name) threadid = 0; tidlog = 0; alive = false; - cthreadname.threadname = (NULL == _name) ? NULL : strdup(_name); - ithreadname = &cthreadname; + cthreadname.set(_name); prioritydelta = 0; nicelevel = 0; stacksize = 0; // default is EXE default stack size (set by /STACK) @@ -511,7 +510,6 @@ bool Thread::join(unsigned timeout) Thread::~Thread() { - ithreadname = &cthreadname; // safer (as derived classes destroyed) #ifdef _DEBUG if (alive) { if (!stopped.wait(0)) { // see if fell out of threadmain and signal stopped @@ -523,9 +521,7 @@ Thread::~Thread() #endif Link(); threadCount--; -// DBGLOG("Thread %x (%s) destroyed\n", threadid, threadname); - free(cthreadname.threadname); - cthreadname.threadname = NULL; +// DBGLOG("Thread %x (%s) destroyed\n", threadid, cthreadname.str()); } unsigned getThreadCount() @@ -868,7 +864,7 @@ class CPooledThreadWrapper: public Thread IPooledThread *thread; Semaphore sem; CThreadPoolBase &parent; - char *runningname; + StringAttr runningName; public: CPooledThreadWrapper(CThreadPoolBase &_parent, PooledThreadHandle _handle, @@ -877,16 +873,15 @@ class CPooledThreadWrapper: public Thread { thread = _thread; handle = _handle; - runningname = strdup(_parent.poolname); + runningName.set(_parent.poolname); } ~CPooledThreadWrapper() { thread->Release(); - free(runningname); } - void setName(const char *name) { free(runningname); runningname=strdup(name); } + void setName(const char *name) { runningName.set(name); } void setHandle(PooledThreadHandle _handle) { handle = _handle; } PooledThreadHandle queryHandle() { return handle; } IPooledThread &queryThread() { return *thread; } @@ -919,30 +914,19 @@ class CPooledThreadWrapper: public Thread parent.notifyStarted(this); try { - char *&threadname = cthreadname.threadname; - char *temp = threadname; // swap running name and threadname - threadname = runningname; - runningname = temp; + cthreadname.swapWith(runningName); // swap running name and threadname thread->threadmain(); - temp = threadname; // and back - threadname = runningname; - runningname = temp; + cthreadname.swapWith(runningName); // swap back } catch (IException *e) { - char *&threadname = cthreadname.threadname; - char *temp = threadname; // swap back - threadname = runningname; - runningname = temp; + cthreadname.swapWith(runningName); // swap back handleException(e); } #ifndef NO_CATCHALL catch (...) { - char *&threadname = cthreadname.threadname; - char *temp = threadname; // swap back - threadname = runningname; - runningname = temp; + cthreadname.swapWith(runningName); // swap back handleException(MakeStringException(0, "Unknown exception in Thread from pool %s", parent.poolname.get())); } #endif @@ -1160,11 +1144,6 @@ class CThreadPool: public CThreadPoolBase, implements IThreadPool, public CInter return _start(param, NULL, true); } - PooledThreadHandle startNoBlock(void *param,const char *name) - { - return _start(param, name, true); - } - PooledThreadHandle start(void *param) { return _start(param, NULL, false); diff --git a/system/jlib/jthread.hpp b/system/jlib/jthread.hpp index 74d04fa48c2..b9a8934bb72 100644 --- a/system/jlib/jthread.hpp +++ b/system/jlib/jthread.hpp @@ -95,12 +95,7 @@ class jlib_decl Thread : public CInterface, public IThread void adjustNiceLevel(); protected: - struct cThreadName: implements IThreadName - { - char *threadname; - const char *get() { return threadname; } - } cthreadname; - IThreadName *ithreadname; + StringAttr cthreadname; public: #ifndef _WIN32 Semaphore suspend; @@ -118,7 +113,7 @@ class jlib_decl Thread : public CInterface, public IThread bool isCurrentThread() const; void setNice(int nicelevel); void setStackSize(size32_t size); // required stack size in bytes - called before start() (obviously) - const char *getName() { const char *ret = ithreadname?ithreadname->get():NULL; return ret?ret:"unknown"; } + const char *getName() { return cthreadname.isEmpty() ? "unknown" : cthreadname.str(); } bool isAlive() { return alive; } bool join(unsigned timeout=INFINITE); @@ -140,10 +135,6 @@ class jlib_decl Thread : public CInterface, public IThread // run method not implemented - concrete derived classes must do so static void setDefaultStackSize(size32_t size); // NB under windows requires linker setting (/stack:) - - IThreadName *queryThreadName() { return ithreadname; } - void setThreadName(IThreadName *name) { ithreadname = name; } - }; interface IThreaded @@ -275,7 +266,6 @@ interface IThreadPool : extends IInterface virtual IPooledThreadIterator *running()=0; // return an iterator for all currently running threads virtual unsigned runningCount()=0; // number of currently running threads virtual PooledThreadHandle startNoBlock(void *param)=0; // starts a new thread if it can do so without blocking, else throws exception - virtual PooledThreadHandle startNoBlock(void *param,const char *name)=0; // starts a new thread if it can do so without blocking, else throws exception virtual void setStartDelayTracing(unsigned secs) = 0; // set start delay tracing period virtual bool waitAvailable(unsigned timeout) = 0; // wait until a pool member is available }; From d9d1f8ecbc875cb61bcca68be77a7ec54a4ea7c2 Mon Sep 17 00:00:00 2001 From: Richard Chapman Date: Fri, 6 May 2022 12:32:31 +0100 Subject: [PATCH 18/24] HPCC-27580 Revisit the ThreadList Fix windows build break Signed-off-by: Richard Chapman --- system/jlib/jexcept.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/system/jlib/jexcept.cpp b/system/jlib/jexcept.cpp index a033b7f3593..f86bab3c615 100644 --- a/system/jlib/jexcept.cpp +++ b/system/jlib/jexcept.cpp @@ -915,10 +915,7 @@ static void doPrintStackReport( size_t ip, size_t _bp, size_t sp ) StackWalk( ip , _bp); - ModuleWalk(); - StringBuffer threadlist; - IERRLOG( "ThreadList:\n%s",getThreadList(threadlist).str()); - + ModuleWalk(); } From f83023836721c608c25efda04b4c33386be058f8 Mon Sep 17 00:00:00 2001 From: Richard Chapman Date: Thu, 5 May 2022 14:55:50 +0100 Subject: [PATCH 19/24] HPCC-27599 Eclccserver to fails to start with server-side bundle installed via Helm chart Needs git to be installed (Debug images were ok) Signed-off-by: Richard Chapman --- dockerfiles/platform-core/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dockerfiles/platform-core/Dockerfile b/dockerfiles/platform-core/Dockerfile index 4a4808875ec..8f1ed498610 100644 --- a/dockerfiles/platform-core/Dockerfile +++ b/dockerfiles/platform-core/Dockerfile @@ -31,6 +31,8 @@ RUN apt-get clean -y && \ RUN apt-get install -y \ default-jdk \ g++ \ + git \ + git-lfs \ openssh-client \ libapr1 \ libnuma1 \ From 86afe21fc6fb887f0c9af45267fab81d59a48c84 Mon Sep 17 00:00:00 2001 From: Tim Klemm Date: Tue, 19 Apr 2022 07:26:21 -0400 Subject: [PATCH 20/24] HPCC-27532 Send HTTP response on all exceptions Signed-off-by: Tim Klemm --- esp/bindings/http/platform/httpservice.cpp | 39 ++++++++++++++++++++++ esp/bindings/http/platform/httpservice.hpp | 1 + 2 files changed, 40 insertions(+) diff --git a/esp/bindings/http/platform/httpservice.cpp b/esp/bindings/http/platform/httpservice.cpp index fa94bdef383..7f67fa9b1ea 100644 --- a/esp/bindings/http/platform/httpservice.cpp +++ b/esp/bindings/http/platform/httpservice.cpp @@ -204,6 +204,7 @@ int CEspHttpServer::processRequest() catch (IException *e) { DBGLOG(e); + sendInternalError(true); ctx->addTraceSummaryValue(LogMin, "msg", e->errorMessage(errMessage).str(), TXSUMMARY_GRP_ENTERPRISE); e->Release(); return 0; @@ -211,6 +212,7 @@ int CEspHttpServer::processRequest() catch (...) { IERRLOG("Unknown Exception - reading request [CEspHttpServer::processRequest()]"); + sendInternalError(false); ctx->addTraceSummaryValue(LogMin, "msg", "Unknown Exception - reading request [CEspHttpServer::processRequest()]", TXSUMMARY_GRP_ENTERPRISE); return 0; } @@ -428,6 +430,7 @@ int CEspHttpServer::processRequest() catch (IException *e) { DBGLOG(e); + sendInternalError(true); ctx->addTraceSummaryValue(LogMin, "msg", e->errorMessage(errMessage).str(), TXSUMMARY_GRP_ENTERPRISE); VStringBuffer fault("F%d", e->errorCode()); ctx->addTraceSummaryValue(LogMin, "custom_fields.soapFaultCode", fault.str(), TXSUMMARY_GRP_ENTERPRISE); @@ -442,6 +445,7 @@ int CEspHttpServer::processRequest() UWARNLOG("METHOD: %s, PATH: %s, TYPE: %s, CONTENT-LENGTH: %" I64F "d", m_request->queryMethod(), m_request->queryPath(), m_request->getContentType(content_type).str(), len); if (len > 0) m_request->logMessage(LOGCONTENT, "HTTP request content received:\n"); + sendInternalError(false); ctx->addTraceSummaryValue(LogMin, "msg", "Unknown exception caught in CEspHttpServer::processRequest", TXSUMMARY_GRP_ENTERPRISE); return 0; } @@ -1624,6 +1628,41 @@ void CEspHttpServer::sendException(EspAuthRequest& authReq, unsigned code, const sendMessage(resp.str(), (format == ESPSerializationJSON) ? "application/json" : "text/xml"); } +/** + * @brief Return a generic internal server error to the client. + * + * Exceptions caught by processRequest fall into three categories. + * + * 1. HTTP exceptions. These are assumed to contain messages suitable for client consumption and + * are returned to the caller. + * 2. Other known exceptions, derived from IException. These are assumed to contain messages that + * include implementation details not appropriate for client consumption. + * 3. Unexpected exceptions that cannot be described. + * + * This method handles categories 2 and 3. A generic message indicating that an exception occurred + * is returned to the client with status code 500. The parameter indicates whether trace output is + * likely to include exception details, and the returned message will refer the client to examine + * the logs for more information when appropriate. + * + * @param loggedDetails + */ +void CEspHttpServer::sendInternalError(bool loggedDetails) +{ + IEspContext* ctx = m_request->queryContext(); + const char* globalId = (ctx ? ctx->getGlobalId() : nullptr); + StringBuffer content; + if (!isEmptyString(globalId)) + content.appendf("Request failed for global transaction '%s'.", globalId); + else + content.append("Request failed."); + if (loggedDetails) + content.append(" Check log files for more information."); + m_response->setStatus(HTTP_STATUS_INTERNAL_SERVER_ERROR); + m_response->setContentType(HTTP_TYPE_TEXT_PLAIN); + m_response->setContent(content); + m_response->send(); +} + void CEspHttpServer::sendAuthorizationMsg(EspAuthRequest& authReq) { StringBuffer resp; diff --git a/esp/bindings/http/platform/httpservice.hpp b/esp/bindings/http/platform/httpservice.hpp index e2985f81bfd..6eb957bd3b4 100644 --- a/esp/bindings/http/platform/httpservice.hpp +++ b/esp/bindings/http/platform/httpservice.hpp @@ -119,6 +119,7 @@ class CEspHttpServer : implements IHttpServerService, public CInterface void createGetSessionTimeoutResponse(StringBuffer& resp, ESPSerializationFormat format, IPropertyTree* sessionTree); void resetSessionTimeout(EspAuthRequest& authReq, unsigned sessionID, StringBuffer& resp, ESPSerializationFormat format, IPropertyTree* sessionTree); void sendException(EspAuthRequest& authReq, unsigned code, const char* msg); + void sendInternalError(bool loggedDetails); void sendMessage(const char* msg, const char* msgType); void sendSessionReloadHTMLPage(IEspContext* ctx, EspAuthRequest& authReq, const char* msg); bool isServiceMethodReq(EspAuthRequest& authReq, const char* serviceName, const char* methodName); From 1cc2b510e58a7ba6b8d21e85d7a901ca36c54283 Mon Sep 17 00:00:00 2001 From: g-pan Date: Mon, 28 Mar 2022 14:16:56 -0400 Subject: [PATCH 21/24] HPCC-25984 Document Deploying to Azure Signed-off-by: g-pan --- .../ContainerizedMods/AzureDeployChapter.xml | 705 ++++++++++++++++++ 1 file changed, 705 insertions(+) create mode 100644 docs/EN_US/ContainerizedHPCC/ContainerizedMods/AzureDeployChapter.xml diff --git a/docs/EN_US/ContainerizedHPCC/ContainerizedMods/AzureDeployChapter.xml b/docs/EN_US/ContainerizedHPCC/ContainerizedMods/AzureDeployChapter.xml new file mode 100644 index 00000000000..25cb4b9eb31 --- /dev/null +++ b/docs/EN_US/ContainerizedHPCC/ContainerizedMods/AzureDeployChapter.xml @@ -0,0 +1,705 @@ + + + + Azure Deployment (Development, Testing, and Production) + + This section should apply for most Azure subscriptions. You may need + to adjust some commands or instructions according to your subscription's + requirements. + + + Using Azure + + Though there are many ways to interact with Azure, this section will + use the Azure cloud shell command line interface. + + The major advantage to using the cloud shell is that it will also + have the other prerequisites installed for you. + + + Azure Prerequisites + + To deploy an HPCC Systems containerized platform instance to + Azure, you should have: + + + + A working computer that supports Linux, MacOS, or Windows + OS. + + + + A web browser, such as Chrome or Firefox. + + + + An Azure account with sufficient permissions, rights, and + credentials. To obtain this, please go to www.azure.com or talk to + your manager if you believe that your employer might have a + corporate account. + + + + A text editor. You can use one of the editors available in the + Azure cloud shell (code, vi, or nano) or any other text editor of + your preference. + + + + At minimum using the 64-bit Helm 3.5 or higher - even if using + the Azure cloud shell. + + + + Assuming you have an Azure account with adequate credits, you can + make use of Azure's browser-based shell, known as the Azure cloud shell, + to deploy and manage your resources. The Azure cloud shell comes with + pre-installed tools, such as Helm, Kubectl, Python, Terraform, + etc. + + https://portal.azure.com/ + + If this is your first time accessing the cloud shell, Azure will + likely notify you about the need for storage in order to save your + virtual machine settings and files. + + + + Click through the prompts to create your account + storage. + + + + You should now be presented with an Azure cloud shell which is + ready to use. You can now proceed to the next section. + + + Third Party Tools + + Should you decide not to use the Azure cloud shell, you will + need to install and configure the Azure CLI on your host machine in + order to deploy and manage Azure resources. In addition, you will also + need to install Helm and Kubectl to manage your Kubernetes packages + and clusters respectively. + + + + Azure Client Interface (CLI) + + + + Kubectl + + + + Helm 3.5 or greater + + All third-party tools listed above should use the + 64-bit architecture. + + The documentation and instructions for how to install and set up + the third party tools are available from the respective vendors on + their websites. + + + + + Azure Resource Group + + An Azure resource group is similar to a folder where a group of + related resources are stored. Generally, you should only use one + resource group per deployment. For instance, deploying two Kubernetes + clusters in one resource group can cause confusion and difficulties to + manage. Unless you or someone in your organization has already created a + resource group and specified to work in that pre-existing resource + group, you will need to create one. + + To create a new resource group, you must choose a name and an + Azure location. Additionally, you may choose to use tags for ease of + management of your resource groups. Some of the details around this may + be subject to you or your organization's subscriptions, quotas, + restrictions or policies. Please ensure that you have a properly + configured Azure subscription with a sufficient access level and credits + for a successful deployment. + + Run the following command to create a new resource group called + rg-hpcc in Azure location eastus: + + az group create --name rg-hpcc --location eastus + + The following message indicates that the resource group has been + successfully created. + + { + "id": "/subscriptions/<my_subscription_id>/resourceGroups/rsg-hpcc", + "location": "eastus", + "managedBy": null, + "name": "rg-hpcc", + "properties": { + "provisioningState": "Succeeded" + }, + + "tags": null, + "type": "Microsoft.Resources/resourceGroups" + } + + Please note that the list of regions available to you might vary + based on your company's policies and/or location. + + + Azure Kubernetes Service Cluster + + Next we will create an Azure Kubernetes Service (AKS) cluster. + AKS stands for Azure Kubernetes Service. It is a service provided by + Azure that offers serverless Kubernetes, which promotes rapid + delivery, scaling, etc. + + You can choose any name for your Kubernetes cluster, we will use + aks-hpcc. To create a Kubernetes cluster, run the following + command: + + az aks create --resource-group rg-hpcc --name aks-hpcc --location <location> + + + + NOTE + + + There are some optional parameters including --node-vm-size and --node-count. Node size refers to the + specs of your VM of choice while node count refers to the number + of VMs you wish to use. In Azure the names VM and node are used + interchangeably. For more on node sizes, please visit https://docs.microsoft.com/en-us/azure/virtual-machines/sizes + + + + + This step can take a few minutes. The time it takes for Azure to + create and provision the requested resources can vary. While you wait, + for your deployment to complete, you can view the progress in the + Azure portal. To view the progress, open another browser tab + to: + + https://portal.azure.com/#blade/HubsExtension/BrowseAll + + + + Azure Node Pools + + The Azure Kubernetes Service (AKS) automatically creates one + node pool. It is a system node pool, by default. There are two node + pool types: system node pools and user + node pools. The system node pool is reserved for core + Kubernetes services and workloads, such as kubelets, kube-proxies, + etc. A user node pool should be used to host your application services + and workloads. Additional node pools can be added after the deployment + of the AKS cluster. + + To follow the recommendations for reserving the system node pool + only for the core AKS services and workloads. You will need to use a + node taint on the newly created system node pool. Since you can't add + taints to any pre-existing node pool, swap the default system node + pool for the newly created one. + + In order to do this, enter the following command (all on one + line, if possible, and remove the connectors "\" as they are only + included here for the code to fit on a single page): + + az aks nodepool add \ +--name sysnodepool \ +--cluster-name aks-hpcc \ +--resource-group rg-hpcc \ +--mode System \ +--enable-cluster-autoscaler \ +--node-count=2 \ +--min-count=1 \ +--max-count=2 \ +--node-vm-size \ +--node-taints CriticalAddonsOnly=true:NoSchedule + + + Delete the automatically created default pool, which we called + "nodepool1" as an example, the actual name may vary. + + Once again enter the following command on one line, (without + connectors "\" if possible). + + az aks nodepool delete \ +--name nodepool1 \ +--cluster-name aks-hpcc \ +--resource-group rg-hpcc + + + Having at least one user node pool is recommended. + + Next add a user node pool which will + schedule the HPCC Systems pods. Also remember to do so on a single + line without the connectors, if possible: + + az aks nodepool add \ +--name usrnodepool1 \ +--cluster-name aks-hpcc \ +--resource-group rg-hpcc \ +--enable-cluster-autoscaler \ +--node-count=2 \ +--min-count=1 \ +--max-count=2 \ +--mode User + + + For more information about Azure virtual machine pricing and + types, please visit https://azure.microsoft.com/en-us/pricing/details/virtual-machines/linux/ + + + + Configure Credentials + + To manage your AKS cluster from your host machine and use + kubectl, you need to authenticate against the + cluster. In addition, this will also allow you to deploy your HPCC + Systems instance using Helm. To configure the Kubernetes client + credentials enter the following command: + + az aks get-credentials --resource-group rg-hpcc --name aks-hpcc --admin + + + + + Installing the Helm charts + + This section will demonstrate how to fetch, modify, and deploy the + HPCC Systems charts. First we will need to access the HPCC Systems + repository. + + Add, or update if already installed, the HPCC Systems Helm chart + repository: + + helm repo add hpcc https://hpcc-systems.github.io/helm-chart/ + + To update the repository: + + helm repo update + + You should always update the repository before deploying. That + allows you to get the latest versions of the chart dependencies. + + + Installing the HPCC Systems components + + In order for a even a basic installation to succeed, it must + have some type of storage enabled. The following steps will create + ephemeral storage using the azstorage utility + that will allow the HPCC Systems to start and run but will not + persist. To do this we will deploy the hpcc-azurefile + chart which will set up Azure's ephemeral storage for the + HPCC Systems deployment. + + To Install the hpcc-azurefile chart: + + helm install azstorage hpcc/hpcc-azurefile + + The goal here is to get the default values from this + azstorage chart and create a customization file + that will pass in the appropriate values to the HPCC Systems + instance. + + Copy the output from the helm install command that you issued in + the previous step, from the storage: + parameter through the end of the file and save the file as + mystorage.yaml. The + mystorage.yaml file should look very similar to + the following: + + storage: + planes: + - name: dali + pvc: dali-azstorage-hpcc-azurefile-pvc + prefix: "/var/lib/HPCCSystems/dalistorage" + category: dali + - name: dll + pvc: dll-azstorage-hpcc-azurefile-pvc + prefix: "/var/lib/HPCCSystems/queries" + category: dll + - name: sasha + pvc: sasha-azstorage-hpcc-azurefile-pvc + prefix: "/var/lib/HPCCSystems/sasha" + category: sasha + - name: data + pvc: data-azstorage-hpcc-azurefile-pvc + prefix: "/var/lib/HPCCSystems/hpcc-data" + category: data + - name: mydropzone + pvc: mydropzone-azstorage-hpcc-azurefile-pvc + prefix: "/var/lib/HPCCSystems/dropzone" + category: lz + + +sasha: + wu-archiver: + plane: sasha + dfuwu-archiver: + plane: sasha + + + + + Note: + + + The indentation, syntax, and characters are very + critical, please be sure those are an exact match to the above + sample. A single extra space in this file can cause + unnecessary headaches. + + + We can now use this + mystorage.yaml file to pass in these values when + we start up our HPCC Systems cluster. + + + + Enable Access the ESP Services + + To access your HPCC Systems cloud instance you must enable the + visibility of the ESP services. As delivered the ESP services are + private with only local visibility. In order to enable global + visibility, we will be installing the HPCC Systems cluster using a + customization file to override the ESP dictionary. There is more + information about customizing your deployment in the + Containerized HPCC Systems documentation. + + The goal here is to get the values from this delivered chart and + create a customization file that will pass in the values you want to + the HPCC Systems instance. To get the values from that chart, enter + the following command: + + helm show values hpcc/hpcc > defaultvalues.yaml + + + + + + + + + + + + + + IMPORTANT: The + indentation, syntax, characters, as well as every single + key-value pair are very critical. Please be sure these are + an exact match to the sample below. A single extra space, or + missing character in this file can cause unnecessary + headaches. + + + + + + Using the text editor, open the + defaultvalues.yaml file and copy the esp: portion from that file, as illustrated + below: + + esp: +- name: eclwatch + ## Pre-configured esp applications include eclwatch, eclservices, and eclqueries + application: eclwatch + auth: none + replicas: 1 +# Add remote clients to generated client certificates and make the ESP require that one of +r to connect +# When setting up remote clients make sure that certificates.issuers.remote.enabled is set +# remoteClients: +# - name: myclient +# organization: mycompany + service: + ## port can be used to change the local port used by the pod. If omitted, the default por + port: 8888 + ## servicePort controls the port that this service will be exposed on, either internally + servicePort: 8010 + ## Specify visibility: local (or global) if you want the service available from outside +externally, while eclservices is designed for internal use. + visibility: local + ## Annotations can be specified on a service - for example to specify provider-specific i +-balancer-internal-subnet + #annotations: + # service.beta.kubernetes.io/azure-load-balancer-internal-subnet: "mysubnet" + # The service.annotations prefixed with hpcc.eclwatch.io should not be declared here. T + # in other services in order to be exposed in the ECLWatch interface. Similar function c + # applications. For other applications, the "eclwatch" inside the service.annotations sh + # their application names. + # hpcc.eclwatch.io/enabled: "true" + # hpcc.eclwatch.io/description: "some description" + ## You can also specify labels on a service + #labels: + # mylabel: "3" + ## Links specify the web links for a service. The web links may be shown on ECLWatch. + #links: + #- name: linkname + # description: "some description" + # url: "http://abc.com/def?g=1" + ## CIDRS allowed to access this service. + #loadBalancerSourceRanges: [1.2.3.4/32, 5.6.7.8/32] + #resources: + # cpu: "1" + # memory: "2G" +- name: eclservices + application: eclservices + auth: none + replicas: 1 + service: + servicePort: 8010 + visibility: cluster + #resources: + # cpu: "250m" + # memory: "1G" +- name: eclqueries + application: eclqueries + auth: none + replicas: 1 + service: + visibility: local + servicePort: 8002 + #annotations: + # hpcc.eclwatch.io/enabled: "true" + # hpcc.eclwatch.io/description: "Roxie Test page" + # hpcc.eclwatch.io/port: "8002" + #resources: + # cpu: "250m" + # memory: "1G" +- name: esdl-sandbox + application: esdl-sandbox + auth: none + replicas: 1 + service: + visibility: local + servicePort: 8899 + #resources: + # cpu: "250m" + # memory: "1G" +- name: sql2ecl + application: sql2ecl + auth: none + replicas: 1 +# remoteClients: +# - name: sqlclient111 + service: + visibility: local + servicePort: 8510 + #domain: hpccsql.com + #resources: + # cpu: "250m" + # memory: "1G" +- name: dfs + application: dfs + auth: none + replicas: 1 + service: + visibility: local + servicePort: 8520 + #resources: + # cpu: "250m" + # memory: "1G" + + + Save that ESP portion off into a new file called + myesp.yaml. You need to modify that file then use + it to override those default values into your deployment. + + In order to access the HPCC Systems services you must override + these default settings to make them visible. We will now set the + visibility for eclwatch and eclqueries from local to global as in the + below example. Edit the myesp.yaml file and + change the two sections highlighted in the code examples below: + + esp: +- name: eclwatch + ## Pre-configured esp applications include eclwatch, eclservices, and eclqueries + application: eclwatch + auth: none + replicas: 1 + service: + ## port can be used to change the local port used by the pod. If omitted, the default por + port: 8888 + ## servicePort controls the port that thi cesps service will be exposed on, either intern + servicePort: 8010 + ## Specify visibility: local (or global) if you want the service available from outside t +externally, while eclservices is designed for internal use. + visibility: global + ## Annotations can be specified on a service - for example to specify provider-specific i + + + + - name: eclqueries + application: eclqueries + auth: none + replicas: 1 + service: + visibility: global + servicePort: 8002 + + Save that modified myesp.yaml customization + file. + + We can now use this myesp.yaml file to pass + in these values when we start up our HPCC Systems cluster. + + + + Install the customized HPCC Systems chart + + This section will install the delivered HPCC Systems chart where + we supply the myesp.yaml and + mystorage.yaml customization files created in the + previous section. You should create or add your own additional + customizations in one of these or even another customization + yaml file specific to your requirements. Creating + and using customized versions of the HPCC Systems + values.yaml file are described in the + Customizing Configurations section of the + Containerized HPCC Systems docs. To install your + customized HPCC Systems charts: + + helm install myhpcccluster hpcc/hpcc -f myesp.yaml -f mystorage.yaml + + Where the -f option forces the system to merge in the values set + in the myesp.yaml and + mystorage.yaml files. + + + + Note: + + + You can also use the --values option as a substitute for + -f + + + + + If successful, your output will be similar to this: + + NAME: myhpcccluster +LAST DEPLOYED: Wed Dec 15 09:41:38 2021 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None + + + At this point, Kubernetes should start provisioning the HPCC + Systems pods. To check their status run: + + kubectl get pods + + + + Note: + + + If this is the first time helm install has been run, it + will take some time for the pods to get to a Running state, + since Azure will need to pull the container images from Docker. + Once all the pods are running, the HPCC Systems Cluster is ready + to be used. + + + + + + + + + + Accessing ECLWatch + + To access ECLWatch, an external IP to the ESP service running + ECLWatch is required. If you successfully deployed your cluster with the + proper visibility settings, then this will be listed as the + eclwatch service. The IP address can be obtained by + running the following command: + + kubectl get svc + + Your output should be similar to: + + NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +eclservices ClusterIP 10.0.44.11 <none> 8010/TCP 11m +eclwatch LoadBalancer 10.0.21.16 12.87.156.228 8010:30190/TCP 11m +kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 4h28m +mydali ClusterIP 10.0.195.229 <none> 7070/TCP 11m + + Use the EXTERNAL-IP address listed for the ECLWatch service. Open + a browser and go to http://<external-ip>:8010/. For example in + this case, go to http://12.87.156.228:8010. If everything is working as + expected, the ECLWatch landing page will be displayed. + + + + Uninstall Your Cluster + + When you are done using your HPCC Systems cluster, you may destroy + it to avoid incurring charges for unused resources. A storage account is + recommended to save your HPCC Systems data outside of the Azure + Kubernetes Service. That allows you to destroy the service without + losing your data. + + The various storage options and strategies are discussed elsewhere + in addition to the HPCC Systems documentation. + + + Stopping Your HPCC Systems Cluster + + This will simply stop your HPCC Systems instance. If you are + deleting the resource group, as detailed in the following section, + that will destroy everything in it, including your HPCC Systems + cluster. Uninstalling the HPCC Systems deployment in that case, is + redundant. You will still be charged for the AKS. If, for whatever + reason, you can't destroy the resource group, then you may follow the + steps in this section to shut down your HPCC Systems cluster. + + To shut down your HPCC Systems cluster, you would issue the helm + uninstall command. + + Using the Azure cloud shell, enter: + + helm list + + Enter the helm uninstall command using your clusters name as the + argument, for example: + + helm uninstall myhpcccluster + + This will remove the HPCC Systems cluster named + <myhpcccluster> you had previously deployed. + + + + Removing the Resource Group + + Removing the resource group will irreversibly destroy any pods, + clusters, contents, or any other work stored on there. Please + carefully consider these actions, before removing the resource group. + Once removed it can not be undone. + + To remove the entire resource group rg-hpcc + which we created earlier, and all the entirety of its contents, issue + the following command: + + az group delete --name rg-hpcc + + It will prompt you if you are sure you want to do this, and if + you confirm it will delete the entire resource group. + + + + From b54c6f6caa1b7da6171c0f1f49bdaa2d1a63b514 Mon Sep 17 00:00:00 2001 From: Jake Smith Date: Fri, 6 May 2022 12:51:27 +0100 Subject: [PATCH 22/24] HPCC-27616 Fix local/group sort stats. issues Also change to reuse a single iLoader instance, rather than recreating per CQ execution. Signed-off-by: Jake Smith --- system/jlib/jstats.h | 36 +++++++++++++++++++ thorlcr/activities/msort/thgroupsortslave.cpp | 28 ++++++--------- thorlcr/thorutil/thmem.cpp | 34 +++++++++--------- thorlcr/thorutil/thmem.hpp | 2 +- 4 files changed, 65 insertions(+), 35 deletions(-) diff --git a/system/jlib/jstats.h b/system/jlib/jstats.h index 268c98b113c..20ed059e702 100644 --- a/system/jlib/jstats.h +++ b/system/jlib/jstats.h @@ -815,6 +815,42 @@ void mergeStat(CRuntimeStatisticCollection & stats, INTERFACE * source, Statisti template void mergeStat(CRuntimeStatisticCollection & stats, const Shared & source, StatisticKind kind) { mergeStat(stats, source.get(), kind); } + +//Some template helper classes for overwriting/setting statistics from external sources. + +template +void setStats(CRuntimeStatisticCollection & stats, INTERFACE * source, const StatisticsMapping & mapping) +{ + if (!source) + return; + + unsigned max = mapping.numStatistics(); + for (unsigned i=0; i < max; i++) + { + StatisticKind kind = mapping.getKind(i); + stats.setStatistic(kind, source->getStatistic(kind)); + } +} + +template +void setStats(CRuntimeStatisticCollection & stats, const Shared & source, const StatisticsMapping & mapping) { setStats(stats, source.get(), mapping); } + +template +void setStats(CRuntimeStatisticCollection & stats, INTERFACE * source) { setStats(stats, source, stats.queryMapping()); } + +template +void setStats(CRuntimeStatisticCollection & stats, const Shared & source) { setStats(stats, source.get(), stats.queryMapping()); } + +template +void setStat(CRuntimeStatisticCollection & stats, INTERFACE * source, StatisticKind kind) +{ + if (source) + stats.setStatistic(kind, source->getStatistic(kind)); +} + +template +void setStat(CRuntimeStatisticCollection & stats, const Shared & source, StatisticKind kind) { setStat(stats, source.get(), kind); } + //--------------------------------------------------------------------------------------------------------------------- //A class for minimizing the overhead of collecting timestamps. diff --git a/thorlcr/activities/msort/thgroupsortslave.cpp b/thorlcr/activities/msort/thgroupsortslave.cpp index 956a26adc8e..1c3257f8aed 100644 --- a/thorlcr/activities/msort/thgroupsortslave.cpp +++ b/thorlcr/activities/msort/thgroupsortslave.cpp @@ -48,18 +48,20 @@ class CLocalSortSlaveActivity : public CSlaveActivity helper = (IHThorSortArg *)queryHelper(); iCompare = helper->queryCompare(); unstable = helper->getAlgorithmFlags()&TAFunstable; + unsigned spillPriority = container.queryGrouped() ? SPILL_PRIORITY_GROUPSORT : SPILL_PRIORITY_LARGESORT; + iLoader.setown(createThorRowLoader(*this, iCompare, unstable ? stableSort_none : stableSort_earlyAlloc, rc_mixed, spillPriority)); setRequireInitData(false); appendOutputLinked(this); } - virtual void start() + virtual void reset() override + { + PARENT::reset(); + iLoader->reset(); + } + virtual void start() override { ActivityTimer s(slaveTimerStats, timeActivities); PARENT::start(); - unsigned spillPriority = container.queryGrouped() ? SPILL_PRIORITY_GROUPSORT : SPILL_PRIORITY_LARGESORT; - { - CriticalBlock block(loaderCs); - iLoader.setown(createThorRowLoader(*this, queryRowInterfaces(input), iCompare, unstable ? stableSort_none : stableSort_earlyAlloc, rc_mixed, spillPriority)); - } eoi = false; if (container.queryGrouped()) out.setown(iLoader->loadGroup(inputStream, abortSoon)); @@ -70,22 +72,14 @@ class CLocalSortSlaveActivity : public CSlaveActivity } virtual void serializeStats(MemoryBuffer &mb) override { - { - CriticalBlock block(loaderCs); - mergeStats(stats, iLoader, spillStatistics); - } + setStats(stats, iLoader, spillStatistics); PARENT::serializeStats(mb); } - - virtual void stop() + virtual void stop() override { out.clear(); if (hasStarted()) - { - CriticalBlock block(loaderCs); - mergeStats(stats, iLoader, spillStatistics); - iLoader.clear(); - } + setStats(stats, iLoader, spillStatistics); PARENT::stop(); } CATCH_NEXTROW() diff --git a/thorlcr/thorutil/thmem.cpp b/thorlcr/thorutil/thmem.cpp index ca7bee2e734..c47244c2d65 100644 --- a/thorlcr/thorutil/thmem.cpp +++ b/thorlcr/thorutil/thmem.cpp @@ -1624,15 +1624,16 @@ class CThorRowCollectorBase : public CSpillable unsigned overflowCount = 0; unsigned maxCores = 0; unsigned outStreams = 0; - offset_t sizeSpill = 0; ICompare *iCompare; StableSortFlag stableSort; EmptyRowSemantics emptyRowSemantics = ers_forbidden; Owned spillableRowSet; unsigned options = 0; unsigned spillCompInfo = 0; - __uint64 spillCycles = 0; - __uint64 sortCycles = 0; + RelaxedAtomic statOverflowCount{0}; + RelaxedAtomic statSizeSpill{0}; + RelaxedAtomic<__uint64> statSpillCycles{0}; + RelaxedAtomic<__uint64> statSortCycles{0}; bool spillRows(bool critical) { @@ -1648,7 +1649,7 @@ class CThorRowCollectorBase : public CSpillable { CCycleTimer timer; spillableRows.sort(*iCompare, maxCores); // sorts committed rows - sortCycles += timer.elapsedCycles(); + statSortCycles.fastAdd(timer.elapsedCycles()); ActPrintLog(&activity, "%sSorting %" RIPF "u rows took: %f", tracingPrefix.str(), spillableRows.numCommitted(), ((float)timer.elapsedMs())/1000); tempPrefix.append("srt"); } @@ -1659,8 +1660,9 @@ class CThorRowCollectorBase : public CSpillable spillableRows.save(*iFile, spillCompInfo, false, spillPrefixStr.str()); // saves committed rows spillFiles.append(new CFileOwner(iFile.getLink())); ++overflowCount; - sizeSpill += iFile->size(); - spillCycles += spillTimer.elapsedCycles(); + statOverflowCount.fastAdd(1); // NB: this is total over multiple uses of this class + statSizeSpill.fastAdd(iFile->size()); + statSpillCycles.fastAdd(spillTimer.elapsedCycles()); return true; } void setEmptyRowSemantics(EmptyRowSemantics _emptyRowSemantics) @@ -1748,7 +1750,7 @@ class CThorRowCollectorBase : public CSpillable { CCycleTimer timer; spillableRows.sort(*iCompare, maxCores); - sortCycles += timer.elapsedCycles(); + statSortCycles.fastAdd(timer.elapsedCycles()); } if ((rc_allDiskOrAllMem == diskMemMix) || // must supply allMemRows, only here if no spilling (see above) @@ -1829,9 +1831,6 @@ class CThorRowCollectorBase : public CSpillable spillFiles.kill(); totalRows = 0; overflowCount = outStreams = 0; - sizeSpill = 0; - spillCycles = 0; - sortCycles = 0; } public: CThorRowCollectorBase(CActivityBase &_activity, IThorRowInterfaces *_rowIf, ICompare *_iCompare, StableSortFlag _stableSort, RowCollectorSpillFlags _diskMemMix, unsigned _spillPriority) @@ -1896,7 +1895,7 @@ class CThorRowCollectorBase : public CSpillable { CCycleTimer timer; spillableRows.sort(*iCompare, maxCores); - sortCycles += timer.elapsedCycles(); + statSortCycles.fastAdd(timer.elapsedCycles()); } out.transferFrom(spillableRows); } @@ -1946,17 +1945,17 @@ class CThorRowCollectorBase : public CSpillable switch (kind) { case StCycleSpillElapsedCycles: - return spillCycles; + return statSpillCycles; case StCycleSortElapsedCycles: - return sortCycles; + return statSortCycles; case StTimeSpillElapsed: - return cycle_to_nanosec(spillCycles); + return cycle_to_nanosec(statSpillCycles); case StTimeSortElapsed: - return cycle_to_nanosec(sortCycles); + return cycle_to_nanosec(statSortCycles); case StNumSpills: - return overflowCount; + return statOverflowCount; case StSizeSpillFile: - return sizeSpill; + return statSizeSpill; default: break; } @@ -2032,6 +2031,7 @@ class CThorRowLoader : public CThorRowCollectorBase, implements IThorRowLoader virtual unsigned __int64 getStatistic(StatisticKind kind) override { return CThorRowCollectorBase::getStatistic(kind); } virtual bool hasSpilt() const override { return CThorRowCollectorBase::hasSpilt(); } virtual void setTracingPrefix(const char *tracing) override { CThorRowCollectorBase::setTracingPrefix(tracing); } + virtual void reset() override { CThorRowCollectorBase::reset(); } // IThorArrayLock virtual void lock() const override { CThorRowCollectorBase::lock(); } diff --git a/thorlcr/thorutil/thmem.hpp b/thorlcr/thorutil/thmem.hpp index 6fa5c2d08aa..eeb371a31e1 100644 --- a/thorlcr/thorutil/thmem.hpp +++ b/thorlcr/thorutil/thmem.hpp @@ -544,6 +544,7 @@ interface IThorRowCollectorCommon : extends IInterface, extends IThorArrayLock virtual unsigned __int64 getStatistic(StatisticKind kind) = 0; virtual bool hasSpilt() const = 0; // equivalent to numOverlows() >= 1 virtual void setTracingPrefix(const char *tracing) = 0; + virtual void reset() = 0; }; interface IThorRowLoader : extends IThorRowCollectorCommon @@ -556,7 +557,6 @@ interface IThorRowCollector : extends IThorRowCollectorCommon { virtual void setEmptyRowSemantics(EmptyRowSemantics emptyRowSemantics) = 0; virtual IRowWriter *getWriter() = 0; - virtual void reset() = 0; virtual IRowStream *getStream(bool shared=false, CThorExpandingRowArray *allMemRows=NULL) = 0; virtual bool spill(bool critical) = 0; // manual spill. Returns true if anything spilt virtual bool flush() = 0; // manual flush (free array space and potentially ptr table) From 609ed0599148f67ab06df659766f8c7f6b7a25f3 Mon Sep 17 00:00:00 2001 From: Russ Whitehead Date: Tue, 10 May 2022 11:24:22 -0400 Subject: [PATCH 23/24] HPCC-27621 LDAP regression connecting to secure AD When connecting to LDAP AD query server parameters, the code used to attempt anonymous bind on nonsecure (389) port if the secure port failed. This code was erroneously removed (https://track.hpccsystems.com/browse/HPCC-27522), as it has now been determined it needed to query the domain name other to allow the component to properly initialize. Signed-off-by: Russ Whitehead --- system/security/LdapSecurity/ldaputils.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/system/security/LdapSecurity/ldaputils.cpp b/system/security/LdapSecurity/ldaputils.cpp index 6b7ef7860ad..9c5b079ba13 100644 --- a/system/security/LdapSecurity/ldaputils.cpp +++ b/system/security/LdapSecurity/ldaputils.cpp @@ -248,16 +248,18 @@ int LdapUtils::getServerInfo(const char* ldapserver, const char* userDN, const c if (nullptr == ld) { ld = ldapInitAndSimpleBind(ldapserver, userDN, pwd, ldapprotocol, ldapport, timeout, &err); - if(nullptr == ld) + if(nullptr == ld && strieq(ldapprotocol,"ldaps")) { - DBGLOG("ldap bind error (%d) - %s", err, ldap_err2string(err)); + //if that failed, and was for ldaps, see if we can do anonymous bind using ldap/389 + ld = ldapInitAndSimpleBind(ldapserver, nullptr, nullptr, "ldap", 389, timeout, &err); + } - // for new versions of openldap, version 2.2.* - if(err == LDAP_PROTOCOL_ERROR && stype != ACTIVE_DIRECTORY) - DBGLOG("If you're trying to connect to an OpenLdap server, make sure you have \"allow bind_v2\" enabled in slapd.conf"); + // for new versions of openldap, version 2.2.* + if(nullptr == ld && err == LDAP_PROTOCOL_ERROR && stype != ACTIVE_DIRECTORY) + DBGLOG("If you're trying to connect to an OpenLdap server, make sure you have \"allow bind_v2\" enabled in slapd.conf"); - return err; - } + if(nullptr == ld) + return err;//unable to connect, give up } LDAPMessage* msg = NULL; From f0a7a4673cc36b643cf7586a33856bb51d784e6b Mon Sep 17 00:00:00 2001 From: Gavin Halliday Date: Tue, 10 May 2022 17:42:37 +0100 Subject: [PATCH 24/24] Split off 8.6.26 Signed-off-by: Gavin Halliday --- helm/hpcc/Chart.yaml | 4 ++-- helm/hpcc/templates/_helpers.tpl | 2 +- helm/hpcc/templates/dali.yaml | 2 +- helm/hpcc/templates/dfuserver.yaml | 2 +- helm/hpcc/templates/eclagent.yaml | 4 ++-- helm/hpcc/templates/eclccserver.yaml | 4 ++-- helm/hpcc/templates/eclscheduler.yaml | 2 +- helm/hpcc/templates/esp.yaml | 2 +- helm/hpcc/templates/localroxie.yaml | 2 +- helm/hpcc/templates/roxie.yaml | 8 ++++---- helm/hpcc/templates/sasha.yaml | 2 +- helm/hpcc/templates/thor.yaml | 10 +++++----- version.cmake | 2 +- 13 files changed, 23 insertions(+), 23 deletions(-) diff --git a/helm/hpcc/Chart.yaml b/helm/hpcc/Chart.yaml index 78b65742266..08218dde0a9 100644 --- a/helm/hpcc/Chart.yaml +++ b/helm/hpcc/Chart.yaml @@ -6,9 +6,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. -version: 8.6.25-closedown0 +version: 8.6.27-closedown0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. -appVersion: 8.6.25-closedown0 +appVersion: 8.6.27-closedown0 diff --git a/helm/hpcc/templates/_helpers.tpl b/helm/hpcc/templates/_helpers.tpl index 10a84b54823..9c73f357726 100644 --- a/helm/hpcc/templates/_helpers.tpl +++ b/helm/hpcc/templates/_helpers.tpl @@ -1166,7 +1166,7 @@ kind: Service metadata: name: {{ $lvars.serviceName | quote }} labels: - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 {{- if $lvars.labels }} {{ toYaml $lvars.labels | indent 4 }} {{- end }} diff --git a/helm/hpcc/templates/dali.yaml b/helm/hpcc/templates/dali.yaml index aedbc874e85..e1be67700c6 100644 --- a/helm/hpcc/templates/dali.yaml +++ b/helm/hpcc/templates/dali.yaml @@ -83,7 +83,7 @@ spec: run: {{ $dali.name | quote }} server: {{ $dali.name | quote }} app: dali - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 {{- if hasKey $.Values.global "metrics" }} {{- include "hpcc.generateMetricsReporterLabel" $.Values.global.metrics | nindent 8 }} {{- end }} diff --git a/helm/hpcc/templates/dfuserver.yaml b/helm/hpcc/templates/dfuserver.yaml index e931160737c..145c05d9bfd 100644 --- a/helm/hpcc/templates/dfuserver.yaml +++ b/helm/hpcc/templates/dfuserver.yaml @@ -55,7 +55,7 @@ spec: labels: run: {{ .name | quote }} accessDali: "yes" - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 {{- if hasKey . "labels" }} {{ toYaml .labels | indent 8 }} {{- end }} diff --git a/helm/hpcc/templates/eclagent.yaml b/helm/hpcc/templates/eclagent.yaml index 224ee8b86d8..48c09ebcb1c 100644 --- a/helm/hpcc/templates/eclagent.yaml +++ b/helm/hpcc/templates/eclagent.yaml @@ -57,7 +57,7 @@ data: labels: accessDali: "yes" accessEsp: "yes" - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 {{- if hasKey .me "labels" }} {{ toYaml .me.labels | indent 12 }} {{- end }} @@ -131,7 +131,7 @@ spec: run: {{ .name | quote }} accessDali: "yes" accessEsp: {{ .useChildProcesses | default false | ternary "yes" "no" | quote }} - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 {{- if hasKey . "labels" }} {{ toYaml .labels | indent 8 }} {{- end }} diff --git a/helm/hpcc/templates/eclccserver.yaml b/helm/hpcc/templates/eclccserver.yaml index be784e4a56e..a87bd6715f1 100644 --- a/helm/hpcc/templates/eclccserver.yaml +++ b/helm/hpcc/templates/eclccserver.yaml @@ -56,7 +56,7 @@ data: labels: accessDali: "yes" accessEsp: "yes" - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 {{- if hasKey .me "labels" }} {{ toYaml .me.labels | indent 12 }} {{- end }} @@ -144,7 +144,7 @@ spec: run: {{ .name | quote }} accessDali: "yes" accessEsp: {{ .useChildProcesses | default false | ternary "yes" "no" | quote }} - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 {{- if hasKey . "labels" }} {{ toYaml .labels | indent 8 }} {{- end }} diff --git a/helm/hpcc/templates/eclscheduler.yaml b/helm/hpcc/templates/eclscheduler.yaml index aa6a391cc17..b978f36cc97 100644 --- a/helm/hpcc/templates/eclscheduler.yaml +++ b/helm/hpcc/templates/eclscheduler.yaml @@ -64,7 +64,7 @@ spec: run: {{ .name | quote }} accessDali: "yes" accessEsp: "no" - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 {{- if hasKey . "labels" }} {{ toYaml .labels | indent 8 }} {{- end }} diff --git a/helm/hpcc/templates/esp.yaml b/helm/hpcc/templates/esp.yaml index 4258d788275..f0aa2cb343e 100644 --- a/helm/hpcc/templates/esp.yaml +++ b/helm/hpcc/templates/esp.yaml @@ -114,7 +114,7 @@ spec: server: {{ .name | quote }} accessDali: "yes" app: {{ $application }} - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 {{- if hasKey $.Values.global "metrics" }} {{- include "hpcc.generateMetricsReporterLabel" $.Values.global.metrics | nindent 8 }} {{- end }} diff --git a/helm/hpcc/templates/localroxie.yaml b/helm/hpcc/templates/localroxie.yaml index 9cdc091e44f..cb000c6d2a9 100644 --- a/helm/hpcc/templates/localroxie.yaml +++ b/helm/hpcc/templates/localroxie.yaml @@ -68,7 +68,7 @@ spec: server: {{ $servername | quote }} accessDali: "yes" accessEsp: "yes" - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 {{- if hasKey . "labels" }} {{ toYaml .labels | indent 8 }} {{- end }} diff --git a/helm/hpcc/templates/roxie.yaml b/helm/hpcc/templates/roxie.yaml index bd9bd26bc4b..1ee908749bc 100644 --- a/helm/hpcc/templates/roxie.yaml +++ b/helm/hpcc/templates/roxie.yaml @@ -115,7 +115,7 @@ spec: labels: run: {{ $commonCtx.toponame | quote }} roxie-cluster: {{ $roxie.name | quote }} - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 {{- if hasKey $.Values.global "metrics" }} {{- include "hpcc.generateMetricsReporterLabel" $.Values.global.metrics | nindent 8}} {{- end }} @@ -175,7 +175,7 @@ kind: Service metadata: name: {{ $commonCtx.toponame | quote }} labels: - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 spec: ports: - port: {{ $commonCtx.topoport }} @@ -234,7 +234,7 @@ spec: roxie-cluster: {{ $roxie.name | quote }} accessDali: "yes" accessEsp: "yes" - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 {{- if hasKey $.Values.global "metrics" }} {{- include "hpcc.generateMetricsReporterLabel" $.Values.global.metrics | nindent 8}} {{- end }} @@ -335,7 +335,7 @@ spec: roxie-cluster: {{ $roxie.name | quote }} accessDali: "yes" accessEsp: "yes" - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 {{- if hasKey $.Values.global "metrics" }} {{- include "hpcc.generateMetricsReporterLabel" $.Values.global.metrics | nindent 8}} {{- end }} diff --git a/helm/hpcc/templates/sasha.yaml b/helm/hpcc/templates/sasha.yaml index 4123e811440..1746a43b3b6 100644 --- a/helm/hpcc/templates/sasha.yaml +++ b/helm/hpcc/templates/sasha.yaml @@ -52,7 +52,7 @@ spec: run: {{ $serviceName | quote }} server: {{ $serviceName | quote }} accessDali: {{ (has "dali" $sasha.access) | ternary "yes" "no" | quote }} - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 {{- if hasKey $sasha "labels" }} {{ toYaml $sasha.labels | indent 8 }} {{- end }} diff --git a/helm/hpcc/templates/thor.yaml b/helm/hpcc/templates/thor.yaml index 226d2a3d4f3..90292eae80d 100644 --- a/helm/hpcc/templates/thor.yaml +++ b/helm/hpcc/templates/thor.yaml @@ -82,7 +82,7 @@ data: labels: accessDali: "yes" accessEsp: "yes" - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 {{- if hasKey .me "labels" }} {{ toYaml .me.labels | indent 12 }} {{- end }} @@ -148,7 +148,7 @@ data: accessEsp: "yes" app: "thor" component: "thormanager" - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 instance: "_HPCC_JOBNAME_" job: "_HPCC_JOBNAME_" {{- if hasKey $thorScope "labels" }} @@ -215,7 +215,7 @@ data: accessEsp: "true" app: "thor" component: "thorworker" - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 instance: "_HPCC_JOBNAME_" job: "_HPCC_JOBNAME_" {{- if hasKey $thorScope "labels" }} @@ -348,7 +348,7 @@ spec: accessEsp: {{ $commonCtx.eclAgentUseChildProcesses | ternary "yes" "no" | quote }} app: "thor" component: "thor-eclagent" - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 instance: {{ $commonCtx.eclAgentName | quote }} {{- if hasKey $commonCtx.me "labels" }} {{ toYaml $commonCtx.me.labels | indent 8 }} @@ -409,7 +409,7 @@ spec: accessEsp: "no" app: "thor" component: "thor-thoragent" - helmVersion: 8.6.25-closedown0 + helmVersion: 8.6.27-closedown0 instance: {{ $commonCtx.thorAgentName | quote }} {{- if hasKey $commonCtx.me "labels" }} {{ toYaml $commonCtx.me.labels | indent 8 }} diff --git a/version.cmake b/version.cmake index b9ebfe3126b..a504e8a7c6b 100644 --- a/version.cmake +++ b/version.cmake @@ -5,7 +5,7 @@ set ( HPCC_NAME "Community Edition" ) set ( HPCC_PROJECT "community" ) set ( HPCC_MAJOR 8 ) set ( HPCC_MINOR 6 ) -set ( HPCC_POINT 25 ) +set ( HPCC_POINT 27 ) set ( HPCC_MATURITY "closedown" ) set ( HPCC_SEQUENCE 0 ) ###