Process container

How to create a ProcessContainer.

Key takeaways

ProcessContainer Container that can be used to run contained code as a system process.
Custom component Custom component created for the tutorial's example.
Factory Component used to create the contained components.
Configuration file File used by the container to locate components.

Introduction

This tutorial will help you understand how to create a ProcessContainer that manages the life cycle of a custom component. First, we will see a brief description of the component and how to import it. Then, we will create a custom component, a factory to create it, and a process container. Finally, we will run the container and summarize all the learned concepts.

The ProcessContainer container

Docker defines a container as a standard unit of software that packages up code and all its dependencies so the application runs quickly and reliably from one computing environment to another.

Similarly, Pip.Services offers the ProcessContainer, which allows us to run our contained code as a system process.

In this tutorial, we will create a ProcessContainer component that packages a custom component, run it, and check the results.

Pre-requisites

In order to use the ProcessContainer component, we need to import it first. This can be done with the following command:

import { ProcessContainer } from 'pip-services4-container-node';
Not available
import ccont "github.com/pip-services4/pip-services4-go/pip-services4-container-go/container"
Not available
from pip_services4_container import ProcessContainer
Not available

Creating a component

Now, we create a component that will operate in a ProcessContainer. In our example, this is a mock component, that only prints a message once an operation is called. Moreover, to see how the container manages the lifecycle of this component, we want this class to be openable and configurable. An example of what this class looks like is:

import { 
    ConfigParams, IConfigurable, IOpenable, Context, IReferences, 
    Parameters, IReferenceable, IUnreferenceable, IExecutable 
} from "pip-services4-components-node";

export class MyComponentA implements IReferenceable, IUnreferenceable, IConfigurable, IOpenable, IExecutable {

    private _open = false;

    public constructor() {
        console.log("MyComponentA has been created.");
    }

    public configure(config: ConfigParams): void {
        console.log("MyComponentA has been configured.");
    }

    public setReferences(references: IReferences): void {
        console.log("MyComponentA's references have been defined.");
    }

    public unsetReferences(): void {
        console.log("References cleared");
    }

    public isOpen(): boolean {
        return this._open;
    }

    public async open(ctx: Context): Promise<void> {
        console.log("MyComponentA has been opened.");
    }

    public async close(ctx: Context): Promise<void> {
        console.log("MyComponentA has been closed.");
    }

    public async execute(ctx: Context, args: Parameters): Promise<any> {
        console.log("Executing");
        let result = args;
        return result;
    }
}
Not available
import (
	"context"
	"fmt"

	cconf "github.com/pip-services4/pip-services4-go/pip-services4-components-go/config"
	cexec "github.com/pip-services4/pip-services4-go/pip-services4-components-go/exec"
	crefer "github.com/pip-services4/pip-services4-go/pip-services4-components-go/refer"
)

type MyComponentA struct {
	opened bool
}

func NewMyComponentA() *MyComponentA {
	defer fmt.Println("MyComponentA has been created.")
	c := &MyComponentA{}
	return c
}

func (c *MyComponentA) Configure(ctx context.Context, config *cconf.ConfigParams) {
	fmt.Println("MyComponentA has been configured.")
}

func (c *MyComponentA) SetReferences(ctx context.Context, references crefer.IReferences) {
	fmt.Println("MyComponentA's references have been defined.")
}

func (c *MyComponentA) UnsetReferences() {
	fmt.Println("References cleared")
}

func (c *MyComponentA) IsOpen() bool {
	return c.opened
}

func (c *MyComponentA) Open(ctx context.Context, correlationId string) error {
	fmt.Println("MyComponentA has been opened.")
	return nil
}

func (c *MyComponentA) Close(ctx context.Context, correlationId string) error {
	fmt.Println("MyComponentA has been closed.")
	return nil
}

func (c *MyComponentA) Execute(ctx context.Context, correlationId string, args cexec.Parameters) (result any, err error) {
	fmt.Println("Executing")
	result = args
	return result, nil
}
Not available
from pip_services4_components.refer import IReferenceable, IReferences, IUnreferenceable
from pip_services4_components.run import IOpenable
from pip_services4_components.exec import IExecutable
from pip_services4_components.config import IConfigurable

class MyComponentA(IReferenceable, IUnreferenceable, IConfigurable, IOpenable, IExecutable):
    
    def __init__(self):
        print("MyComponentA has been created.")
            
    def configure(self, config):
        print("MyComponentA has been configured.")
       
    def is_open(self):
        return self._open

    def open(self, context):
        print("MyComponentA has been opened.")
    def close(self, context):
        print("MyComponentA has been closed.")
        
    def my_task(self, context):
        print("Doing my business task")
        dummy_variable = "dummy value"
    
    def set_references(self, references):
        print("MyComponentA's references have been defined.")    
        
    def unset_references(self):
        print("References cleared")
        
    def execute(self, context: str, args: str): 
        print('Executing')
        result = args
        return result

Not available

Creating a factory

The next step is to create a factory for our component. For this, we create a class that extends the Factory class and registers our previously defined custom component. The following code shows how to do this:

import { Descriptor, Factory } from "pip-services4-components-node";

export class MyFactory extends Factory {
    public constructor() {
        super();

        this.registerAsType(new Descriptor("myservice", "MyComponentA", "*", "*", "1.0"), MyComponentA);
    }
}
Not available
import cbuild "github.com/pip-services4/pip-services4-go/pip-services4-components-go/build"

type MyFactory struct {
	*cbuild.Factory
}

func NewMyFactory() *MyFactory {
	c := &MyFactory{}
	c.Factory = cbuild.NewFactory()
	c.RegisterType(crefer.NewDescriptor("myservice", "MyComponentA", "*", "*", "1.0"), NewMyComponentA)
	return c
}
Not available
from pip_services4_components.build import Factory 

class MyFactory(Factory):
    def __init__(self):
        super(MyFactory, self).__init__()

        ComponentADescriptor = Descriptor("mycontroller", "MyComponentA", "*", "*", "1.0")
        self.register_as_type(ComponentADescriptor, MyComponentA)
Not available

Configuration file

Once our container and factory are ready, we create a configuration file that will be used by the ProcessContainer to locate the component. As the ProcessContainer class offers the possibility of using a logger, we also add a reference to it. In this manner, we will be able to see the logs on our console. The following code explains how to do this:

---
# Context information
- descriptor: "pip-services:context-info:default:default:1.0"
  name: mycontroller
  description: My controller is running in a process container

# Console logger
- descriptor: "pip-services:logger:console:default:1.0"
  level: {{LOG_LEVEL}}{{^LOG_LEVEL}}info{{/LOG_LEVEL}}

# Performance counters that posts values to log
- descriptor: "pip-services:counters:log:default:1.0"
  
# My component
- descriptor: "mycontroller:MyComponentA:default:*:1.0"

Creating a ProcessContainer

The final step is to create the ProcessContainer. To do this, we create a class that extends the ProcessContainer component. In it, we add our previously created factory and specify the path to our configuration file (The default path is ./config/config.yml). The following code shows how to do this:

import { ProcessContainer } from "pip-services4-container-node";

export class MyProcess extends ProcessContainer {
    public constructor() {
        super('myservice', 'My service running as a process');
        this._configPath = './config.yaml'
        this._factories.add(new MyFactory())
    }
}
Not available
import (
        crefer "github.com/pip-services4/pip-services4-go/pip-services4-components-go/refer"
	ccont "github.com/pip-services4/pip-services4-go/pip-services4-container-go/container"
)

type MyProcess struct {
	*ccont.ProcessContainer
}

func NewMyProcess() *MyProcess {
	c := &MyProcess{}
	c.ProcessContainer = ccont.NewProcessContainer("myservice", "My service running as a process")
	c.SetConfigPath("./config.yaml")
	c.AddFactory(NewMyFactory())
	return c
}
Not available
from pip_services4_container import ProcessContainer
from pip_services4_components.refer import Descriptor

class MyProcess(ProcessContainer):
    def __init__(self):
        super(MyProcess, self).__init__('mycontroller', 'My controller running as a process')
        self._config_path = './config.yaml'
        self._factories.add(MyFactory())
Not available

Running the container

Now that our code is ready, we can run our container. This is done via its run() method. For example, in the code below, we create an instance of our container and call this method.

export async function main() {
    let runner = new MyProcess();
    try {
        runner.run(process.argv);
    } catch (err) {
        console.error(err)
    }
}
Not available
func main() {
	proc := NewMyProcess()
    // proc.SetConfigPath("./config/config.yml")
	proc.Run(context.Background(), os.Args)
}
Not available
if __name__ == '__main__':
    runner = MyProcess()
    print("run")
    try:
        runner.run()
    except Exception as ex:
        print(ex)
Not available

As a result, we get the following output on our console:

figure 1

As we can see from the above results, the container takes care of the lifecycle of the packaged components:

  1. It obtains information about dependencies from the configuration file.
  2. Creates the packaged components via their respective factories.
  3. Configures the configurable components.
  4. Links the components to the components registered in their set_references() method.
  5. Opens the openable components.

Once the process is stopped, the container:

  1. Unlinks the linked components.
  2. Closes the opened components
  3. Closes itself.

Final code

Finally, we merge the code from the previous sections into one program:

import { 
    ConfigParams, Descriptor, Factory, IConfigurable, 
    IExecutable, IOpenable, IReferenceable, IReferences, 
    IUnreferenceable, Context, Parameters 
} from "pip-services4-components-node";
import { ProcessContainer } from "pip-services4-container-node";

export async function main() {
    let runner = new MyProcess();
    try {
        runner.run(process.argv);
    } catch (err) {
        console.error(err)
    }
}

export class MyProcess extends ProcessContainer {
    public constructor() {
        super('myservice', 'My service running as a process');
        this._configPath = './config.yaml'
        this._factories.add(new MyFactory())
    }
}

export class MyFactory extends Factory {
    public constructor() {
        super();

        this.registerAsType(new Descriptor("myservice", "MyComponentA", "*", "*", "1.0"), MyComponentA);
    }
}

export class MyComponentA implements IReferenceable, IUnreferenceable, IConfigurable, IOpenable, IExecutable {

    private _open = false;

    public constructor() {
        console.log("MyComponentA has been created.");
    }

    public configure(config: ConfigParams): void {
        console.log("MyComponentA has been configured.");
    }

    public setReferences(references: IReferences): void {
        console.log("MyComponentA's references have been defined.");
    }

    public unsetReferences(): void {
        console.log("References cleared");
    }

    public isOpen(): boolean {
        return this._open;
    }

    public async open(ctx: Context): Promise<void> {
        console.log("MyComponentA has been opened.");
    }

    public async close(ctx: Context): Promise<void> {
        console.log("MyComponentA has been closed.");
    }

    public async execute(ctx: Context, args: Parameters): Promise<any> {
        console.log("Executing");
        let result = args;
        return result;
    }
}
Not available
import (
	"context"
	"fmt"
	"os"

	cbuild "github.com/pip-services4/pip-services4-go/pip-services4-components-go/build"
	cconf "github.com/pip-services4/pip-services4-go/pip-services4-components-go/config"
	cexec "github.com/pip-services4/pip-services4-go/pip-services4-components-go/exec"
	crefer "github.com/pip-services4/pip-services4-go/pip-services4-components-go/refer"
	ccont "github.com/pip-services4/pip-services4-go/pip-services4-container-go/container"
)

func main() {
	proc := NewMyProcess()
	// proc.SetConfigPath("./config/config.yml")
	proc.Run(context.Background(), os.Args)
}

type MyProcess struct {
	*ccont.ProcessContainer
}

