giudicelli/symfony-distributed-architecture

Symfony distributed architecture

0.5.8 2020-08-25 07:53 UTC

This package is auto-updated.

Last update: 2022-02-25 11:32:31 UTC


README

Symfony Distributed Architecture is a Symfony bundle. It extends distributed-architecture and distributed-architecture-queue to provide compatibility with the Command system from Symfony.

If you want to use an interface to control you distributed architecture, you can install symfony-distributed-architecture-admin.

Installation

$ composer require giudicelli/symfony-distributed-architecture

Using

To run your distributed architecture you will mainly need to use one command "bin/console distributed_architecture:run-master". It will parse the configuration and launch all processes.

The following options are handled by "distributed_architecture:run-master":

  • --max-running-time will gracefully stop all slave processes after a certain duration. It's usually a good idea to use this as Symfony commands tend to use more and more memory over time. A duration of 3600 seconds is in most case a good value. Default is 0, meaning the master will only exit once all the slaves a exited.
  • --max-process-timeout Set the maximum number of times a process can timeout before it is considered dead and removed. Default is 3.
  • --timeout Set the timeout for the master. Default is 300.
  • --service Run as detached service, even when all processes will have exited, "distributed_architecture:run-master" will not exit. You can install symfony-distributed-architecture-admin to control "distributed_architecture:run-master".
  • --user When --service is activated, run as this user. Ignored if not root.
  • --group When --service is activated, run as this group. Ignored if not root.
  • --log When --service is activated, specify in which file to log, default is %kernel.logs_dir%/distributed_architecture.log.
  • --pid When --service is activated, specify in which file to store the PID of the service, default is %kernel.logs_dir%/distributed_architecture.pid.
  • --stop perform a clean stop of the previously started service, you need to specify the same values for --timeout and --pid as when started the service, if you did not specify any of those just ignore those options.

Configuration

Place your configuration in "config/packages/distributed_architecture.yaml".

To see all available configuration options, you can execute the following command:

$ bin/console config:dump-reference distributed_architecture

Here is a complete example of a configuration.

distributed_architecture:
  groups:
    First Group: # The name of the group
      command: app:test-command # The command to be executed using bin/console
      bin_path: /usr/bin/php7 # When the binary is not the same as the master's
      path: /the/path/to/symfony # When Symfony's path is not the same as the master's
      priority: -10 # Set all processes' priority, it will require to whole architecture to run as root
      timeout: 60 # We consider a process timed out when not receiving data for this duration
      params: # Parameters that will be passed to all the processes
        Param1: Value1 
        Param2: Value2
      local: # We want to run a local process
        instances_count: 2 # We want to run 2 instances of the command
        bin_path: /usr/bin/php7-3 # We can overide the default value from the group
        path: /the/path/to/symfony4 # We can overide the default value from the group
        priority: 10 # We can overide the default value from the group
        timeout: 120 # We can overide the default value from the group
      remote: # We want to launch remote processes
        - # First remote process
          instances_count: 2  # We want to run 2 instances of the command on each host
          username: otherusername # When we should use another user name that the user used to run the master process
          private_key: /path/to/privateKey/id_rsa # When the private key used to connect is not stored in ~username/.ssh/id_rsa
          bin_path: /usr/bin/php7-3 # We can overide the default value from the group
          path: /the/path/to/symfony4 # We can overide the default value from the group
          priority: 10 # We can overide the default value from the group
          timeout: 120 # We can overide the default value from the group
          hosts: # The list of hosts
            - server-host-1
            - server-host-2
        - # Second remote process
          instances_count: 2  # We want to run 2 instances of the command on each host
          hosts: # The list of hosts
            - server-host-3
    Second Group: # The name of the group
      command: app:test-command-2 # The command to be executed using bin/console
      params: # Parameters that will be passed to all the processes
        Param1: Value1 
        Param2: Value2
      local: # We want to run a local process
        instances_count: 1 # We want to run 1 instance of the command
      remote: # We want to launch remote processes
        - # First remote process
          instances_count: 1  # We want to run 1 instance of the command on each host
          hosts: # The list of hosts
            - server-host-1
            - server-host-2
            - server-host-3
  queue_groups: # The list of feeder/consumers groups 
    Thrird Group: # The name of the group
      command: app:test-queue-command # The feeder/consumers command to be executed using bin/console
      local_feeder: # We want to run a local feeder process
        bind_to: 192.168.0.254 # The feeder should bind to this IP
        port: 9999 # The feeder should listen on this port (9999 is the default)
      #remote_feeder: # We want to run a remote feeder process
      #  bind_to: 192.168.0.254 # The feeder should bind to this IP
      #  port: 9999 # The feeder should listen on this port (9999 is the default)
      #  hosts:
      #    - server-host-1 #There can only be one host for a remote feeder
      consumers: # The list of consumers
        local:
          instances_count: 1  # We want to run 1 consumer instance of the command
          host: 192.168.0.254 # The IP address of the feeder
        remote: # We want to launch remote processes
          - # First remote process
            instances_count: 2  # We want to run 2 instances of the consumer command on each host
            host: 192.168.0.254 # The IP address of the feeder
            hosts: # The list of hosts
              - server-host-1
              - server-host-2

