Usar Cloud Pub/Sub con PHP

Muchas aplicaciones necesitan realizar procesamientos en segundo plano fuera del contexto de una petición web. En esta muestra, la aplicación Bookshelf envía tareas a un trabajador en segundo plano independiente para su ejecución. El trabajador recopila información de la API de Google Libros y actualiza los datos del libro en la base de datos. Aquí se explica cómo configurar servicios independientes en Google App Engine, cómo ejecutar el proceso de un trabajador en el entorno flexible de App Engine y cómo gestionar los eventos del ciclo de vida.

Esta página forma parte de un tutorial de varias páginas. Para empezar en orden y consultar las instrucciones de configuración, ve a la sección relativa a la aplicación Bookshelf para PHP.

Establecer configuración

  1. En el directorio getting-started-php/6-pubsub, copia el archivo settings.yml del apartado sobre eventos de la aplicación de almacenamiento de registros de este tutorial.

  2. Añade lo siguiente al final del archivo settings.yml:

    pubsub_topic_name: book-created-or-updated
    pubsub_subscription_name: fill-book-details
    

Si usas Cloud SQL para almacenar tus datos, actualiza worker.yaml antes de realizar el despliegue:

  1. Abre worker.yaml para editarlo.

  2. Elimina los comentarios de las líneas beta_settings y cloud_sql_instances. En cloud_sql_instances, define el mismo valor que usaste para cloudsql_connection_name en config/settings.yml. Debería tener el formato your_project_name:your_region:your_instance.

  3. Guarda y cierra worker.yaml.

Instalar dependencias

Introduce el siguiente comando en el directorio 6-pubsub:

composer install

Ejecutar la aplicación en la máquina local

  1. Inicia un servidor web local:

    php -S localhost:8000 -t web
    
  2. En otra ventana de terminal, inicia el trabajador de Cloud Pub/Sub:

    php bin/pubsub/entrypoint.php
    
  3. Introduce la siguiente dirección en el navegador web:

    http://localhost:8000

Desplegar la aplicación en el entorno flexible de App Engine

  1. Despliega la aplicación de muestra:

    gcloud app deploy
    
  2. Introduce la siguiente dirección en el navegador web. Sustituye [YOUR_PROJECT_ID] por el ID del proyecto:

    https://[YOUR_PROJECT_ID].appspot.com
    
  3. Despliega el servicio del trabajador:

    gcloud app deploy worker.yaml
    
  4. Comprueba si el servicio del trabajador se está ejecutando:

    https://worker-dot-[YOUR_PROJECT_ID].appspot.com
    

Si actualizas la aplicación, podrás desplegar la versión actualizada con el mismo comando que utilizaste para desplegar la aplicación por primera vez. El nuevo despliegue crea una nueva versión de la aplicación y la establece como la versión predeterminada. No obstante, se conservarán las versiones anteriores, al igual que las instancias de máquina virtual asociadas. Ten en cuenta que todas estas versiones de la aplicación y las instancias de máquina virtual son recursos facturables.

Si eliminas las versiones no predeterminadas de la aplicación, puedes reducir los costes.

Para eliminar una versión de la aplicación, sigue las instrucciones que figuran a continuación:

  1. In the GCP Console, go to the Versions page for App Engine.

    Go to the Versions page

  2. Select the checkbox for the non-default app version you want to delete.
  3. Click Delete to delete the app version.

Consulta la sección sobre cómo eliminar los recursos facturables en el último paso de este tutorial para obtener más información al respecto.

Estructura de la aplicación

En el siguiente diagrama se muestran los componentes de la aplicación y cómo interactúan entre ellos:

Estructura de muestra de Cloud Pub/Sub

Información sobre el código

En esta sección, se explica detalladamente el código de la aplicación y su funcionamiento.

Colocación en cola de las tareas

La aplicación crea la instancia de un objeto PubSubClient mediante el uso de la biblioteca cliente de Google Cloud para PHP. Google Cloud es un cliente PHP idiomático que interactúa con Google Cloud Platform. La biblioteca se incluye con Composer como google/cloud. El código utiliza el ID del proyecto para crear el objeto PubSubClient.

use Google\Cloud\PubSub\PubSubClient;
$app['pubsub.client'] = function ($app) {
    // create the pubsub client
    $projectId = $app['config']['google_project_id'];
    $pubsub = new PubSubClient([
        'projectId' => $projectId,
    ]);
    return $pubsub;
};

El código usa el objeto PubSubClient para recuperar un tema de Cloud Pub/Sub llamado fill-book-details. El nombre del tema se especifica en config/settings.yml. Si el tema aún no existe, se crea.

$app['pubsub.topic'] = function ($app) {
    // create the topic if it does not exist.
    /** @var Google\Cloud\PubSub\PubSubClient **/
    $pubsub = $app['pubsub.client'];
    $topicName = $app['config']['pubsub_topic_name'];
    $topic = $pubsub->topic($topicName);
    if (!$topic->exists()) {
        $topic->create();
    }
    return $topic;
};

