Command Executor

The engine behind the OT-Node application.

Introduction

The Command Executor is a component of the ot-node implementation which uses an approach similar to the event sourcing pattern. Essentially it allows developers to organize functionalities (code) in "commands" which can be executed in sequence to implement the protocol features, as well as enable system recovery in case of the node stopping or restarting for some reason.

Commands

The Command Executor splits business logic into commands. A command is a general abstraction with many features that can be enabled. The Command interface is described in the table below.

Table 1.1 Command interface

Creating a command is done by extending this abstract class called simply Command, meaning it inherits the default behaviour of all the methods and can override them with specific behaviour.

The core command method is the execute method. This method executes the code of the command and returns one of the three results:

  • this.continueSequence(data,sequence,opts) - A list of commands taken from the execution context that will be executed after the current command is finished successfully.

    • Returns Command.empty() if the current command is the last one in the sequence.

  • Command.repeat() - Command object with the repeat flag set to true. That means that the command will be executed once again.

  • Command.retry() - Command object with the retry flag set to true. That means that the command will be executed again and the retried counter will be decreased.

Command data describes everything that is related to the specific command. This is described in the table below.

Table 1.2 Command data parameters

Command Executor and dependency injection

Command executor is initialized on ot-node start. Commands are stored in the */src/commands directory. Commands will be injected into Awilix automatically. The naming convention of the command is in camel case and the name of the file where the command is described by using slashes(kebab case). In the following chapter we will create a simple command called PublishStartedCommand.

PublishStartedCommand

Let’s create a simple PublishStartedCommand and call it inhandleHttpApiPublishRequest controller method that handles the asset publishing request.

publish-started-command.js
import Command from "./command.js";

class PublishStartedCommand extends Command {
    constructor(ctx){
        super(ctx);
}

    async execute(command){
        const {name} = command.data;
        
        this.logger.info(`Hello from ${name}`);
        return this.continueSequence(
            { ...command.data, retry: undefined, period: undefined},
            command.sequence,
            );
    }
    
    default(map) {
        const command = {
            name: 'publishStartedCommand',
            transactional: false
        }
        Object.assign(command,map);
        return command;
    }
}
export default PublishStartedCommand;

PublishStartedCommand will be called before the passed assertion is validated and propagated to the network.

async handleHttpApiPublishRequest(req, res) {
    try{
        /**
        Code responsible for creating operation record and notifying the client
        that the operation started is intentionaly left out for simplicity reasons.
        
        You can see the whole implementaton here:
        https://github.com/OriginTrail/ot-node/blob/1181ec828bba51aa7f115e30c48458352199c67b/src/controller/v1/publish-controller.js#L18
 
        **/
        const commandData = {
            ...req.body,
            name: 'publishController',
            operationId,
        };
        
        //adding the publishStartedCommand to the command sequence
        const commandSequence = [
         'publishStartedCommand',
         'validateAssertionCommand',
         'networkPublishCommand'];

        await this.commandExecutor.add({
            name: commandSequence[0],
            sequence: commandSequence.slice(1),
            delay: 0,
            period: 5000,
            retries: 3,
            data: commandData,
            transactional: false,
        });
    } catch (error) {
        /**
        ... error handling
        /**
    }
}

This is the simplest command that logs the name given in the data parameter. After this command is finished, command executor continues with the next command in the sequence, in this case it continues with the execution of validateAssertionCommand and networkPublishCommand.

If we want to return some new command or list of commands, the return statement will look like in this

return {
    commands: [
        {
            name: 'someCommand',
            data: {
                param: 'value'
            },
            transactional: false
        }
    ]
}

In order to make this new command repetitive and add a delay for example, we would add these parameters to the command. The code snippet will look like in this

return {
    commands: [
        {
            name: 'someCommand',
            data: {
                param: 'value'
            },
            delay: 10000,
            period: 5000,
            transactional: false
        }
    ]
}

Command Executor API

Command Executor API is simple and it looks like this:

/**
* Initialize executor
* @returns {Promise<void>}
*/
async init() {...}

/**
* Starts the command executor
* @return {Promise<void>}
*/
async start() {...}

/**
* Adds single command to queue
* @param addCommand
* @param addDelay
* @param insert
*/
async add(addCommand, addDelay = 0, insert = true) {...}

/**
* Replays pending commands from the database
* @returns {Promise<void>}
*/
async replay() {...}

One of the key methods of the API is add() which is responsible for adding new commands to the array of commands for command-executor to carry out. Such a call would look like this:

await this.commandExecutor.add({
    name: 'nameOfTheCommand',
    delay:45000,
    data,
    transactional: false
})

The start() command starts the executor and all the repetitive commands listed in the constants.PERMANENT_COMMANDS. Those commands will be executed permanently throughout the node execution.

Need help with command executor?

Jump into our Discord and someone from the OriginTrail Community of developers will gladly help!

Last updated