The above code creates three groups.

One group is called "First Group" and it will run "bin/console app:test-command":

  • 2 instances on the local machine,
  • 2 instances on the "server-host-1" machine,
  • 2 instances on the "server-host-2" machine,
  • 2 instances on the "server-host-3" machine.

A total of 8 instances of "bin/console test-command" will run.

The second group is called "Second Group" and it will run "bin/console app:test-command-2":

  • 1 instance on the local machine,
  • 1 instance on the "server-host-1" machine,
  • 1 instance on the "server-host-2" machine,
  • 1 instance on the "server-host-3" machine.

A total of 4 instances of "bin/console test-command-2" will run.

The third group is called "Third Group" and it will run "bin/console app:test-queue-command", which is a feeder/consumers model:

  • 1 feeder instance on the local machine, listening on 192.168.0.254:9999,
  • 2 consumer instances on the "server-host-1" machine, connecting to the feeder on 192.168.0.254:9999,
  • 2 consumer instances on the "server-host-2" machine, connecting to the feeder on 192.168.0.254:9999.

A total of 5 instances of "bin/console test-queue-command" will run.

Usually your configuration is the same between your master machine and your slave machines. Meaning:

  • the path to Symfony is the same,
  • the PHP binary is the same,
  • the current username as access to all remote machines using a private key,
  • the private key is stored in $HOME/.ssh/id_rsa.

When all those are true, your configuration can be very minimal.

distributed_architecture:
  groups:
    First Group: # The name of the group
      command: app:test-command # The command to be executed using bin/console
      params: # Parameters that will be passed to all the processes
        Param1: Value1 
        Param2: Value2
      local: # We want to run a local process
        instances_count: 2 # We want to run 2 instances of the command
      remote: # We want to launch remote processes
        - # First remote process
          instances_count: 2  # We want to run 2 instances of the command on each host
          hosts: # The list of hosts
            - server-host-1
            - server-host-2
        - # Second remote process
          instances_count: 2  # We want to run 2 instances of the command on each host
          hosts: # The list of hosts
            - server-host-3
    Second Group: # The name of the group
      command: app:test-command-2 # The command to be executed using bin/console
      params: # Parameters that will be passed to all the processes
        Param1: Value1 
        Param2: Value2
      local: # We want to run a local process
        instances_count: 1 # We want to run 2 instances of the command
      remote: # We want to launch remote processes
        - # First remote process
          instances_count: 1  # We want to run 2 instances of the command on each host
          hosts: # The list of hosts
            - server-host-1
            - server-host-2
            - server-host-3
  queue_groups: # The list of feeder/consumers groups 
    Thrird Group: # The name of the group
      command: app:test-queue-command # The feeder/consumers command to be executed using bin/console
      local_feeder: # We want to run a local feeder process
        bind_to: 192.168.0.254 # The feeder should bind to this IP
        port: 9999 # The feeder should listen on this port (9999 is the default)
      #remote_feeder: # We want to run a remote feeder process
      #  bind_to: 192.168.0.254 # The feeder should bind to this IP
      #  port: 9999 # The feeder should listen on this port (9999 is the default)
      #  hosts:
      #    - server-host-1 #There can only be one host for a remote feeder
      consumers: # The list of consumers
        local:
          instances_count: 1  # We want to run 1 consumer instance of the command
          host: 192.168.0.254 # The IP address of the feeder
        remote: # We want to launch remote processes
          - # First remote process
            instances_count: 2  # We want to run 2 instances of the consumer command on each host
            host: 192.168.0.254 # The IP address of the feeder
            hosts: # The list of hosts
              - server-host-1
              - server-host-2

Slave command

A slave command must extend the "giudicelli\DistributedArchitectureBundle\Command\AbstractSlaveCommand" class.

You may not pass it options, the only acceptable options are defined by "AbstractSlaveCommand" and are passed by "distributed_architecture:run-master". If you need to pass it some parameters, please use the "params" entries in the group's configuration.

Using the above example, here is a possible implementation for "app:test-command" or "app:test-command-2".

<?php

namespace App\Command;

use giudicelli\DistributedArchitectureBundle\Command\AbstractSlaveCommand;
use giudicelli\DistributedArchitectureBundle\Handler;
use giudicelli\DistributedArchitecture\Slave\HandlerInterface;

class TestCommand extends AbstractSlaveCommand
{
    protected function configure()
    {
        parent::configure();
        $this->setName('app:test-command');
        $this->setDescription('Do the work load');
    }

    // This method must be implemented
    protected function runSlave(?HandlerInterface $handler): void
    {
        if(!$handler) {
          echo "Not executed in distributed-architecture\n";
          die(1);
        }

        $groupConfig = $handler->getGroupConfig();

        $params = $groupConfig->getParams();

        // Handler::sleep will return false if we were
        // asked to stop by the master command
        while($handler->sleep(1)) {

            // Anything echoed here will be considered log level "info" by the master process.
            // If you want another level for certain messages, use $handler->getLogger().
            // echo "Hello world!\n" is the same as $handler->getLogger()->info('Hello world!')

            echo $params['Param1']." ".$params['Param2']."\n";
        }
    }
}

Feeder/Consumers slave command

A feeder/consumers slave command must extend the "giudicelli\DistributedArchitectureBundle\Command\AbstractSlaveQueueCommand" class.

You may not pass it options, the only acceptable options are defined by "AbstractSlaveQueueCommand" and are passed by "distributed_architecture:run-master". If you need to pass it some parameters, please use the "params" entries in the group's configuration.

Using the above example, here is a possible implementation for "app:test-queue-command".

<?php

namespace App\Command;

use giudicelli\DistributedArchitecture\Slave\HandlerInterface;
use giudicelli\DistributedArchitectureBundle\Command\AbstractSlaveQueueCommand;
use giudicelli\DistributedArchitectureQueue\Slave\Queue\Feeder\FeederInterface;

class TestQueueCommand extends AbstractSlaveQueueCommand
{
    protected function configure()
    {
        parent::configure();
        $this->setName('da:my-queue-command');
        $this->setDescription('Launch the slave test queue command');
    }

    /**
     * Return the instance of the FeederInterface, 
     * this is called when this command is run as a feeder 
     */
    protected function getFeeder(): FeederInterface
    {
        // The feeder is application related, 
        // it loads the items that need to be fed to the consumers
        return new Feeder();
    }

    /**
     * Handle an item sent by the feeder, 
     * this is called when this command is run as a consumer 
     */
    protected function handleItem(HandlerQueue $handler, array $item): void
    {
        // Anything echoed here will be considered log level "info" by the master process.
        // If you want another level for certain messages, use $handler->getLogger().
        // echo "Hello world!\n" is the same as $handler->getLogger()->info('Hello world!')

        // The content of $item is application related
        ...
    }
}

Pre run checks

If your slave command needs to run some checks before being actually run by the master it needs to implement the following method:

<?php

class TestQueueCommand extends AbstractSlaveQueueCommand
{
    [...]

    public function preRunCheck(GroupConfigInterface $groupConfig, LoggerInterface $logger): bool
    {
        if($this->someCheck()) {
          return true; // Everything is ok, proceed.
        }
        return false; // Not ok, master will not run this group config.
    }

    [...]
}

Events

This bundle dispatches a few events:

  • distributed_architecture.master_starting with a MasterStartingEvent object, is dispatched everytime a LauncherInterface is starting. It can be either on the master server or on a remote server, use MasterStartingEvent::getLauncher()::isMaster() to know if you're on the master server or on a remote.
  • distributed_architecture.master_started with a MasterStartedEvent object, is dispatched everytime a LauncherInterface is started. It can be either on the master server or on a remote server, use MasterStartedEvent::getLauncher()::isMaster() to know if you're on the master server or on a remote.
  • distributed_architecture.master_running with a MasterRunningEvent object, is dispatched on a regular basis. It can be either on the master server or on a remote server, use MasterRunningEvent::getLauncher()::isMaster() to know if you're on the master server or on a remote.
  • distributed_architecture.master_stopped with a MasterStoppedEvent object, is dispatched when a LauncherInterface exits. It can be either on the master server or on a remote server, use MasterStoppedEvent::getLauncher()::isMaster() to know if you're on the master server or on a remote.
  • distributed_architecture.process_created with a ProcessCreatedEvent object, is dispatched everytime a ProcessInterface is created.
  • distributed_architecture.process_started with a ProcessStartedEvent object, is dispatched everytime a ProcessInterface is started.
  • distributed_architecture.process_running with a ProcessRunningEvent object, is dispatched everytime the ProcessInterface sends data.
  • distributed_architecture.process_stopped with a ProcessStoppedEvent object, is dispatched when a ProcessInterface exits.
  • distributed_architecture.process_timedout with a ProcessTimedOutEvent object, is dispatched when a ProcessInterface has not sent any data in a certain time.