El controlador pone en cola una tarea cada vez que se crea o actualiza un libro mediante la publicación del ID del libro en el tema de Cloud Pub/Sub.

if ($id = $model->create($book)) {
    /** @var Google\Cloud\PubSub\PubSub\Topic $topic */
    $topic = $app['pubsub.topic'];
    $topic->publish([
        'data' => 'Updated Book',
        'attributes' => [
            'id' => $id
        ]
    ]);
}

Una vez publicado el tema de Cloud Pub/Sub, un servicio de trabajador puede suscribirse a él. Los trabajadores se ejecutan en instancias independientes y realizan tareas por separado que no aumentan la latencia de la aplicación web principal. En este tutorial, el trabajador busca más información sobre los libros. Si a un libro le falta información, el trabajador usa el ID proporcionado para extraer los datos del libro, llamar a la API de Google Libros y actualizar el libro.

Backend de tareas de Cloud Pub/Sub

En el backend, un proceso PHP de larga ejecución realiza una operación GET dependiente y asíncrona con la API de Cloud Pub/Sub, de modo que los mensajes nuevos se reciben de inmediato. A través de la biblioteca de sockets Ratchet, el proceso escucha el comprobador de estado de App Engine mientras espera al mismo tiempo nuevos mensajes.

use Google\Cloud\Samples\Bookshelf\PubSub\HealthCheckListener;
use Ratchet\Http\HttpServer;
use Ratchet\Server\IoServer;
// Listen to port 8080 for our health checker
$server = IoServer::factory(
    new HttpServer(new HealthCheckListener($logger)),
    8080
);

La clase HealthCheckListener implementa Ratchet\Http\HttpServerInterface y escucha en el puerto 8080. Al acceder a este servicio, se muestra el mensaje "Pubsub worker is running!" en el navegador, que indica que el trabajador de Pub/Sub se está ejecutando. Si se produce un error inesperado en el proceso, el comprobador de estado recibe una respuesta que especifica que está en mal estado y se crea una nueva instancia del servicio del trabajador.

public function onOpen(ConnectionInterface $conn, RequestInterface $request = null)
{
    // send the 200 OK health response and return
    $response = new Response(200, [], 'Pubsub worker is running!');
    $conn->send((string) $response);
    $conn->close();
}

La clase Worker se comunica con la API de Cloud Pub/Sub y se añade al servidor de Ratchet con un temporizador periódico para garantizar que se llame continuamente.

use Google\Cloud\Samples\Bookshelf\PubSub\LookupBookDetailsJob;
use Google\Cloud\Samples\Bookshelf\PubSub\Worker;
// create the job and worker
$job = new LookupBookDetailsJob($app['bookshelf.model'], $app['google_client']);
$worker = new Worker($app['pubsub.subscription'], $job, $logger);
// add our worker to the event loop
$server->loop->addPeriodicTimer(0, $worker);

La clase Worker define una devolución de llamada para cuando la API de Cloud Pub/Sub devuelve una respuesta. La devolución de la llamada es necesaria porque la petición que se realiza a la API de Cloud Pub/Sub es asíncrona.

$callback = function ($response) use ($job, $subscription, $logger) {
    $ackMessages = [];
    $messages = json_decode($response->getBody(), true);
    if (isset($messages['receivedMessages'])) {
        foreach ($messages['receivedMessages'] as $message) {
            $pubSubMessage = new Message($message['message'], array('ackId' => $message['ackId']));
            $attributes = $pubSubMessage->attributes();
            $logger->info(sprintf('Message received for book ID "%s" ', $attributes['id']));
            // Do the actual work in the LookupBookDetailsJob class
            $job->work($attributes['id']);
            $ackMessages[] = $pubSubMessage;
        }
    }
    // Acknowledge the messsages have been handled
    if (!empty($ackMessages)) {
        $subscription->acknowledgeBatch($ackMessages);
    }
};

Las promesas se usan para realizar la petición y ejecutar la devolución de llamada.

$promise = $this->connection->pull([
    'maxMessages' => 1000,
    'subscription' => $this->subscription->info()['name'],
]);
$promise->then($callback);

La devolución de llamada recibe todos los mensajes nuevos y transfiere el ID del libro a la clase Worker.

API de Google Libros

Para procesar libros que se añadieron a una cola, una suscripción de Cloud Pub/Sub escucha los mensajes publicados en el tema book-created-or-updated. La aplicación de muestra utiliza el cliente PHP de las API de Google para buscar detalles del libro desde la API de Google Libros.

Cuando se ejecuta una tarea, la clase LookupBookDetailsJob recupera una lista de libros, basada en el título de un libro, de la API de Google Libros.

$service = new Google_Service_Books($this->client);
$options = ['orderBy' => 'relevance'];
$results = $service->volumes->listVolumes($book['title'], $options);

Si algún resultado de la API incluye la imagen de portada de un libro, se selecciona como la mejor coincidencia. La imagen del libro se actualiza y guarda en la base de datos.

