To Recipes

Component lifecycle

by Alex Mazur

A microservice is a set of loosely coupled components, each of which serves a specific purpose, such as logging events, reading records from a database, or connecting to a 3rd party service.
One of the roles of the microservice’s container is to correctly initialize all internal components, each of which can have its own lifecycle. For example, loading its own configuration, running certain functional processes, and even waiting for results from other components. The order in which component lifecycle management methods are called is as follows:

  1. Configuration of the component is performed (configure)
  2. Components are linked (setReference)
  3. Connections are opened and active processes are started (open)
    ...
  4. Business processes are called or event notifications are sent (execute, notify)
    ...
  5. Active processes are stopped and connections are closed (close)
  6. The links between components are destroyed (unsetReferences)
  7. The component is destroyed

A flexible and, at the same time, standardized approach was developed in the Pip.Services Toolkit for initializing components. This approach preserves the conceptual integrity of the whole microservice, while keeping the source code clean, coherent, and testable. A component’s lifecycle is determined by which of the following interfaces it implements:

  • IConfigurable – component configuration [Config],
  • IReferenceable и IUnreferenceable – setting and destroying references to other components [Reference],
  • IOpenable и IClosable – starting and stopping internal functional processes [Openable],
  • IExecutable – execution of functional processes [Executable].
  • INotifiable - sending event notifications [Notifiable]
public interface IConfigurable
{
  void Configure(ConfigParams config);
}
public interface IReferenceable
{
  void SetReferences(IReferences references);
}
public interface IOpenable : IClosable
{
  bool IsOpen();
  Task OpenAsync(string correlationId);
}
public interface IClosable
{
  Task CloseAsync(string correlationId);
}
public interface IExecutable
{
  Task<object> ExecuteAsync(string correlationId, Parameters args);
}

Microservice developers are free to implement just the interfaces needed by their components. When the container is started, all implemented methods will be called in the previously mentioned order. 
For example: 

public sealed class CounterController : IReferenceable, IReconfigurable, IOpenable, IExecutable
{
  private readonly CompositeLogger _logger = new CompositeLogger();
  private FixedRateTimer Timer { get; set; } = new FixedRateTimer();
  private Parameters Parameters { get; set; } = new Parameters();
  private long Counter { get; set; } = 0;
  public void Configure(ConfigParams config)
  {
    Parameters = Parameters.FromConfig(config);
  }
  public void SetReferences(IReferences references)
  {
    _logger.SetReferences(references);
  }
  public bool IsOpen()
  {
    return Timer.IsStarted;
  }
  public Task OpenAsync(string correlationId)
  {
    Timer.Task = new Action(async () => await ExecuteAsync(correlationId, Parameters));
    Timer.Interval = 1000;
    Timer.Delay = 1000;
    Timer.Start();
    _logger.Trace(correlationId, "Counter controller opened");
    return Task.CompletedTask;
  }
  public Task CloseAsync(string correlationId)
  {
    Timer.Stop();
    _logger.Trace(correlationId, "Counter controller closed");
    return Task.CompletedTask;
  }
  public async Task<object> ExecuteAsync(string correlationId, Parameters parameters)
  {
   _logger.Info(correlationId, "{0} - {1}", Counter++, 
    Parameters.GetAsStringWithDefault("message", "Hello World!"));
    return await Task.FromResult(Counter);
  }
}

The Pip.Service’s Toolkit also includes a few utilities that can be used during microservice development:
- Opener – initiates the functional processes of selected components,
- Closer – stops the functional processes of selected components,
- Executor – runs the functional processes of selected components,
- Notifier - sends event notifications for selected components.
- Cleaner – cleans the current state of selected components.
For example:
 await Opener.OpenAsync(correlationId, _references.GetAll());

 await Closer.CloseAsync(correlationId, _references.GetAll());

abstract class IConfigurable {
  void configure(ConfigParams config);
}
abstract class IReferenceable {
  void setReferences(IReferences references);
}
abstract class IOpenable implements IClosable {
  bool isOpen();
  Future open(String correlationId);
}
abstract class IClosable {
    Future close(String correlationId);
}
abstract class IExecutable {
   Future<dynamic> execute(String correlationId, Parameters args);
}

Microservice developers are free to implement just the interfaces needed by their components. When the container is started, all implemented methods will be called in the previously mentioned order. 
For example: 

class CounterController implements IReferenceable, IReconfigurable, IOpenable, IExecutable
{
  final CompositeLogger _logger = new CompositeLogger();
  final FixedRateTimer timer = new FixedRateTimer();
  final Parameters parameters  = new Parameters();
  int counter  = 0;
  void configure(config ConfigParams )
  {
    parameters = Parameters.fromConfig(config);
  }
  public void setReferences(references IReferences)
  {
    _logger.setReferences(references);
  }
  public bool IsOpen()
  {
    return timer.isStarted;
  }
  Future open(correlationId String) async
  {
    timer.task = () async {return await Execute(correlationId, parameters)};
    timer.interval = 1000;
    timer.delay = 1000;
    timer.start();
    _logger.trace(correlationId, "Counter controller opened");
  }
  Future close(correlationId String)
  {
    timer.stop();
    _logger.trace(correlationId, "Counter controller closed");
  }
  Future<int> Execute(correlationId String, parameters Parameters ) async
  {
   _logger.info(correlationId, "$s - $s", counter++, 
    parameters.getAsStringWithDefault("message", "Hello World!"));
    return counter;
  }
}

The Pip.Service’s Toolkit also includes a few utilities that can be used during microservice development:
- Opener – initiates the functional processes of selected components,
- Closer – stops the functional processes of selected components,
- Executor – runs the functional processes of selected components,
- Notifier - sends event notifications for selected components.
- Cleaner – cleans the current state of selected components.
For example:
 await Opener.open(correlationId, references.getAll());

 await Closer.close(correlationId, references.getAll());