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 codekata o tutorial que 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, todo lo podemos hacer desde un proyecto Symfony en el 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. Además también podemos desde PHP con las funciones exec, shell_exec, passthru y system lanzar cualquier tipo de proceso hecho cualquier otro lenguaje para hacer cualquier cosa. De esta manera, tenemos 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, lanzando todo tipo de procesos, paralelizándolos para que en conjunto todo se ejecute más rápido, esperar respuestas, enlazar con otros sistemas, etc..

He subido los códigos fuentes de este codekata a Github, pero si quieres hacer el codekata mejor si sigues todos los pasos creando en local tu proyecto 😉
https://github.com/jaimenj/symfony-starter-tutorials

Al grano, creando un proyecto desastre para probar

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

symfony new symfony-tutorial-18
cd symfony-tutorial-18/
composer require twig annotations process
composer require --dev maker 

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 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/usuario/projects-src/symfony-starter-tutorials/symfony-tutorial-18/src/Controller/MainController.php'|file_link(0) }}">src/Controller/MainController.php</a></code></li>
        <li>Your template at <code><a href="{{ '/home/usuario/projects-src/symfony-starter-tutorials/symfony-tutorial-18/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,
        ]);
    }
}

Mira que se añaden estas dos clases Process y ProcessFailedException.

use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

Y la plantilla de l fichero symfony-tutorial-18/templates/main/index.html.twig la modificamos para que nos muestre el contenido de la variable $output:

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

Podría ser 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 hemos 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í:

symfony server:start

Si todo ha ido bien, podemos 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

Una cosa muy potente del componente Process 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;
        $sigint = 0;
        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.

Esto mismo pero en un ordenador de ocho núcleos usando al 100% siete de ellos se vería como algo así:

Symfony lanzando en paralelo 7 procesos el componente Process..

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

Y remitirte a los códigos fuentes que los he subido a Github con el resto de tutoriales de la serie:
https://github.com/jaimenj/symfony-starter-tutorials

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

Deja un comentario

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