foreach ($results as $result) {
    $volumeInfo = $result->getVolumeInfo();
    if ($volumeInfo === null) {
        return false;
    }
    $imageInfo = $volumeInfo->getImageLinks();
    if ($imageInfo === null) {
        return false;
    }
    if ($thumbnail = $imageInfo->getThumbnail()) {
        $book['image_url'] = $thumbnail;
        $this->client->getLogger()->info(sprintf(
            'Updating book "%s" with thumbnail "%s"',
            $id, basename($thumbnail)));
        return $this->model->update($book);
    }
}

Configuración de supervisord

El entorno flexible de App Engine usa supervisord para gestionar los procesos. De forma predeterminada, supervisord ejecuta nginx y php-fpm para poner en funcionamiento aplicaciones web en PHP. Sin embargo, algunas aplicaciones no necesitan nginx ni php-fpm. En tales casos, puedes sustituir la configuración principal de supervisord por la opción de configuración supervisord_conf_override como se muestra a continuación:

runtime_config:
  document_root: .
  supervisord_conf_override: config/worker-supervisord.conf

worker-supervisord.conf tiene una sección para ejecutar el trabajador de Pub/Sub como se muestra a continuación:

[supervisord]
nodaemon = true
logfile = /dev/null
logfile_maxbytes = 0
pidfile = /var/run/supervisord.pid

[program:pubsub-worker]
command = php /app/bin/pubsub/entrypoint.php
stdout_logfile = /dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile = /dev/stderr
stderr_logfile_maxbytes=0
user = www-data
autostart = true
autorestart = true
priority = 5
stopwaitsecs = 20

Con estos archivos presentes, el tiempo de ejecución solo ejecutará nuestro trabajador en lugar de nginx y php-fpm.

Este programa escucha directamente en el puerto 8080, de modo que puede responder al comprobador de estado mientras espera nuevas tareas.

A nivel local, puedes introducir el siguiente comando para ejecutar el trabajador:

php bin/pubsub/entrypoint.php

Cuando se ejecuta el trabajador, escucha los mensajes en la suscripción de Cloud Pub/Sub fill-book-details correspondiente al tema book-created-or-updated. Cuando se recibe un mensaje, el libro asociado se recupera de la base de datos y LookupBookDetailsJob se ejecuta inmediatamente para actualizarlo.

<?php
/*
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

require_once __DIR__ . '/../../vendor/autoload.php';

/** @var Silex\Application $app */
$app = require __DIR__ . '/../../src/app.php';

/** @var Ratchet\Server\IoServer $server */
$server = $app['pubsub.server'];
$server->run();

Ejecución en Cloud Platform

El trabajador se despliega como un módulo independiente de la misma aplicación. Las aplicaciones de App Engine pueden tener varios servicios independientes. Esto significa que puedes desplegar, configurar, escalar y actualizar de manera fácil e independiente partes de la aplicación. La interfaz se despliega en el módulo predeterminado y el trabajador, en el módulo del trabajador.

En el siguiente diagrama, se compara el despliegue de un solo módulo (a la izquierda) y el de varios módulos (a la derecha):

Despliegue de Cloud Pub/Sub

Eliminar los recursos

Para evitar que los recursos utilizados en este tutorial se cobren en tu cuenta de Google Cloud Platform, sigue estas instrucciones:

Eliminar el proyecto

La forma más fácil de evitar la facturación consiste en eliminar el proyecto que has creado para el tutorial.

Para eliminar el proyecto, sigue las instrucciones que figuran a continuación:

  1. In the GCP Console, go to the Projects page.

    Go to the Projects page

  2. In the project list, select the project you want to delete and click Delete .
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

Eliminar versiones no predeterminadas de la aplicación

Si no quieres eliminar el proyecto, puedes reducir los costes quitando las versiones no predeterminadas de la aplicación.

Para eliminar una versión de la aplicación, sigue las instrucciones que figuran a continuación:

  1. In the GCP Console, go to the Versions page for App Engine.

    Go to the Versions page

  2. Select the checkbox for the non-default app version you want to delete.
  3. Click Delete to delete the app version.

Eliminar una instancia de Cloud SQL

Para eliminar una instancia de Cloud SQL, sigue las instrucciones que se muestran a continuación:

  1. En GCP Console, ve a la página SQL Instances.

    Ir a la página SQL Instances.

  2. Selecciona el nombre de la instancia de SQL que quieres borrar.
  3. Haz clic en el botón Borrar en la parte superior de la página para borrar la instancia.

Eliminar un segmento de Cloud Storage

Para eliminar un segmento de Cloud Storage, sigue las instrucciones que se indican a continuación:

  1. In the GCP Console, go to the Cloud Storage Browser page.

    Go to the Cloud Storage Browser page

  2. Click the checkbox for the bucket you want to delete.
  3. Click Delete to delete the bucket.

Siguientes pasos

Obtén información sobre cómo ejecutar la muestra de Bookshelf para PHP en Kubernetes Engine.

Obtén información sobre cómo ejecutar la muestra de Bookshelf para PHP en Compute Engine.

Prueba otras funciones de Google Cloud Platform por tu cuenta. Echa un vistazo a nuestros tutoriales.

¿Te ha resultado útil esta página? Enviar comentarios: