Procesamiento en segundo plano con PHP


Muchas apps necesitan procesar en segundo plano fuera del contexto de una solicitud web. En este instructivo, se crea una app web que permite a los usuarios ingresar texto para traducir y, luego, muestra una lista de traducciones anteriores. La traducción se realiza en segundo plano para evitar que se bloquee la solicitud del usuario.

En el siguiente diagrama se ilustra el proceso de solicitud de traducción.

Diagrama de la arquitectura

Esta es la secuencia de eventos de cómo funciona la app del instructivo:

  1. Visita la página web para ver una lista de traducciones anteriores almacenadas en Firestore.
  2. Ingresa un formulario HTML para solicitar una traducción de texto.
  3. La solicitud de traducción se publica en Pub/Sub.
  4. Una app de Cloud Run recibe el mensaje de Pub/Sub.
  5. La app de Cloud Run usa Cloud Translation para traducir el texto.
  6. La app de Cloud Run almacena el resultado en Firestore.

Este instructivo está dirigido a cualquier persona que desee aprender sobre el procesamiento en segundo plano con Google Cloud. No se requiere experiencia en Pub/Sub, Firestore, App Engine o Cloud Run. Sin embargo, para comprender el código completo, resulta útil contar con cierta experiencia en PHP, JavaScript y HTML.

Objetivos

  • Comprender e implementar los servicios de Cloud Run
  • Comprender e implementar una app de App Engine
  • Probar la app

Costos

En este documento, usarás los siguientes componentes facturables de Google Cloud:

Para generar una estimación de costos en función del uso previsto, usa la calculadora de precios. Es posible que los usuarios nuevos de Google Cloud califiquen para obtener una prueba gratuita.

Cuando finalices las tareas que se describen en este documento, puedes borrar los recursos que creaste para evitar que continúe la facturación. Para obtener más información, consulta Cómo realizar una limpieza.

Antes de comenzar

  1. Accede a tu cuenta de Google Cloud. Si eres nuevo en Google Cloud, crea una cuenta para evaluar el rendimiento de nuestros productos en situaciones reales. Los clientes nuevos también obtienen $300 en créditos gratuitos para ejecutar, probar y, además, implementar cargas de trabajo.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. Asegúrate de que la facturación esté habilitada para tu proyecto de Google Cloud.

  4. Enable the Firestore, Cloud Run, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  5. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  6. Asegúrate de que la facturación esté habilitada para tu proyecto de Google Cloud.

  7. Enable the Firestore, Cloud Run, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  8. En la consola de Google Cloud, abre la app en Cloud Shell.

    Ir a Cloud Shell

    Cloud Shell brinda acceso de línea de comandos a los recursos en la nube directamente desde el navegador. Abre Cloud Shell en el navegador y haz clic en Continuar para descargar el código de muestra y pasar al directorio de la app.

  9. En Cloud Shell, configura la herramienta de gcloud para usar el proyecto de Google Cloud:
    # Configure gcloud for your project
    gcloud config set project YOUR_PROJECT_ID

Información sobre el backend de Cloud Run

Cuando invocas esta función, se define una sola función translateString de PHP y se configura el servicio de Cloud Run para que responda a un mensaje de Pub/Sub.

use Google\Cloud\Firestore\FirestoreClient;
use Google\Cloud\Firestore\Transaction;
use Google\Cloud\Translate\TranslateClient;

/**
 * @param array $data {
 *     The PubSub message data containing text and target language.
 *
 *     @type string $text
 *           The full text to translate.
 *     @type string $language
 *           The target language for the translation.
 * }
 */
function translateString(array $data)
{
    if (empty($data['language']) || empty($data['text'])) {
        throw new Exception('Error parsing translation data');
    }

    $firestore = new FirestoreClient();
    $translate = new TranslateClient();

    $translation = [
        'original' => $data['text'],
        'lang' => $data['language'],
    ];

    $docId = sprintf('%s:%s', $data['language'], base64_encode($data['text']));
    $docRef = $firestore->collection('translations')->document($docId);

    $firestore->runTransaction(
        function (Transaction $transaction) use ($translate, $translation, $docRef) {
            $snapshot = $transaction->snapshot($docRef);
            if ($snapshot->exists()) {
                return; // Do nothing if the document already exists
            }

            $result = $translate->translate($translation['original'], [
                'target' => $translation['lang'],
            ]);
            $transaction->set($docRef, $translation + [
                'translated' => $result['text'],
                'originalLang' => $result['source'],
            ]);
        }
    );

    echo "Done.";
}
  1. La función debe importar varias dependencias para conectarse con Firestore y Translation.

    use Google\Cloud\Firestore\FirestoreClient;
    use Google\Cloud\Firestore\Transaction;
    use Google\Cloud\Translate\TranslateClient;
    
  2. Primero, Cloud Run inicializa los clientes de Firestore y Pub/Sub. Luego, analiza los datos de los mensajes de Pub/Sub para obtener el texto que se traducirá y el idioma objetivo deseado.

    $firestore = new FirestoreClient();
    $translate = new TranslateClient();
    
    $translation = [
        'original' => $data['text'],
        'lang' => $data['language'],
    ];
  3. La API de Translation se usa para traducir la string al idioma deseado.

    $result = $translate->translate($translation['original'], [
        'target' => $translation['lang'],
    ]);
  4. La función asigna un nombre único a la solicitud de traducción para asegurarse de que no se almacene ninguna traducción duplicada. Luego, realiza la traducción en una transacción de Firestore para garantizar que, durante las ejecuciones simultáneas, no ejecute la misma traducción dos veces por accidente.

    $docId = sprintf('%s:%s', $data['language'], base64_encode($data['text']));
    $docRef = $firestore->collection('translations')->document($docId);
    
    $firestore->runTransaction(
        function (Transaction $transaction) use ($translate, $translation, $docRef) {
            $snapshot = $transaction->snapshot($docRef);
            if ($snapshot->exists()) {
                return; // Do nothing if the document already exists
            }
    
            $result = $translate->translate($translation['original'], [
                'target' => $translation['lang'],
            ]);
            $transaction->set($docRef, $translation + [
                'translated' => $result['text'],
                'originalLang' => $result['source'],
            ]);
        }
    );

Compila e implementa el backend de Cloud Run

  • Usa el siguiente comando para compilar la app de Cloud Run en el directorio backend:

    gcloud builds submit backend/ \
      --tag gcr.io/PROJECT_ID/background-function
  • Usa el siguiente comando para implementar la app de Cloud Run con la etiqueta de imagen del paso anterior:

    gcloud run deploy background-processing-function --platform managed \
      --image gcr.io/PROJECT_ID/background-function --region REGION

    REGION es una región de Google Cloud.

  • Cuando termine la implementación, el resultado del comando incluirá una URL correspondiente a la app implementada. Por ejemplo:

    Service [background-processing-function] revision [default-00002-vav] has been deployed and is serving 100 percent of traffic at https://default-c457u4v2ma-uc.a.run.app

    Copia esta URL para el siguiente paso.

Configura la suscripción de Pub/Sub

Tu app de Cloud Run recibirá mensajes de Pub/Sub cada vez que se publique un mensaje en el tema translate.

Una verificación de autenticación integrada garantiza que el mensaje de Pub/Sub contenga un token de autorización válido de una cuenta de servicio que tenga permiso para invocar el backend de Cloud Run.