func NewMyProcess() *MyProcess {
	c := &MyProcess{}
	c.ProcessContainer = ccont.NewProcessContainer("myservice", "My service running as a process")
	c.SetConfigPath("./config.yaml")
	c.AddFactory(NewMyFactory())
	return c
}

type MyFactory struct {
	*cbuild.Factory
}

func NewMyFactory() *MyFactory {
	c := &MyFactory{}
	c.Factory = cbuild.NewFactory()
	c.RegisterType(crefer.NewDescriptor("myservice", "MyComponentA", "*", "*", "1.0"), NewMyComponentA)
	return c
}

type MyComponentA struct {
	opened bool
}

func NewMyComponentA() *MyComponentA {
	defer fmt.Println("MyComponentA has been created.")
	c := &MyComponentA{}
	return c
}

func (c *MyComponentA) Configure(ctx context.Context, config *cconf.ConfigParams) {
	fmt.Println("MyComponentA has been configured.")
}

func (c *MyComponentA) SetReferences(ctx context.Context, references crefer.IReferences) {
	fmt.Println("MyComponentA's references have been defined.")
}

func (c *MyComponentA) UnsetReferences() {
	fmt.Println("References cleared")
}

func (c *MyComponentA) IsOpen() bool {
	return c.opened
}

func (c *MyComponentA) Open(ctx context.Context, correlationId string) error {
	fmt.Println("MyComponentA has been opened.")
	return nil
}

func (c *MyComponentA) Close(ctx context.Context, correlationId string) error {
	fmt.Println("MyComponentA has been closed.")
	return nil
}

func (c *MyComponentA) Execute(ctx context.Context, correlationId string, args cexec.Parameters) (result any, err error) {
	fmt.Println("Executing")
	result = args
	return result, nil
}
Not available
from pip_services4_components.refer import IReferenceable, IReferences, IUnreferenceable
from pip_services4_components.run import IOpenable
from pip_services4_components.exec import IExecutable
from pip_services4_components.config import IConfigurable

class MyComponentA(IReferenceable, IUnreferenceable, IConfigurable, IOpenable, IExecutable):
    
    def __init__(self):
        print("MyComponentA has been created.")
            
    def configure(self, config):
        print("MyComponentA has been configured.")
       
    def is_open(self):
        return self._open

    def open(self, context):
        print("MyComponentA has been opened.")
    def close(self, context):
        print("MyComponentA has been closed.")
        
    def my_task(self, context):
        print("Doing my business task")
        dummy_variable = "dummy value"
    
    def set_references(self, references):
        print("MyComponentA's references have been defined.")    
        
    def unset_references(self):
        print("References cleared")
    
    def __del__(self):
        print("Component destroyed")
        
    def execute(self, context: str, args: str): 
        print('Executing')
        result = args
        return result
    
    
from pip_services4_components.build import Factory 

class MyFactory(Factory):
    def __init__(self):
        super(MyFactory, self).__init__()

        ComponentADescriptor = Descriptor("mycontroller", "MyComponentA", "*", "*", "1.0")
        self.register_as_type(ComponentADescriptor, MyComponentA)
        
from pip_services4_container import ProcessContainer
from pip_services4_components.refer import Descriptor

class MyProcess(ProcessContainer):
    def __init__(self):
        super(MyProcess, self).__init__('mycontroller', 'My controller running as a process')
        self._config_path = './configProcessContainer.yaml'
        self._factories.add(MyFactory())
        
if __name__ == '__main__':
    runner = MyProcess()
    print("run")
    try:
        runner.run()
    except Exception as ex:
        print(ex)
        
Not available

Wrapping up

In this tutorial, we have learned how to create a ProcessContainer. This component allows us to run our code as a system process.

First, we created a custom component that is openable and configurable. Then, we created a factory to create this component via the container and a configuration file used by the container to locate the different components. Finally, we created the container, run it, and obtained an output on our console showing the creation, configuration and opening of our component.

The example in this tutorial used a custom component. The same procedure applies to the containerization of any other component or set of components.