diff --git a/src/backend/src/CoreModule.js b/src/backend/src/CoreModule.js index 94b1938f5..b4e2a31e9 100644 --- a/src/backend/src/CoreModule.js +++ b/src/backend/src/CoreModule.js @@ -86,9 +86,7 @@ const install = async ({ services, app, useapi, modapi }) => { // call to services.registerService. We'll clean this up // in a future PR. - const { PagerService } = require('./services/runtime-analysis/PagerService'); const { CommandService } = require('./services/CommandService'); - const { ExpectationService } = require('./services/runtime-analysis/ExpectationService'); const { HTTPThumbnailService } = require('./services/thumbnails/HTTPThumbnailService'); const { PureJSThumbnailService } = require('./services/thumbnails/PureJSThumbnailService'); const { NAPIThumbnailService } = require('./services/thumbnails/NAPIThumbnailService'); @@ -141,8 +139,6 @@ const install = async ({ services, app, useapi, modapi }) => { services.registerService('__api-filesystem', FilesystemAPIService); services.registerService('__api', PuterAPIService); services.registerService('__gui', ServeGUIService); - services.registerService('expectations', ExpectationService); - services.registerService('pager', PagerService); services.registerService('registry', RegistryService); services.registerService('__registrant', RegistrantService); services.registerService('fslock', FSLockService); @@ -352,7 +348,6 @@ const install = async ({ services, app, useapi, modapi }) => { } const install_legacy = async ({ services }) => { - const { ProcessEventService } = require('./services/runtime-analysis/ProcessEventService'); // const { FilesystemService } = require('./filesystem/FilesystemService'); const PerformanceMonitor = require('./monitor/PerformanceMonitor'); const { OperationTraceService } = require('./services/OperationTraceService'); @@ -362,7 +357,6 @@ const install_legacy = async ({ services }) => { const { FileCacheService } = require('./services/file-cache/FileCacheService'); // === Services which do not yet extend BaseService === - services.registerService('process-event', ProcessEventService); // services.registerService('filesystem', FilesystemService); services.registerService('operationTrace', OperationTraceService); services.registerService('file-cache', FileCacheService); diff --git a/src/backend/src/filesystem/batch/BatchExecutor.js b/src/backend/src/filesystem/batch/BatchExecutor.js index a0ed2ea5e..062766a6d 100644 --- a/src/backend/src/filesystem/batch/BatchExecutor.js +++ b/src/backend/src/filesystem/batch/BatchExecutor.js @@ -19,11 +19,11 @@ const { AdvancedBase } = require('@heyputer/putility'); const PathResolver = require('../../routers/filesystem_api/batch/PathResolver'); const commands = require('./commands').commands; -const { WorkUnit } = require('../../services/runtime-analysis/ExpectationService'); const APIError = require('../../api/APIError'); const { Context } = require('../../util/context'); const config = require('../../config'); const { TeePromise } = require('../../util/promise'); +const { WorkUnit } = require('../../modules/core/lib/expect'); class BatchExecutor extends AdvancedBase { constructor (x, { actor, log, errors }) { diff --git a/src/backend/src/modules/core/Core2Module.js b/src/backend/src/modules/core/Core2Module.js index dc1a1f1d6..458f2b6a0 100644 --- a/src/backend/src/modules/core/Core2Module.js +++ b/src/backend/src/modules/core/Core2Module.js @@ -27,6 +27,15 @@ class Core2Module extends AdvancedBase { const { ErrorService } = require("./ErrorService.js"); services.registerService('error-service', ErrorService); + + const { PagerService } = require("./PagerService.js"); + services.registerService('pager', PagerService); + + const { ExpectationService } = require("./ExpectationService.js"); + services.registerService('expectations', ExpectationService); + + const { ProcessEventService } = require("./ProcessEventService.js"); + services.registerService('process-event', ProcessEventService); } } diff --git a/src/backend/src/services/runtime-analysis/ExpectationService.js b/src/backend/src/modules/core/ExpectationService.js similarity index 64% rename from src/backend/src/services/runtime-analysis/ExpectationService.js rename to src/backend/src/modules/core/ExpectationService.js index 28e58d427..99731fa08 100644 --- a/src/backend/src/services/runtime-analysis/ExpectationService.js +++ b/src/backend/src/modules/core/ExpectationService.js @@ -18,82 +18,8 @@ * along with this program. If not, see . */ const { v4: uuidv4 } = require('uuid'); -const { quot } = require('../../util/strutil'); -const BaseService = require('../BaseService'); +const BaseService = require('../../services/BaseService'); - -/** -* @class WorkUnit -* @description The WorkUnit class represents a unit of work that can be tracked and monitored for checkpoints. -* It includes methods to create instances, set checkpoints, and manage the state of the work unit. -*/ -class WorkUnit { - /** - * Represents a unit of work with checkpointing capabilities. - * - * @class - */ - - /** - * Creates and returns a new instance of WorkUnit. - * - * @static - * @returns {WorkUnit} A new instance of WorkUnit. - */ - static create () { - return new WorkUnit(); - } - /** - * Creates a new instance of the WorkUnit class. - * @static - * @returns {WorkUnit} A new WorkUnit instance. - */ - constructor () { - this.id = uuidv4(); - this.checkpoint_ = null; - } - checkpoint (label) { - console.log('CHECKPOINT', label); - this.checkpoint_ = label; - } -} - - -/** -* @class CheckpointExpectation -* @classdesc The CheckpointExpectation class is used to represent an expectation that a specific checkpoint -* will be reached during the execution of a work unit. It includes methods to check if the checkpoint has -* been reached and to report the results of this check. -*/ -class CheckpointExpectation { - constructor (workUnit, checkpoint) { - this.workUnit = workUnit; - this.checkpoint = checkpoint; - } - /** - * Constructor for CheckpointExpectation class. - * Initializes the instance with a WorkUnit and a checkpoint label. - * @param {WorkUnit} workUnit - The work unit associated with the checkpoint. - * @param {string} checkpoint - The checkpoint label to be checked. - */ - check () { - // TODO: should be true if checkpoint was ever reached - return this.workUnit.checkpoint_ == this.checkpoint; - } - report (log) { - if ( this.check() ) return; - log.log( - `operation(${this.workUnit.id}): ` + - `expected ${quot(this.checkpoint)} ` + - `and got ${quot(this.workUnit.checkpoint_)}.` - ); - } -} - -/** - * This service helps diagnose errors involving the potentially - * complex relationships between asynchronous operations. - */ /** * @class ExpectationService * @extends BaseService @@ -108,6 +34,10 @@ class CheckpointExpectation { * runtime behaviors in a system. */ class ExpectationService extends BaseService { + static USE = { + expect: 'core.expect' + }; + /** * Constructs the ExpectationService and initializes its internal state. * This method is intended to be called asynchronously. @@ -119,6 +49,29 @@ class ExpectationService extends BaseService { this.expectations_ = []; } + /** + * ExpectationService registers its commands at the consolidation phase because + * the '_init' method of CommandService may not have been called yet. + */ + ['__on_boot.consolidation'] () { + const commands = this.services.get('commands'); + commands.registerCommands('expectations', [ + { + id: 'pending', + description: 'lists pending expectations', + handler: async (args, log) => { + this.purgeExpectations_(); + if ( this.expectations_.length < 1 ) { + log.log(`there are none`); + return; + } + for ( const expectation of this.expectations_ ) { + expectation.report(log); + } + } + } + ]); + } /** * Initializes the ExpectationService, setting up interval functions and registering commands. @@ -145,24 +98,6 @@ class ExpectationService extends BaseService { setInterval(() => { this.purgeExpectations_(); }, 1000); - - const commands = services.get('commands'); - commands.registerCommands('expectations', [ - { - id: 'pending', - description: 'lists pending expectations', - handler: async (args, log) => { - this.purgeExpectations_(); - if ( this.expectations_.length < 1 ) { - log.log(`there are none`); - return; - } - for ( const expectation of this.expectations_ ) { - expectation.report(log); - } - } - } - ]); } @@ -187,13 +122,12 @@ class ExpectationService extends BaseService { } expect_eventually ({ workUnit, checkpoint }) { - this.expectations_.push(new CheckpointExpectation(workUnit, checkpoint)); + this.expectations_.push(new this.expect.CheckpointExpectation(workUnit, checkpoint)); } } module.exports = { - WorkUnit, ExpectationService }; \ No newline at end of file diff --git a/src/backend/src/services/runtime-analysis/PagerService.js b/src/backend/src/modules/core/PagerService.js similarity index 91% rename from src/backend/src/services/runtime-analysis/PagerService.js rename to src/backend/src/modules/core/PagerService.js index 07d9d67d0..20d3fc3ea 100644 --- a/src/backend/src/services/runtime-analysis/PagerService.js +++ b/src/backend/src/modules/core/PagerService.js @@ -18,9 +18,8 @@ * along with this program. If not, see . */ const pdjs = require('@pagerduty/pdjs'); -const BaseService = require('../BaseService'); +const BaseService = require('../../services/BaseService'); const util = require('util'); -const { Context } = require('../../util/context'); /** @@ -33,11 +32,24 @@ const { Context } = require('../../util/context'); * command registration. */ class PagerService extends BaseService { + static USE = { + Context: 'core.context', + } + async _construct () { this.config = this.global_config.pager; this.alertHandlers_ = []; } + + /** + * PagerService registers its commands at the consolidation phase because + * the '_init' method of CommandService may not have been called yet. + */ + ['__on_boot.consolidation'] () { + this._register_commands(this.services.get('commands')); + } + /** * Initializes the PagerService instance by setting the configuration and * initializing an empty alert handler array. @@ -56,11 +68,8 @@ class PagerService extends BaseService { } this.onInit(); - - this._register_commands(services.get('commands')); } - /** * Initializes PagerDuty configuration and registers alert handlers. * If PagerDuty is enabled in the configuration, it sets up an alert handler @@ -83,7 +92,7 @@ class PagerService extends BaseService { server_id: this.global_config.server_id, }; - const ctx = Context.get(undefined, { allow_fallback: true }); + const ctx = this.Context.get(undefined, { allow_fallback: true }); // Add request payload if any exists const req = ctx.get('req'); diff --git a/src/backend/src/services/runtime-analysis/ProcessEventService.js b/src/backend/src/modules/core/ProcessEventService.js similarity index 88% rename from src/backend/src/services/runtime-analysis/ProcessEventService.js rename to src/backend/src/modules/core/ProcessEventService.js index ab53c0056..9a992b4d9 100644 --- a/src/backend/src/services/runtime-analysis/ProcessEventService.js +++ b/src/backend/src/modules/core/ProcessEventService.js @@ -17,8 +17,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -const { Context } = require("../../util/context"); +const BaseService = require("../../services/BaseService"); /** * Service class that handles process-wide events and errors. @@ -28,8 +28,13 @@ const { Context } = require("../../util/context"); * * @class ProcessEventService */ -class ProcessEventService { - constructor ({ services }) { +class ProcessEventService extends BaseService { + static USE = { + Context: 'core.context', + }; + + _init () { + const services = this.services; const log = services.get('log-service').create('process-event-service'); const errors = services.get('error-service').create(log); @@ -44,7 +49,7 @@ class ProcessEventService { * @param {string} origin - The origin of the uncaught exception * @returns {Promise} */ - await Context.allow_fallback(async () => { + await this.Context.allow_fallback(async () => { errors.report('process:uncaughtException', { source: err, origin, @@ -62,7 +67,7 @@ class ProcessEventService { * @param {Promise} promise - The rejected promise * @returns {Promise} Resolves when error is reported */ - await Context.allow_fallback(async () => { + await this.Context.allow_fallback(async () => { errors.report('process:unhandledRejection', { source: reason, promise, diff --git a/src/backend/src/modules/core/lib/__lib__.js b/src/backend/src/modules/core/lib/__lib__.js index 747fc1f0a..c5c389e06 100644 --- a/src/backend/src/modules/core/lib/__lib__.js +++ b/src/backend/src/modules/core/lib/__lib__.js @@ -5,4 +5,5 @@ module.exports = { identutil: require('./identifier.js'), stdioutil: require('./stdio.js'), }, + expect: require('./expect.js'), }; diff --git a/src/backend/src/modules/core/lib/expect.js b/src/backend/src/modules/core/lib/expect.js new file mode 100644 index 000000000..814913879 --- /dev/null +++ b/src/backend/src/modules/core/lib/expect.js @@ -0,0 +1,73 @@ +// METADATA // {"def":"core.expect"} + +/** +* @class WorkUnit +* @description The WorkUnit class represents a unit of work that can be tracked and monitored for checkpoints. +* It includes methods to create instances, set checkpoints, and manage the state of the work unit. +*/ +class WorkUnit { + /** + * Represents a unit of work with checkpointing capabilities. + * + * @class + */ + + /** + * Creates and returns a new instance of WorkUnit. + * + * @static + * @returns {WorkUnit} A new instance of WorkUnit. + */ + static create () { + return new WorkUnit(); + } + /** + * Creates a new instance of the WorkUnit class. + * @static + * @returns {WorkUnit} A new WorkUnit instance. + */ + constructor () { + this.id = uuidv4(); + this.checkpoint_ = null; + } + checkpoint (label) { + console.log('CHECKPOINT', label); + this.checkpoint_ = label; + } +} + +/** +* @class CheckpointExpectation +* @classdesc The CheckpointExpectation class is used to represent an expectation that a specific checkpoint +* will be reached during the execution of a work unit. It includes methods to check if the checkpoint has +* been reached and to report the results of this check. +*/ +class CheckpointExpectation { + constructor (workUnit, checkpoint) { + this.workUnit = workUnit; + this.checkpoint = checkpoint; + } + /** + * Constructor for CheckpointExpectation class. + * Initializes the instance with a WorkUnit and a checkpoint label. + * @param {WorkUnit} workUnit - The work unit associated with the checkpoint. + * @param {string} checkpoint - The checkpoint label to be checked. + */ + check () { + // TODO: should be true if checkpoint was ever reached + return this.workUnit.checkpoint_ == this.checkpoint; + } + report (log) { + if ( this.check() ) return; + log.log( + `operation(${this.workUnit.id}): ` + + `expected ${JSON.stringify(this.checkpoint)} ` + + `and got ${JSON.stringify(this.workUnit.checkpoint_)}.` + ); + } +} + +module.exports = { + WorkUnit, + CheckpointExpectation, +}; diff --git a/src/backend/src/routers/filesystem_api/batch/all.js b/src/backend/src/routers/filesystem_api/batch/all.js index f92d1f77d..2bcf594d6 100644 --- a/src/backend/src/routers/filesystem_api/batch/all.js +++ b/src/backend/src/routers/filesystem_api/batch/all.js @@ -20,7 +20,6 @@ const APIError = require("../../../api/APIError"); const eggspress = require("../../../api/eggspress"); const config = require("../../../config"); const PathResolver = require("./PathResolver"); -const { WorkUnit } = require("../../../services/runtime-analysis/ExpectationService"); const { Context } = require("../../../util/context"); const Busboy = require('busboy'); const { BatchExecutor } = require("../../../filesystem/batch/BatchExecutor"); diff --git a/src/backend/src/services/runtime-analysis/HeapMonService.js b/src/backend/src/services/runtime-analysis/HeapMonService.js deleted file mode 100644 index aebcbd129..000000000 --- a/src/backend/src/services/runtime-analysis/HeapMonService.js +++ /dev/null @@ -1,69 +0,0 @@ -// METADATA // {"ai-commented":{"service":"openai-completion","model":"gpt-4o"}} -/* - * Copyright (C) 2024 Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -const memwatch = require('@airbnb/node-memwatch'); - - -/** -* The HeapMonService class monitors the application's memory usage, -* utilizing the memwatch library to detect heap memory leaks and -* gather heap statistics at specified intervals. It interfaces with -* logging and alarm services to report memory conditions and -* trigger alerts as necessary. -*/ -class HeapMonService { - constructor ({ services, my_config }) { - this.log = services.get('log-service').create('heap-monitor'); - this.alarm = services.get('alarm'); - - let hd, hd_ts; - - if ( my_config.heapdiff ) { - hd = new memwatch.HeapDiff(); - hd_ts = Date.now(); - } - - let heapdiff_interval = my_config.heapdiff_interval ?? 1; - heapdiff_interval *= 1000; - - memwatch.on('stats', (stats) => { - this.log.info('stats', stats); - - (() => { - if ( ! my_config.heapdiff ) return - - const now = Date.now(); - - if ( (now - hd_ts) < heapdiff_interval ) return; - - const diff = hd.end(); - this.log.info('heapdiff', diff); - hd = new memwatch.HeapDiff(); - hd_ts = now; - })(); - }); - - memwatch.on('leak', (info) => { - this.log.error('leak', info); - this.alarm.create('heap-leak', 'memory leak detected', info); - }); - } -} - -module.exports = { HeapMonService }; \ No newline at end of file