From f0983b81774cb5aa93cb250a4d43813d45135cb1 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Thu, 23 May 2024 21:15:44 +0100 Subject: [PATCH 1/9] added log to file capability Signed-off-by: Jeromy Cannon --- src/Executable.js | 4 +-- src/grpc/GrpcServiceError.js | 7 ++++ src/grpc/GrpcStatus.js | 7 ++++ src/logger/Logger.js | 63 +++++++++++++++++++++++++++++------- test/unit/LoggerTest.js | 21 ++++++++++++ 5 files changed, 89 insertions(+), 13 deletions(-) diff --git a/src/Executable.js b/src/Executable.js index 0171cea13..9d8140d64 100644 --- a/src/Executable.js +++ b/src/Executable.js @@ -725,7 +725,7 @@ export default class Executable { throw this._mapStatusError(request, response); default: throw new Error( - "(BUG) non-exhuastive switch statement for `ExecutionState`", + "(BUG) non-exhaustive switch statement for `ExecutionState`", ); } } @@ -742,7 +742,7 @@ export default class Executable { /** * The current purpose of this method is to easily support signature providers since * signature providers need to serialize _any_ request into bytes. `Query` and `Transaction` - * already implement `toBytes()` so it only made sense to make it avaiable here too. + * already implement `toBytes()` so it only made sense to make it available here too. * * @abstract * @returns {Uint8Array} diff --git a/src/grpc/GrpcServiceError.js b/src/grpc/GrpcServiceError.js index 5345043c4..5ce614454 100644 --- a/src/grpc/GrpcServiceError.js +++ b/src/grpc/GrpcServiceError.js @@ -61,4 +61,11 @@ export default class GrpcServiceError extends Error { return /** @type {Error} */ (obj); } } + + /** + * @returns {string} + */ + toString() { + return `${this.name}: ${this.message} (${this.status.toString()})`; + } } diff --git a/src/grpc/GrpcStatus.js b/src/grpc/GrpcStatus.js index 4f398f0df..5fd44035a 100644 --- a/src/grpc/GrpcStatus.js +++ b/src/grpc/GrpcStatus.js @@ -131,6 +131,13 @@ export default class GrpcStatus { } } + /** + * @returns {string} + */ + toJSON() { + return `{"toString":"${this.toString()}","code":"${this._code}"}`; + } + /** * @returns {number} */ diff --git a/src/logger/Logger.js b/src/logger/Logger.js index 704d925d2..dd60158eb 100644 --- a/src/logger/Logger.js +++ b/src/logger/Logger.js @@ -23,22 +23,63 @@ import LogLevel from "./LogLevel.js"; export default class Logger { /** * @param {LogLevel} level + * @param {string} logFile the file to log to, if empty, logs to console + * @param {boolean} sync perform writes synchronously (similar to console.log) + * @param {boolean} fsync perform a fsyncSync every time a write is completed + * @param {boolean} mkdir ensure directory for dest file exists when true (default false) + * @param {number} minLength the minimum length of the internal buffer that is required to be full before flushing */ - constructor(level) { + constructor( + level, + logFile = "", + sync = true, + fsync = true, + mkdir = true, + minLength = 0, + ) { + const fileTransport = logFile + ? pino.destination({ + dest: logFile, + sync, + fsync, + mkdir, + minLength, + }) + : null; + + const loggerOptions = fileTransport + ? { + level: level.toString(), + timestamp: pino.stdTimeFunctions.isoTime, + formatters: { + bindings: () => { + return {}; + }, + // @ts-ignore + level: (label) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + return { level: label.toUpperCase() }; + }, + }, + } + : { + level: level.toString(), + transport: { + target: "pino-pretty", + options: { + translateTime: "SYS:dd-mm-yyyy HH:MM:ss", + ignore: "pid,hostname", + }, + }, + }; + /** * @private * @type {import("pino").Logger} */ - this._logger = pino({ - level: level.toString(), - transport: { - target: "pino-pretty", - options: { - translateTime: "SYS:dd-mm-yyyy HH:MM:ss", - ignore: "pid,hostname", - }, - }, - }); + this._logger = fileTransport + ? pino(loggerOptions, fileTransport) + : pino(loggerOptions); /** * @private diff --git a/test/unit/LoggerTest.js b/test/unit/LoggerTest.js index 59d968a73..ee1aedbff 100644 --- a/test/unit/LoggerTest.js +++ b/test/unit/LoggerTest.js @@ -1,5 +1,7 @@ import { Logger, LogLevel, Transaction } from "../../src/exports.js"; import { Client } from "../../src/index.js"; +import { tmpdir } from "node:os"; +import fs from "fs"; describe("Logger", function () { this.timeout(50000); @@ -68,4 +70,23 @@ describe("Logger", function () { expect(levels).to.include("error"); expect(levels).to.include("fatal"); }); + + it("check that it can write to a log file", async function () { + const logFile = tmpdir() + "/test.log"; + const logger = new Logger(LogLevel.Info, logFile); + logger.info("This is a test log message"); + logger.warn("This is a test warning message"); + logger.error("This is a test error message"); + logger.fatal("This is a test fatal message"); + // read the log file and check if the messages are there and check that the levels are correct + const logContent = fs.readFileSync(logFile, "utf8"); + expect(logContent).to.contain("This is a test log message"); + expect(logContent).to.contain(LogLevel.Info.toString().toUpperCase()); + expect(logContent).to.contain("This is a test warning message"); + expect(logContent).to.contain(LogLevel.Warn.toString().toUpperCase()); + expect(logContent).to.contain("This is a test error message"); + expect(logContent).to.contain(LogLevel.Error.toString().toUpperCase()); + expect(logContent).to.contain("This is a test fatal message"); + expect(logContent).to.contain(LogLevel.Fatal.toString().toUpperCase()); + }); }); From ba968669ba9cdff4eb0507c4987c7a2437fa6207 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Fri, 24 May 2024 12:38:20 +0100 Subject: [PATCH 2/9] updated test case and the way the grpc error is created Signed-off-by: Jeromy Cannon --- src/grpc/GrpcServiceError.js | 5 ++-- src/grpc/GrpcStatus.js | 45 ++++++++++++++-------------------- test/unit/FileAppendMocking.js | 2 +- 3 files changed, 23 insertions(+), 29 deletions(-) diff --git a/src/grpc/GrpcServiceError.js b/src/grpc/GrpcServiceError.js index 5ce614454..a3659e720 100644 --- a/src/grpc/GrpcServiceError.js +++ b/src/grpc/GrpcServiceError.js @@ -55,7 +55,8 @@ export default class GrpcServiceError extends Error { if (obj.code != null && obj.details != null) { const status = GrpcStatus._fromValue(obj.code); const err = new GrpcServiceError(status); - err.message = obj.details; + err.stack += + "\nCaused by: " + (obj.stack ? obj.stack.toString() : ""); return err; } else { return /** @type {Error} */ (obj); @@ -66,6 +67,6 @@ export default class GrpcServiceError extends Error { * @returns {string} */ toString() { - return `${this.name}: ${this.message} (${this.status.toString()})`; + return `${this.name}: ${this.message}`; } } diff --git a/src/grpc/GrpcStatus.js b/src/grpc/GrpcStatus.js index 5fd44035a..21617733d 100644 --- a/src/grpc/GrpcStatus.js +++ b/src/grpc/GrpcStatus.js @@ -89,55 +89,48 @@ export default class GrpcStatus { toString() { switch (this) { case GrpcStatus.Ok: - return "OK"; + return `OK (${this._code})`; case GrpcStatus.Cancelled: - return "CANCELLED"; + return `CANCELLED (${this._code})`; case GrpcStatus.Unknown: - return "UNKNOWN"; + return `UNKNOWN (${this._code})`; case GrpcStatus.InvalidArgument: - return "INVALID_ARGUMENT"; + return `INVALID_ARGUMENT (${this._code})`; case GrpcStatus.DeadlineExceeded: - return "DEADLINE_EXCEEDED"; + return `DEADLINE_EXCEEDED (${this._code})`; case GrpcStatus.NotFound: - return "NOT_FOUND"; + return `NOT_FOUND (${this._code})`; case GrpcStatus.AlreadyExists: - return "ALREADY_EXISTS"; + return `ALREADY_EXISTS (${this._code})`; case GrpcStatus.PermissionDenied: - return "PERMISSION_DENIED"; + return `PERMISSION_DENIED (${this._code})`; case GrpcStatus.Unauthenticated: - return "UNAUTHENTICATED"; + return `UNAUTHENTICATED (${this._code})`; case GrpcStatus.ResourceExhausted: - return "RESOURCE_EXHAUSTED"; + return `RESOURCE_EXHAUSTED (${this._code})`; case GrpcStatus.FailedPrecondition: - return "FAILED_PRECONDITION"; + return `FAILED_PRECONDITION (${this._code})`; case GrpcStatus.Aborted: - return "ABORTED"; + return `ABORTED (${this._code})`; case GrpcStatus.OutOfRange: - return "OUT_OF_RANGE"; + return `OUT_OF_RANGE (${this._code})`; case GrpcStatus.Unimplemented: - return "UNIMPLEMENTED"; + return `UNIMPLEMENTED (${this._code})`; case GrpcStatus.Internal: - return "INTERNAL"; + return `INTERNAL (${this._code})`; case GrpcStatus.Unavailable: - return "UNAVAILABLE"; + return `UNAVAILABLE (${this._code})`; case GrpcStatus.DataLoss: - return "DATA_LOSS"; + return `DATA_LOSS (${this._code})`; case GrpcStatus.Timeout: - return "TIMEOUT"; + return `TIMEOUT (${this._code})`; case GrpcStatus.GrpcWeb: - return "GRPC_WEB"; + return `GRPC_WEB (${this._code})`; default: return `UNKNOWN (${this._code})`; } } - /** - * @returns {string} - */ - toJSON() { - return `{"toString":"${this.toString()}","code":"${this._code}"}`; - } - /** * @returns {number} */ diff --git a/test/unit/FileAppendMocking.js b/test/unit/FileAppendMocking.js index aab0ccf5d..bb4bcdd76 100644 --- a/test/unit/FileAppendMocking.js +++ b/test/unit/FileAppendMocking.js @@ -168,7 +168,7 @@ describe("FileAppendMocking", function () { } catch (error) { if ( error.message !== - "max attempts of 1 was reached for request with last error being: GrpcServiceError: node is UNAVAILABLE" + "max attempts of 1 was reached for request with last error being: GrpcServiceError: gRPC service failed with status: UNAVAILABLE (14)" ) { throw error; } From cdd33ba839826ecc65dc1cddea43f101decb2795 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Fri, 24 May 2024 14:56:52 +0100 Subject: [PATCH 3/9] include the original grpc error message in the new error Signed-off-by: Jeromy Cannon --- src/grpc/GrpcServiceError.js | 1 + test/unit/FileAppendMocking.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/grpc/GrpcServiceError.js b/src/grpc/GrpcServiceError.js index a3659e720..e0bfa7c5c 100644 --- a/src/grpc/GrpcServiceError.js +++ b/src/grpc/GrpcServiceError.js @@ -57,6 +57,7 @@ export default class GrpcServiceError extends Error { const err = new GrpcServiceError(status); err.stack += "\nCaused by: " + (obj.stack ? obj.stack.toString() : ""); + err.message += `: ${obj.details}`; return err; } else { return /** @type {Error} */ (obj); diff --git a/test/unit/FileAppendMocking.js b/test/unit/FileAppendMocking.js index bb4bcdd76..fbaab296c 100644 --- a/test/unit/FileAppendMocking.js +++ b/test/unit/FileAppendMocking.js @@ -168,7 +168,7 @@ describe("FileAppendMocking", function () { } catch (error) { if ( error.message !== - "max attempts of 1 was reached for request with last error being: GrpcServiceError: gRPC service failed with status: UNAVAILABLE (14)" + "max attempts of 1 was reached for request with last error being: GrpcServiceError: gRPC service failed with status: UNAVAILABLE (14): node is UNAVAILABLE" ) { throw error; } From 1ff63d154e2c9fc42f85f9fd108f194f35d283e3 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Fri, 24 May 2024 15:41:50 +0100 Subject: [PATCH 4/9] bumped timeout because of pipeline failure Signed-off-by: Jeromy Cannon --- test/unit/AccountInfoMocking.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/AccountInfoMocking.js b/test/unit/AccountInfoMocking.js index 7af12a535..6366938f3 100644 --- a/test/unit/AccountInfoMocking.js +++ b/test/unit/AccountInfoMocking.js @@ -154,7 +154,7 @@ describe("AccountInfoMocking", function () { await query.execute(client, 1); expect(query._queryPayment.toTinybars().toInt()).to.be.equal(10); - }); + }, 15000); it("setQueryPayemnt() + setMaxQueryPayment() avoids querying actual cost", async function () { this.timeout(10000); From cda486d15ea4f135d465c06250aa744c49ae98da Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 29 May 2024 06:50:50 -0500 Subject: [PATCH 5/9] Update src/grpc/GrpcServiceError.js Co-authored-by: Alexander Gadzhalov Signed-off-by: Jeromy Cannon --- src/grpc/GrpcServiceError.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/grpc/GrpcServiceError.js b/src/grpc/GrpcServiceError.js index e0bfa7c5c..8ef02c71e 100644 --- a/src/grpc/GrpcServiceError.js +++ b/src/grpc/GrpcServiceError.js @@ -55,8 +55,7 @@ export default class GrpcServiceError extends Error { if (obj.code != null && obj.details != null) { const status = GrpcStatus._fromValue(obj.code); const err = new GrpcServiceError(status); - err.stack += - "\nCaused by: " + (obj.stack ? obj.stack.toString() : ""); + err.stack += `\nCaused by: ${obj.stack ? obj.stack.toString() : ""}`; err.message += `: ${obj.details}`; return err; } else { From 33fa0729943544c93555e4c5a699918cc7acbd43 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 29 May 2024 18:56:15 +0100 Subject: [PATCH 6/9] updates based on PR review feedback Signed-off-by: Jeromy Cannon --- package.json | 1 + src/grpc/GrpcServiceError.js | 8 +++-- src/grpc/GrpcStatus.js | 38 ++++++++++---------- test/unit/FileAppendMocking.js | 2 +- test/unit/LoggerTest.js | 65 ++++++++++++++++++++++++++-------- 5 files changed, 78 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 937ac93f4..738d50fbf 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "npx": "^10.2.2", "nyc": "^15.1.0", "prettier": "^3.0.3", + "sinon": "^18.0.0", "typedoc": "^0.25.1", "typescript": "^5.1.6", "vite": "^4.4.9", diff --git a/src/grpc/GrpcServiceError.js b/src/grpc/GrpcServiceError.js index 8ef02c71e..5a80b0197 100644 --- a/src/grpc/GrpcServiceError.js +++ b/src/grpc/GrpcServiceError.js @@ -33,7 +33,9 @@ export default class GrpcServiceError extends Error { * @param {GrpcStatus} status */ constructor(status) { - super(`gRPC service failed with status: ${status.toString()}`); + super( + `gRPC service failed with: Status: ${status.toString()}, Code: ${status.valueOf()}`, + ); /** * @readonly @@ -55,7 +57,9 @@ export default class GrpcServiceError extends Error { if (obj.code != null && obj.details != null) { const status = GrpcStatus._fromValue(obj.code); const err = new GrpcServiceError(status); - err.stack += `\nCaused by: ${obj.stack ? obj.stack.toString() : ""}`; + err.stack += `\nCaused by: ${ + obj.stack ? obj.stack.toString() : "" + }`; err.message += `: ${obj.details}`; return err; } else { diff --git a/src/grpc/GrpcStatus.js b/src/grpc/GrpcStatus.js index 21617733d..4f398f0df 100644 --- a/src/grpc/GrpcStatus.js +++ b/src/grpc/GrpcStatus.js @@ -89,43 +89,43 @@ export default class GrpcStatus { toString() { switch (this) { case GrpcStatus.Ok: - return `OK (${this._code})`; + return "OK"; case GrpcStatus.Cancelled: - return `CANCELLED (${this._code})`; + return "CANCELLED"; case GrpcStatus.Unknown: - return `UNKNOWN (${this._code})`; + return "UNKNOWN"; case GrpcStatus.InvalidArgument: - return `INVALID_ARGUMENT (${this._code})`; + return "INVALID_ARGUMENT"; case GrpcStatus.DeadlineExceeded: - return `DEADLINE_EXCEEDED (${this._code})`; + return "DEADLINE_EXCEEDED"; case GrpcStatus.NotFound: - return `NOT_FOUND (${this._code})`; + return "NOT_FOUND"; case GrpcStatus.AlreadyExists: - return `ALREADY_EXISTS (${this._code})`; + return "ALREADY_EXISTS"; case GrpcStatus.PermissionDenied: - return `PERMISSION_DENIED (${this._code})`; + return "PERMISSION_DENIED"; case GrpcStatus.Unauthenticated: - return `UNAUTHENTICATED (${this._code})`; + return "UNAUTHENTICATED"; case GrpcStatus.ResourceExhausted: - return `RESOURCE_EXHAUSTED (${this._code})`; + return "RESOURCE_EXHAUSTED"; case GrpcStatus.FailedPrecondition: - return `FAILED_PRECONDITION (${this._code})`; + return "FAILED_PRECONDITION"; case GrpcStatus.Aborted: - return `ABORTED (${this._code})`; + return "ABORTED"; case GrpcStatus.OutOfRange: - return `OUT_OF_RANGE (${this._code})`; + return "OUT_OF_RANGE"; case GrpcStatus.Unimplemented: - return `UNIMPLEMENTED (${this._code})`; + return "UNIMPLEMENTED"; case GrpcStatus.Internal: - return `INTERNAL (${this._code})`; + return "INTERNAL"; case GrpcStatus.Unavailable: - return `UNAVAILABLE (${this._code})`; + return "UNAVAILABLE"; case GrpcStatus.DataLoss: - return `DATA_LOSS (${this._code})`; + return "DATA_LOSS"; case GrpcStatus.Timeout: - return `TIMEOUT (${this._code})`; + return "TIMEOUT"; case GrpcStatus.GrpcWeb: - return `GRPC_WEB (${this._code})`; + return "GRPC_WEB"; default: return `UNKNOWN (${this._code})`; } diff --git a/test/unit/FileAppendMocking.js b/test/unit/FileAppendMocking.js index fbaab296c..e0c55883a 100644 --- a/test/unit/FileAppendMocking.js +++ b/test/unit/FileAppendMocking.js @@ -168,7 +168,7 @@ describe("FileAppendMocking", function () { } catch (error) { if ( error.message !== - "max attempts of 1 was reached for request with last error being: GrpcServiceError: gRPC service failed with status: UNAVAILABLE (14): node is UNAVAILABLE" + "max attempts of 1 was reached for request with last error being: GrpcServiceError: gRPC service failed with: Status: UNAVAILABLE, Code: 14: node is UNAVAILABLE" ) { throw error; } diff --git a/test/unit/LoggerTest.js b/test/unit/LoggerTest.js index ee1aedbff..c96eacb60 100644 --- a/test/unit/LoggerTest.js +++ b/test/unit/LoggerTest.js @@ -2,6 +2,7 @@ import { Logger, LogLevel, Transaction } from "../../src/exports.js"; import { Client } from "../../src/index.js"; import { tmpdir } from "node:os"; import fs from "fs"; +import { spy } from "sinon"; describe("Logger", function () { this.timeout(50000); @@ -71,22 +72,58 @@ describe("Logger", function () { expect(levels).to.include("fatal"); }); - it("check that it can write to a log file", async function () { - const logFile = tmpdir() + "/test.log"; - const logger = new Logger(LogLevel.Info, logFile); - logger.info("This is a test log message"); - logger.warn("This is a test warning message"); - logger.error("This is a test error message"); + it("check that it can write to a log file", function () { + const logFile = `${tmpdir()}/test.log`; + fs.rmSync(logFile); + const logger = new Logger(LogLevel.Trace, logFile); + let assertionCount = 0; + for (const level of Object.values(LogLevel)) { + if (level === LogLevel.Silent) continue; + logger[level](`This is a test ${level} message`); + + const logContent = fs.readFileSync(logFile, "utf8"); + expect(logContent).to.contain(`This is a test ${level} message`); + expect(logContent).to.contain( + level.toString().toUpperCase(), + `should contain ${level.toString().toUpperCase()}`, + ); + assertionCount += 2; + } + expect(assertionCount).to.be.equal( + 12, + "should have made 12 assertions", + ); + }); + + it("check that it can write to stdout", function () { + let assertionCount = 0; + const logger = new Logger(LogLevel.Trace); + for (const level of Object.values(LogLevel)) { + if (level === LogLevel.Silent) continue; + const loggerLogSpy = spy(logger._logger, level); + logger[level](`This is a test ${level} message`); + expect(loggerLogSpy.calledWith(`This is a test ${level} message`)) + .to.be.true; + assertionCount++; + } + expect(assertionCount).to.be.equal(6, "should have made 6 assertions"); + }); + + it("check that silent blocks output", function () { + const logFile = `${tmpdir()}/test2.log`; + fs.rmSync(logFile); + const logger = new Logger(LogLevel.Trace, logFile); + expect(logger.silent).to.be.equal(false); + logger.warn("This is a test warn message"); + logger.setSilent(true); + expect(logger.silent).to.be.equal(true); logger.fatal("This is a test fatal message"); - // read the log file and check if the messages are there and check that the levels are correct + logger.setSilent(false); + logger.error("This is a test error message"); const logContent = fs.readFileSync(logFile, "utf8"); - expect(logContent).to.contain("This is a test log message"); - expect(logContent).to.contain(LogLevel.Info.toString().toUpperCase()); - expect(logContent).to.contain("This is a test warning message"); - expect(logContent).to.contain(LogLevel.Warn.toString().toUpperCase()); + expect(logger.silent).to.be.equal(false); + expect(logContent).to.contain("This is a test warn message"); expect(logContent).to.contain("This is a test error message"); - expect(logContent).to.contain(LogLevel.Error.toString().toUpperCase()); - expect(logContent).to.contain("This is a test fatal message"); - expect(logContent).to.contain(LogLevel.Fatal.toString().toUpperCase()); + expect(logContent).to.not.contain("This is a test fatal message"); }); }); From 9ac414afa4de50a56a9801cfb7d51ea0362934f5 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 29 May 2024 19:36:19 +0100 Subject: [PATCH 7/9] don't error if the tmp dir log file does not exist Signed-off-by: Jeromy Cannon --- test/unit/LoggerTest.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/LoggerTest.js b/test/unit/LoggerTest.js index c96eacb60..a42238df7 100644 --- a/test/unit/LoggerTest.js +++ b/test/unit/LoggerTest.js @@ -74,7 +74,7 @@ describe("Logger", function () { it("check that it can write to a log file", function () { const logFile = `${tmpdir()}/test.log`; - fs.rmSync(logFile); + fs.rmSync(logFile, { force: true }); const logger = new Logger(LogLevel.Trace, logFile); let assertionCount = 0; for (const level of Object.values(LogLevel)) { @@ -111,7 +111,7 @@ describe("Logger", function () { it("check that silent blocks output", function () { const logFile = `${tmpdir()}/test2.log`; - fs.rmSync(logFile); + fs.rmSync(logFile, { force: true }); const logger = new Logger(LogLevel.Trace, logFile); expect(logger.silent).to.be.equal(false); logger.warn("This is a test warn message"); From eea9ed81c7d2037993cf9f4d3b518c843a93dbb9 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 29 May 2024 21:37:52 +0100 Subject: [PATCH 8/9] use cgroups to prevent github runner from losing communication with server Signed-off-by: Jeromy Cannon --- .github/workflows/build.yml | 64 +++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 577bf1157..142f93a01 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,9 @@ defaults: permissions: contents: read +env: + CG_EXEC: export R_UID=$(id -u); CGROUP_LOGLEVEL=DEBUG cgexec -g cpu,memory:user.slice/user-${R_UID}.slice/user@${R_UID}.service/e2e-${{ github.run_id }} --sticky ionice -c 2 -n 2 nice -n 19 + jobs: build: name: Build using Node ${{ matrix.node }} @@ -65,6 +68,47 @@ jobs: node: [ "16" ] steps: + - name: Setup Control Groups + run: | + echo "::group::Get System Configuration" + USR_ID="$(id -un)" + GRP_ID="$(id -gn)" + E2E_MEM_LIMIT="30064771072" + AGENT_MEM_LIMIT="2147483648" + USER_SLICE="user.slice/user-$(id -u).slice" + USER_SERVICE="${USER_SLICE}/user@$(id -u).service" + E2E_GROUP_NAME="${USER_SERVICE}/e2e-${{ github.run_id }}" + AGENT_GROUP_NAME="${USER_SERVICE}/agent-${{ github.run_id }}" + echo "::endgroup::" + + echo "::group::Install Control Group Tools" + if ! command -v cgcreate >/dev/null 2>&1; then + sudo apt-get update + sudo apt-get install -y cgroup-tools + fi + echo "::endgroup::" + + echo "::group::Create Control Groups" + sudo cgcreate -g cpu,memory:${USER_SLICE} -a ${USR_ID}:${GRP_ID} -t ${USR_ID}:${GRP_ID} + sudo cgcreate -g cpu,memory:${USER_SERVICE} -a ${USR_ID}:${GRP_ID} -t ${USR_ID}:${GRP_ID} + sudo cgcreate -g cpu,memory:${E2E_GROUP_NAME} -a ${USR_ID}:${GRP_ID} -t ${USR_ID}:${GRP_ID} + sudo cgcreate -g cpu,memory:${AGENT_GROUP_NAME} -a ${USR_ID}:${GRP_ID} -t ${USR_ID}:${GRP_ID} + echo "::endgroup::" + + echo "::group::Set Control Group Limits" + cgset -r cpu.weight=768 ${E2E_GROUP_NAME} + cgset -r cpu.weight=500 ${AGENT_GROUP_NAME} + cgset -r memory.max=${E2E_MEM_LIMIT} ${E2E_GROUP_NAME} + cgset -r memory.max=${AGENT_MEM_LIMIT} ${AGENT_GROUP_NAME} + cgset -r memory.swap.max=${E2E_MEM_LIMIT} ${E2E_GROUP_NAME} + cgset -r memory.swap.max=${AGENT_MEM_LIMIT} ${AGENT_GROUP_NAME} + echo "::endgroup::" + + echo "::group::Move Runner Processes to Control Groups" + sudo cgclassify --sticky -g cpu,memory:${AGENT_GROUP_NAME} $(pgrep 'Runner.Listener' | tr '\n' ' ') + sudo cgclassify -g cpu,memory:${AGENT_GROUP_NAME} $(pgrep 'Runner.Worker' | tr '\n' ' ') + echo "::endgroup::" + - name: Harden Runner uses: step-security/harden-runner@f086349bfa2bd1361f7909c78558e816508cdc10 # v2.8.0 with: @@ -101,49 +145,49 @@ jobs: - name: Build @hashgraph/sdk id: build-sdk - run: task build + run: ${{ env.CG_EXEC }} task build - name: Install Playwright Dependencies id: playwright-deps if: ${{ steps.build-sdk.conclusion == 'success' && !cancelled() && always() }} - run: sudo npx playwright install-deps + run: ${{ env.CG_EXEC }} sudo npx playwright install-deps - name: Start the local node id: start-local-node if: ${{ steps.build-sdk.conclusion == 'success' && !cancelled() && always() }} run: | - npx @hashgraph/hedera-local start -d --network-tag=0.49.7 --balance=100000 + ${{ env.CG_EXEC }} npx @hashgraph/hedera-local start -d --network-tag=0.49.7 --balance=100000 # Wait for the network to fully start sleep 30 - name: Run Hedera SDK Integration Tests Codecov if: ${{ steps.build-sdk.conclusion == 'success' && steps.start-local-node.conclusion == 'success' && !cancelled() && always() }} - run: task test:integration:codecov + run: ${{ env.CG_EXEC }} task test:integration:codecov - name: Stop the local node id: stop-local-node if: ${{ steps.start-local-node.conclusion == 'success' && !cancelled() && always() }} - run: npx @hashgraph/hedera-local stop + run: ${{ env.CG_EXEC }} npx @hashgraph/hedera-local stop - name: Build @hashgraph/cryptography working-directory: packages/cryptography if: ${{ steps.build-sdk.conclusion == 'success' && steps.stop-local-node.conclusion == 'success' && !cancelled() && always() }} - run: task build + run: ${{ env.CG_EXEC }} task build - name: Unit Test @hashgraph/cryptography working-directory: packages/cryptography if: ${{ steps.build-sdk.conclusion == 'success' && steps.stop-local-node.conclusion == 'success' && !cancelled() && always() }} - run: task test:unit + run: ${{ env.CG_EXEC }} task test:unit - name: Codecov @hashgraph/cryptography working-directory: packages/cryptography if: ${{ steps.build-sdk.conclusion == 'success' && steps.stop-local-node.conclusion == 'success' && !cancelled() && always() }} - run: task test:unit:codecov + run: ${{ env.CG_EXEC }} task test:unit:codecov - name: Unit Test @hashgraph/sdk if: ${{ steps.build-sdk.conclusion == 'success' && steps.stop-local-node.conclusion == 'success' && steps.playwright-deps.conclusion == 'success' && !cancelled() && always() }} - run: task test:unit + run: ${{ env.CG_EXEC }} task test:unit - name: Codecov @hashgraph/sdk if: ${{ steps.build-sdk.conclusion == 'success' && steps.stop-local-node.conclusion == 'success' && !cancelled() && always() }} - run: task test:unit:codecov + run: ${{ env.CG_EXEC }} task test:unit:codecov From 1223cd616b8dae55f24070ba1357d68dfecf5217 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 29 May 2024 21:46:13 +0100 Subject: [PATCH 9/9] fix syntax error Signed-off-by: Jeromy Cannon --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 142f93a01..e5df90329 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,18 +145,18 @@ jobs: - name: Build @hashgraph/sdk id: build-sdk - run: ${{ env.CG_EXEC }} task build + run: task build - name: Install Playwright Dependencies id: playwright-deps if: ${{ steps.build-sdk.conclusion == 'success' && !cancelled() && always() }} - run: ${{ env.CG_EXEC }} sudo npx playwright install-deps + run: sudo npx playwright install-deps - name: Start the local node id: start-local-node if: ${{ steps.build-sdk.conclusion == 'success' && !cancelled() && always() }} run: | - ${{ env.CG_EXEC }} npx @hashgraph/hedera-local start -d --network-tag=0.49.7 --balance=100000 + ${{ env.CG_EXEC }} npx @hashgraph/hedera-local start -d --network-tag=0.49.7 --balance=100000 # Wait for the network to fully start sleep 30