Los siguientes pasos te guiarán para configurar el tema de Pub/Sub, la suscripción y la cuenta de servicio a fin de realizar llamadas autenticadas a tu backend de Cloud Run. Obtén más detalles sobre esta integración en el artículo sobre la autenticación de servicio a servicio.

  1. Usa este comando para crear el tema translate, en el que se publicarán las nuevas solicitudes de traducción:

    gcloud pubsub topics create translate
    
  2. Usa este comando para permitir que tu proyecto genere tokens de autenticación de Pub/Sub:

    gcloud projects add-iam-policy-binding PROJECT_ID \
         --member=serviceAccount:service-PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com \
         --role=roles/iam.serviceAccountTokenCreator

    PROJECT_NUMBER es el número de tu proyecto de Google Cloud, que se puede encontrar con el comando gcloud projects describe PROJECT_ID | grep projectNumber.

  3. Crea o selecciona una cuenta de servicio para representar la identidad de suscripción de Pub/Sub.

    gcloud iam service-accounts create cloud-run-pubsub-invoker \
         --display-name "Cloud Run Pub/Sub Invoker"

    Nota: Puedes usar cloud-run-pubsub-invoker o reemplazarlo por un nombre que sea único en el proyecto de Google Cloud.

  4. Usa este comando para permitir que la cuenta de servicio invoque el servicio background-processing-function:

    gcloud run services add-iam-policy-binding background-processing-function \
       --member=serviceAccount:cloud-run-pubsub-invoker@PROJECT_ID.iam.gserviceaccount.com \
       --role=roles/run.invoker  --platform managed --region REGION

    Pueden transcurrir varios minutos hasta que se propaguen los cambios de la administración de identidades y accesos. Mientras tanto, puede que veas errores HTTP 403 en los registros del servicio.

  5. Crea una suscripción a Pub/Sub con la cuenta de servicio:

    gcloud pubsub subscriptions create run-translate-string --topic translate \
       --push-endpoint=CLOUD_RUN_URL \
       --push-auth-service-account=cloud-run-pubsub-invoker@PROJECT_ID.iam.gserviceaccount.com

    CLOUD_RUN_URL es la URL HTTPS que copiaste después de compilar e implementar tu backend.

    La marca --push-account-service-account activa las funciones de envío de Pub/Sub para la autenticación y la autorización.

    Tu dominio del servicio de Cloud Run se registra automáticamente para usarlo con las suscripciones de Pub/Sub.

Conoce la app

La app web tiene dos componentes principales:

  • Un servidor HTTP de PHP para controlar las solicitudes web. El servidor tiene los siguientes dos extremos:
    • /: Enumera todas las traducciones existentes y un formulario que los usuarios pueden enviar para solicitar traducciones nuevas.
    • /request-translation: Los formularios se envían a este extremo, que publica la solicitud en Pub/Sub para que se traduzca de forma asíncrona.
  • Una plantilla HTML que completa el servidor de PHP con las traducciones existentes

El servidor HTTP

  • En el directorio app, index.php comienza con la configuración de la app de Lumen y el registro de los controladores HTTP:

    $app = new Laravel\Lumen\Application(__DIR__);
    $app->router->group([
    ], function ($router) {
        require __DIR__ . '/routes/web.php';
    });
    $app->run();
  • El controlador de índice (/) obtiene todas las traducciones existentes de Firestore y procesa una plantilla con la lista:

    /**
     * Homepage listing all requested translations and their results.
     */
    $router->get('/', function (Request $request) use ($projectId) {
        $firestore = new FirestoreClient([
            'projectId' => $projectId,
        ]);
        $translations = $firestore->collection('translations')->documents();
        return view('home', ['translations' => $translations]);
    });
  • El controlador de solicitudes de traducción, registrado en /request-translation, analiza el contenido que se envió mediante el formulario HTML, valida la solicitud y publica un mensaje en Pub/Sub:

    /**
     * Endpoint which publishes a PubSub request for a new translation.
     */
    $router->post('/request-translation', function (Request $request) use ($projectId) {
        $acceptableLanguages = ['de', 'en', 'es', 'fr', 'ja', 'sw'];
        if (!in_array($lang = $request->get('lang'), $acceptableLanguages)) {
            throw new Exception('Unsupported Language: ' . $lang);
        }
        if (!$text = $request->get('v')) {
            throw new Exception('No text to translate');
        }
        $pubsub = new PubSubClient([
            'projectId' => $projectId,
        ]);
        $topic = $pubsub->topic('translate');
        $topic->publish(['data' => json_encode([
            'language' => $lang,
            'text' => $text,
        ])]);
    
        return '';
    });

La plantilla HTML

La plantilla HTML es la base de la página HTML que se muestra al usuario para que pueda ver las traducciones anteriores y solicitar otras nuevas. El servidor HTTP completa la plantilla con la lista de las traducciones existentes.

  • El elemento <head> de la plantilla HTML incluye metadatos, hojas de estilo y JavaScript para la página:

    La página extrae elementos de JavaScript y CSS de Material Design Lite (MDL). MDL te permite dar un estilo de Material Design a los sitios web.

    En la página, se usa JQuery para establecer un controlador de envío de formularios una vez que el documento termina de cargarse. Cuando se envía el formulario de solicitud de traducción, la página realiza una validación mínima para comprobar que el valor no esté vacío y envía una solicitud asíncrona al extremo /request-translation.

    Por último, aparece una barra de notificaciones de MDL para indicar si la solicitud se realizó correctamente o si se produjo un error.

  • El cuerpo HTML de la página usa un diseño de MDL y varios componentes de MDL para mostrar una lista de traducciones y un formulario para solicitar traducciones adicionales:
    <body>
      <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
        <header class="mdl-layout__header">
          <div class="mdl-layout__header-row">
            <!-- Title -->
            <span class="mdl-layout-title">Translate with Background Processing</span>
          </div>
        </header>
        <main class="mdl-layout__content">
          <div class="page-content">
            <div class="mdl-grid">
              <div class="mdl-cell mdl-cell--1-col"></div>
              <div class="mdl-cell mdl-cell--3-col">
                <form id="translate-form" class="translate-form">
                  <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                    <input class="mdl-textfield__input" type="text" id="v" name="v">
                    <label class="mdl-textfield__label" for="v">Text to translate...</label>
                  </div>
                  <select class="mdl-textfield__input lang" name="lang">
                    <option value="de">de</option>
                    <option value="en">en</option>
                    <option value="es">es</option>
                    <option value="fr">fr</option>
                    <option value="ja">ja</option>
                    <option value="sw">sw</option>
                  </select>
                  <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent" type="submit"
                      name="submit">Submit</button>
                </form>
              </div>
              <div class="mdl-cell mdl-cell--8-col">
                <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
                  <thead>
                    <tr>
                      <th class="mdl-data-table__cell--non-numeric"><strong>Original</strong></th>
                      <th class="mdl-data-table__cell--non-numeric"><strong>Translation</strong></th>
                    </tr>
                  </thead>
                  <tbody>
                  <?php foreach ($translations as $translation): ?>
                    <tr>
                      <td class="mdl-data-table__cell--non-numeric">
                        <span class="mdl-chip mdl-color--primary">
                          <span class="mdl-chip__text mdl-color-text--white"><?= $translation['originalLang'] ?></span>
                        </span>
                      <?= $translation['original'] ?>
                      </td>
                      <td class="mdl-data-table__cell--non-numeric">
                        <span class="mdl-chip mdl-color--accent">
                          <span class="mdl-chip__text mdl-color-text--white"><?= $translation['lang'] ?></span>
                        </span>
                        <?= $translation['translated'] ?>
                      </td>
                    </tr>
                  <?php endforeach ?>
                  </tbody>
                </table>
                <br/>
                <button class="mdl-button mdl-js-button mdl-button--raised" type="button" onClick="window.location.reload();">Refresh</button>
              </div>
            </div>
          </div>
          <div aria-live="assertive" aria-atomic="true" aria-relevant="text" class="mdl-snackbar mdl-js-snackbar" id="snackbar">
            <div class="mdl-snackbar__text mdl-color-text--black"></div>
            <button type="button" class="mdl-snackbar__action"></button>
          </div>
        </main>
      </div>
    </body>
    </html>
    

Ejecuta la app en Cloud Shell

Antes de intentar implementar la app web, instala las dependencias y ejecútalas de forma local.

  1. Primero, instala las dependencias con Composer. La extensión de gRPC para PHP es obligatoria y viene preinstalada en Cloud Shell.

    composer install -d app
    
  2. A continuación, ejecuta el servidor web PHP integrado para entregar tu app:

    APP_DEBUG=true php -S localhost:8080 -t app
    

    La marca APP_DEBUG=true hace que se muestren las excepciones que se produzcan.

  3. En Cloud Shell, haz clic en Vista previa web y selecciona Obtener vista previa en el puerto 8080. Se abrirá una ventana nueva con tu app en ejecución.

Implementa la app web

Puedes usar el entorno estándar de App Engine para compilar y, también, implementar una app que se ejecute de manera confiable incluso con cargas pesadas y grandes cantidades de datos.

En este instructivo, se usa el entorno estándar de App Engine para implementar el frontend de HTTP.

El app.yaml configura la app de App Engine:

runtime: php73

env_variables:
  APP_DEBUG: true
  LOG_CHANNEL: stderr
  APP_STORAGE: /tmp
  • Desde el mismo directorio que el archivo app.yaml, implementa tu app en el entorno estándar de App Engine:
    gcloud app deploy

Prueba la app

Después de implementar la Cloud Function y la app de App Engine, solicita una traducción.

  1. Para ver la app en tu navegador,ingresa la siguiente URL:

    https://PROJECT_ID.REGION_ID.r.appspot.com

    Reemplaza lo siguiente:

    Hay una página con una lista vacía de traducciones y un formulario para solicitar traducciones nuevas.

  2. En el campo Texto para traducir, ingresa el texto que desees traducir, por ejemplo, Hello, World.
  3. Selecciona el idioma de la lista desplegable al que quieras traducir el texto.
  4. Haz clic en Enviar.
  5. Para actualizar la página, haz clic en Actualizar . Aparece una fila nueva en la lista de traducciones. Si no ves la traducción, espera unos segundos y vuelve a intentarlo. Si la traducción sigue sin aparecer, consulta la siguiente sección sobre cómo depurar la app.

Depura la app

Si no puedes conectarte a la aplicación de App Engine o no ves las traducciones nuevas, verifica lo siguiente:

  1. Comprueba que los comandos de implementación de gcloud se completaron correctamente y no generaron ningún error. Si se produjeron errores (por ejemplo, message=Build failed), corrígelos y, luego, intenta compilar e implementar la app de Cloud Run, además de implementar nuevamente la aplicación de App Engine.
  2. En la consola de Google Cloud, ve a la página Explorador de registros.

    Ir a la página Explorador de registros

    1. En la lista desplegable Recursos seleccionados recientemente, haz clic en Aplicación de GAE y, luego, en Todos los module_id. Verás una lista de las solicitudes de cuando visitaste tu aplicación. Si no ves una lista de solicitudes, confirma que seleccionaste Todos module_id en la lista desplegable. Si ves mensajes de error impresos en la consola de Google Cloud, verifica que el código de la app coincida con el código de la sección para comprender la app web.
    2. En la lista desplegable Recursos seleccionados recientemente, haz clic en Revisión de Cloud Run y, luego, en Todos los registros. Deberías ver una solicitud POST enviada a la URL de tu app implementada. Si no es así, comprueba que la app de Cloud Run y App Engine usen el mismo tema de Pub/Sub y que exista una suscripción de Pub/Sub que envíe datos a tu extremo de Cloud Run.

Limpia

Para evitar que se apliquen cargos a tu cuenta de Google Cloud por los recursos usados en este instructivo, borra el proyecto que contiene los recursos o conserva el proyecto y borra los recursos individuales.

Borra el proyecto de Google Cloud

  1. En la consola de Google Cloud, ve a la página Administrar recursos.

    Ir a Administrar recursos

  2. En la lista de proyectos, elige el proyecto que quieres borrar y haz clic en Borrar.
  3. En el diálogo, escribe el ID del proyecto y, luego, haz clic en Cerrar para borrar el proyecto.

Borra los recursos del instructivo

  1. Usa este comando para borrar la app de App Engine que creaste en este instructivo:

    1. En la consola de Google Cloud, ve a la página Versiones de App Engine.

      Ir a Versiones

    2. Selecciona la casilla de verificación de la versión no predeterminada de la app que deseas borrar.
    3. Para borrar la versión de la app, haz clic en Borrar.

  2. Usa este comando para borrar el servicio de Cloud Run que implementaste en este instructivo:

    gcloud run services delete background-processing-function

    También puedes borrar los servicios de Cloud Run desde la consola de Google Cloud.

  3. Borra otros recursos de Google Cloud que creaste en este instructivo:

¿Qué sigue?