Symfony: tutorial 18: trabajando con el sistema operativo, Process

Lanzando procesos desde un proyecto Symfony con Process.

Continuando con la serie de tutoriales de iniciación a Symfony, aquí que vengo con el siguiente que me planifiqué repasar. Este tutorial trata sobre cómo lanzar programas del sistema operativo desde un proyecto Symfony. Es decir, si estamos en un sistema operativo GNU/Linux no hay nada que no podamos hacer en un servidor. O por lo menos no se me ocurre nada que no se pueda lanzar desde un proyecto Symfony.

Me explico, si en un servidor GNU/Linux todo lo podemos hacer desde línea de comandos.. si todo lo podemos copiar, mover, lanzar procesos de todo tipo desde línea de comandos.. también podemos desde PHP con las funciones exec, shell_exec, passthru y system. Así mismo, tenemos también en Symfony un componente para usar estas funciones más fácil todavía. Es decir, este componente Process es un wrapper vitaminado para usar estas funciones de PHP.

Empezamos, creando un proyecto desastre para probar

Podemos entonces ir a la línea de comandos y poner algo tal que así:

composer create-project symfony/skeleton symfony-process
cd symfony-process/
composer require twig annotations process
composer require --dev maker server

Entre comandos, tendremos que hacer unas esperas, y ya tenemos el proyecto si todo ha ido bien. Vamos ahora a crear un controlador de pruebas y así lo podemos ver en el navegador los resultados. Ejecutamos lo siguiente:

php bin/console make:controller

Yo le he llamado MainController. Al haber incluído antes los componentes twig y annotations que nos quedará más sencillo de mantener. En el mismo MainController.php tendremos todo lo del controlador, configuraciones incluidas. Ahora tendremos dos ficheros nuevos, el src/Controller/MainController.php y el fichero templates/main/index.html.twig.

El controlador nos quedará recién generado como el siguiente:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class MainController extends AbstractController
{
    /**
     * @Route("/main", name="main")
     */
    public function index()
    {
        return $this->render('main/index.html.twig', [
            'controller_name' => 'MainController',
        ]);
    }
}

La plantilla nos quedará como la siguiente:

{% extends 'base.html.twig' %}

{% block title %}Hello MainController!{% endblock %}

{% block body %}
<style>
    .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
    .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>

<div class="example-wrapper">
    <h1>Hello {{ controller_name }}! ✅</h1>

    This friendly message is coming from:
    <ul>
        <li>Your controller at <code><a href="{{ '/home/jaime/secundario/Descargas/symfony-process/src/Controller/MainController.php'|file_link(0) }}">src/Controller/MainController.php</a></code></li>
        <li>Your template at <code><a href="{{ '/home/jaime/secundario/Descargas/symfony-process/templates/main/index.html.twig'|file_link(0) }}">templates/main/index.html.twig</a></code></li>
    </ul>
</div>
{% endblock %}

Vamos ahora a modificarlo..

Lanzando algunos comandos con Process

Partiendo del controlador anterior, lo primero que podemos hacer es lanzar un comando simple. Por ejemplo si queremos lanzar un comando, que el controlador se espere a que termine, y que se vea la respuesta en el navegador. Podemos editar el controlador anterior a algo tal que así:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

class MainController extends AbstractController
{
    /**
     * @Route("/", name="main")
     */
    public function index()
    {
        $process = new Process(
            ['/bin/ls', '-lah', '../'],
            null,
            ['ENV_VAR_NAME' => 'valueOfEnvironmentVariable']
        );

        /*$process->run();
        $process->disableOutput();
        if (!$process->isSuccessful()) {
            throw new ProcessFailedException($process);
        }*/

        try {
            $process->mustRun();

            $output = $process->getOutput();
            $output = str_replace(PHP_EOL, '<br>', $output);
        } catch (ProcessFailedException $exception) {
            $output = $exception->getMessage();
        }

        return $this->render('main/index.html.twig', [
            'controller_name' => 'MainController',
            'output' => $output,
        ]);
    }
}

Y la plantilla la modificamos a algo como lo siguiente:

{% extends 'base.html.twig' %}

{% block title %}Hello MainController!{% endblock %}

{% block body %}
<style>
    .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
    .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>

<div class="example-wrapper">
    <h1>Hello {{ controller_name }}! ✅</h1>

    This friendly message is coming from:
    <ul>
        <li>Your controller at <code><a href="{{ '/home/jaime/secundario/Descargas/symfony-process/src/Controller/MainController.php'|file_link(0) }}">src/Controller/MainController.php</a></code></li>
        <li>Your template at <code><a href="{{ '/home/jaime/secundario/Descargas/symfony-process/templates/main/index.html.twig'|file_link(0) }}">templates/main/index.html.twig</a></code></li>
    </ul>

    <p>The output of the Process run is:</p>
    <code>{{ output | raw }}</code>
</div>
{% endblock %}

Mira que he modificado la ruta de entrada al controlador para que sea /. Lo siguiente a mirar es que tenemos dos formas de lanzar el objeto de tipo Process. Una forma es con run() y la otra con mustRun(). Como reza la documentación oficial, simplemente la segunda lanza una excepción si no se puede ejecutar. Te los dejo comentados en el código de arriba. Podemos lanzar el servidor de desarrollo así:

php bin/console server:start

..así tenemos que ver algo parecido a lo siguiente en http://localhost:8000/

Symfony Process, viendo en el navegador el resultado de listar un directorio..

Lanzando varios procesos en paralelo sin esperar a que termine la ejecución

Lo potente de este componente es que podemos lanzar el paralelo tantos procesos como queramos. Es una forma fácil de agilizar la ejecución en Symfony de varios procesos que podemos hacer correr en paralelo y que son independientes entre sí.

Podemos ejecutarlos de la forma anterior uno detrás de otro, pero este proceso sólo usará un hilo, un núcleo del procesador. Pero si tenemos varios procesadores o núcleos nos conviene paralelizar las tareas todo lo que podamos. La idea es que en vez de lanzarlo con run() o mustRun().. lo lanzamos con start(). Resumiendo, esto lanza el proceso y sigue la ejecución del script sin esperar a que finalice, lo cual nos permite paralelizar procesos.

Un controlador modificado para que simplemente lanze 40 tareas en paralelo puede ser el siguiente:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

class MainController extends AbstractController
{
    /**
     * @Route("/", name="main")
     */
    public function index()
    {
        // Paralell processing
        $processes = array();
        $output = '';
        $counter = 0;
        for ($i = 0; $i < 40; ++$i) {
            $process = new Process(
                ['echo', $counter]
            );
            $counter++;
            $process->start();
            $processes[] = $process;
        }
        sleep(5);
        $timeout = 3;
        foreach ($processes as $process) {
            $process->stop($timeout, SIGINT);
            $output .= 'Paralell process done! output: '.$process->getOutput().'<br>';
        }

        return $this->render('main/index.html.twig', [
            'controller_name' => 'MainController',
            'output' => $output,
        ]);
    }
}

Los 40 procesos son demasiado simples, simplemente son echos desde línea de comandos. Ni notarás diferencia entre la ejecución secuencial y la paralela. Pero en el siguiente apartado veremos un caso en el que sí se ve claro.

Para el anterior código tenemos que ver en el navegador un resultado como el siguiente:

Symfony, lanzando con Process 40 procesos en paralelo..

Cargando la CPU a ver el rendimiento

Vamos a darle unas vueltas a esto. Para este apartado necesitarás un GNU/Linux o derivado de Debian, ya que es lo que he usado para hacer estas pruebas.

En GNU/Linux hay un comando para probar el rendimiento de una máquina, simulando stress. Como su nombre indica, podemos con este comando hacer pruebas de stress en una máquina. Así con todo esto, podemos lanzar lo siguiente y nos la instalamos:

sudo apt install stress

Mi CPU tiene 4 núcleos, con lo que vamos a intentar lanzar 3 procesos en paralelo para que se vea el resultado a ver si resulta. Modificando el controlador anterior. Lo primero es ver que con el siguiente comando cargamos 1 núcleo al 100% durante 30 segundos:

GNU/Linux, lanzando stress durante 30 segundos en 1 núcleo al 100%..

Si todo va bien tenemos que ver con el comando htop algo como lo siguiente:

Lanzando stress en 1 núcleo al 100%.. viendo con htop el resultado..

Vamos ahora a lanzar 3 procesos iguales desde Symfony, para que se ejecuten en paralelo. Para esto podemos modificar el controlador anterior a algo como lo siguiente:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

class MainController extends AbstractController
{
    /**
     * @Route("/", name="main")
     */
    public function index()
    {
        // Paralell processing
        $processes = array();
        $output = '';
        $counter = 0;
        for ($i = 0; $i < 3; ++$i) {
            $process = new Process(
                ['stress','--cpu', '1', '--timeout', '30']
            );
            $counter++;
            $process->start();
            $processes[] = $process;

            $output .= 'Lanzado el proceso '.$counter.'<br>';
        }

        return $this->render('main/index.html.twig', [
            'controller_name' => 'MainController',
            'output' => $output,
        ]);
    }
}

Ahora si le damos a F5 en el navegador para recargar la página, lanzará los 3 procesos en paralelo, y rápidamente tenemos unos 30 segundos para verlos correr en paralelo. Si esto es cierto, se tiene que ver una imagen como la siguiente en línea de comandos en htop:

Symfony Process, lanzando 3 procesos en paralelo para aprovechar la CPU..

Pensémoslo un poco, si tenemos 3 procesos y los ejecutamos uno detrás de otro, tardarán el triple que usando Process y paralelizando. Ten en cuenta que el número de núcleos del procesador te limitará a lo máximo que puedes lanzar sin petar el servidor.

Prueba si quieres el último script, pero lanzando ahora con run() o mustRun(). Sólo verás que se usa 1 núcleo. Así que siempre que podamos, lanzamos con start(), y le dejamos al sistema operativo que gestione el proceso, como tarea independiente.

Terminando, referencias y algunos comentarios

El componente Process no se queda aquí. También podemos esperar a que un proceso concreto termine. Podemos hacer comprobaciones de estado, etc.. Ya dependiendo de la máquina donde corra el proyecto, y lo bien aprovechada que quieras que esté.

Sólo queda remitirme a la documentación oficial que está muy bien:
https://symfony.com/doc/current/components/process.html

¡Un saludo! Para cualquier cosa no dudes en dejar un comentario 😉

Compartir..

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *