/** @module containers */
/** @hidden */
const process = require('process');
import { BadRequestException } from 'pip-services3-commons-nodex';
import { ConfigParams } from 'pip-services3-commons-nodex';
import { DependencyResolver } from 'pip-services3-commons-nodex';
import { Descriptor } from 'pip-services3-commons-nodex';
import { IReferences } from 'pip-services3-commons-nodex';
import { Schema } from 'pip-services3-commons-nodex';
import { UnknownException } from 'pip-services3-commons-nodex';
import { Container } from 'pip-services3-container-nodex';
import { CompositeCounters } from 'pip-services3-components-nodex';
import { ConsoleLogger } from 'pip-services3-components-nodex';
import { CompositeTracer } from 'pip-services3-components-nodex';
import { InstrumentTiming } from 'pip-services3-rpc-nodex';
import { ILambdaService } from '../services/ILambdaService';
/**
* Abstract AWS Lambda function, that acts as a container to instantiate and run components
* and expose them via external entry point.
*
* When handling calls "cmd" parameter determines which what action shall be called, while
* other parameters are passed to the action itself.
*
* Container configuration for this Lambda function is stored in "./config/config.yml" file.
* But this path can be overriden by CONFIG_PATH environment variable.
*
* ### References ###
*
* - \*:logger:\*:\*:1.0 (optional) [[https://pip-services3-nodex.github.io/pip-services3-components-nodex/interfaces/log.ilogger.html ILogger]] components to pass log messages
* - \*:counters:\*:\*:1.0 (optional) [[https://pip-services3-nodex.github.io/pip-services3-components-nodex/interfaces/count.icounters.html ICounters]] components to pass collected measurements
* - \*:service:awslambda:\*:1.0 (optional) [[https://pip-services3-nodex.github.io/pip-services3-aws-nodex/interfaces/services.ilambdaservice.html ILambdaService]] services to handle action requests
* - \*:service:commandable-awslambda:\*:1.0 (optional) [[https://pip-services3-nodex.github.io/pip-services3-aws-nodex/interfaces/services.ilambdaservice.html ILambdaService]] services to handle action requests
*
* @see [[LambdaClient]]
*
* ### Example ###
*
* class MyLambdaFunction extends LambdaFunction {
* public constructor() {
* base("mygroup", "MyGroup lambda function");
* }
* }
*
* let lambda = new MyLambdaFunction();
*
* await service.run();
* console.log("MyLambdaFunction is started");
*/
export abstract class LambdaFunction extends Container {
/**
* The performanc counters.
*/
protected _counters = new CompositeCounters();
/**
* The tracer.
*/
protected _tracer: CompositeTracer = new CompositeTracer();
/**
* The dependency resolver.
*/
protected _dependencyResolver = new DependencyResolver();
/**
* The map of registred validation schemas.
*/
protected _schemas: { [id: string]: Schema } = {};
/**
* The map of registered actions.
*/
protected _actions: { [id: string]: any } = {};
/**
* The default path to config file.
*/
protected _configPath: string = './config/config.yml';
/**
* Creates a new instance of this lambda function.
*
* @param name (optional) a container name (accessible via ContextInfo)
* @param description (optional) a container description (accessible via ContextInfo)
*/
public constructor(name?: string, description?: string) {
super(name, description);
this._logger = new ConsoleLogger();
this._dependencyResolver
}
private getConfigPath(): string {
return process.env.CONFIG_PATH || this._configPath;
}
private getParameters(): ConfigParams {
return ConfigParams.fromValue(process.env);
}
private captureErrors(correlationId: string): void {
// Log uncaught exceptions
process.on('uncaughtException', (ex) => {
this._logger.fatal(correlationId, ex, "Process is terminated");
process.exit(1);
});
}
private captureExit(correlationId: string): void {
this._logger.info(correlationId, "Press Control-C to stop the microservice...");
// Activate graceful exit
process.on('SIGINT', () => {
process.exit();
});
// Gracefully shutdown
process.on('exit', () => {
this.close(correlationId);
this._logger.info(correlationId, "Goodbye!");
});
}
/**
* Sets references to dependent components.
*
* @param references references to locate the component dependencies.
*/
public setReferences(references: IReferences): void {
super.setReferences(references);
this._counters.setReferences(references);
this._dependencyResolver.setReferences(references);
this.register();
}
/**
* Opens the component.
*
* @param correlationId (optional) transaction id to trace execution through call chain.
*/
public async open(correlationId: string): Promise {
if (this.isOpen()) return;
await super.open(correlationId);
this.registerServices();
}
/**
* Adds instrumentation to log calls and measure call time.
* It returns a InstrumentTiming object that is used to end the time measurement.
*
* Note: This method has been deprecated. Use LambdaService instead.
*
* @param correlationId (optional) transaction id to trace execution through call chain.
* @param name a method name.
* @returns {InstrumentTiming} object to end the time measurement.
*/
protected instrument(correlationId: string, name: string): InstrumentTiming {
this._logger.trace(correlationId, "Executing %s method", name);
this._counters.incrementOne(name + ".exec_count");
let counterTiming = this._counters.beginTiming(name + ".exec_time");
let traceTiming = this._tracer.beginTrace(correlationId, name, null);
return new InstrumentTiming(correlationId, name, "exec",
this._logger, this._counters, counterTiming, traceTiming);
}
/**
* Runs this lambda function, loads container configuration,
* instantiate components and manage their lifecycle,
* makes this function ready to access action calls.
*
*/
public async run(): Promise {
let correlationId = this._info.name;
let path = this.getConfigPath();
let parameters = this.getParameters();
this.readConfigFromFile(correlationId, path, parameters);
this.captureErrors(correlationId);
this.captureExit(correlationId);
await this.open(correlationId);
}
/**
* Registers all actions in this lambda function.
*
* Note: Overloading of this method has been deprecated. Use LambdaService instead.
*/
protected register(): void {}
/**
* Registers all lambda services in the container.
*/
protected registerServices(): void {
// Extract regular and commandable Lambda services from references
let services = this._references.getOptional(
new Descriptor("*", "service", "awslambda", "*", "*")
);
let cmdServices = this._references.getOptional(
new Descriptor("*", "service", "commandable-awslambda", "*", "*")
);
services.push(...cmdServices);
// Register actions defined in those services
for (let service of services) {
// Check if the service implements required interface
if (typeof service.getActions !== "function") continue;
let actions = service.getActions();
for (let action of actions) {
this.registerAction(action.cmd, action.schema, action.action);
}
}
}
/**
* Registers an action in this lambda function.
*
* Note: This method has been deprecated. Use LambdaService instead.
*
* @param cmd a action/command name.
* @param schema a validation schema to validate received parameters.
* @param action an action function that is called when action is invoked.
*/
protected registerAction(cmd: string, schema: Schema,
action: (params: any) => Promise): void {
if (cmd == '') {
throw new UnknownException(null, 'NO_COMMAND', 'Missing command');
}
if (action == null) {
throw new UnknownException(null, 'NO_ACTION', 'Missing action');
}
if (typeof action != "function") {
throw new UnknownException(null, 'ACTION_NOT_FUNCTION', 'Action is not a function');
}
if (this._actions.hasOwnProperty(cmd)) {
throw new UnknownException(null, 'DUPLICATED_ACTION', `"${cmd}" action already exists`);
}
// Hack!!! Wrapping action to preserve prototyping context
const actionCurl = (params) => {
// Perform validation
if (schema != null) {
let correlationId = params.correlaton_id;
let err = schema.validateAndReturnException(correlationId, params, false);
if (err != null) {
throw err;
}
}
// Todo: perform verification?
return action.call(this, params);
};
this._actions[cmd] = actionCurl;
}
/**
* Executes this AWS Lambda function and returns the result.
* This method can be overloaded in child classes
* if they need to change the default behavior
*
* @params event the event parameters (or function arguments)
* @returns the result of the function execution.
*/
protected async execute(event: any): Promise {
let cmd: string = event.cmd;
let correlationId = event.correlation_id;
if (cmd == null) {
throw new BadRequestException(
correlationId,
'NO_COMMAND',
'Cmd parameter is missing'
);
}
const action: any = this._actions[cmd];
if (action == null) {
throw new BadRequestException(
correlationId,
'NO_ACTION',
'Action ' + cmd + ' was not found'
)
.withDetails('command', cmd);
}
return action(event);
}
private async handler(event: any): Promise {
// If already started then execute
if (this.isOpen()) {
return this.execute(event);
}
// Start before execute
await this.run();
return this.execute(event);
}
/**
* Gets entry point into this lambda function.
*
* @param event an incoming event object with invocation parameters.
*/
public getHandler(): (event: any) => Promise {
let self = this;
// Return plugin function
return async function (event) {
// Calling run with changed context
return self.handler.call(self, event);
}
}
/**
* Calls registered action in this lambda function.
* "cmd" parameter in the action parameters determin
* what action shall be called.
*
* This method shall only be used in testing.
*
* @param params action parameters.
*/
public async act(params: any): Promise {
return this.getHandler()(params);
}
}