Processamento em segundo plano com PHP


Para muitos aplicativos, é necessário fazer o processamento em segundo plano fora do contexto de uma solicitação da Web. Este tutorial cria um app da Web que permite aos usuários inserir texto para traduzir e, em seguida, exibe uma lista de traduções anteriores. A tradução é feita em um processo em segundo plano para evitar o bloqueio da solicitação do usuário.

O diagrama a seguir ilustra o processo de solicitação da tradução.

Diagrama da arquitetura.

Aqui está a sequência de eventos sobre como o tutorial do app funciona:

  1. Acesse a página da Web para ver uma lista de traduções anteriores armazenadas no Firestore.
  2. Solicite uma tradução de texto inserindo um formulário HTML.
  3. A solicitação de tradução é publicada no Pub/Sub.
  4. Um aplicativo do Cloud Run recebe a mensagem do Pub/Sub.
  5. O aplicativo do Cloud Run usa o Cloud Translation para traduzir o texto.
  6. O aplicativo Cloud Run armazena o resultado no Firestore.

Este tutorial se destina a qualquer pessoa interessada em aprender sobre o processamento em segundo plano com o Google Cloud. Nenhuma experiência anterior é necessária com o Pub/Sub, o Firestore, o App Engine ou o Cloud Run. No entanto, para entender todo o código, é útil ter experiência com PHP, JavaScript e HTML.

Objetivos

  • Entenda e implante os serviços do Cloud Run.
  • Entenda e implante um aplicativo do App Engine.
  • Testar o app.

Custos

Neste documento, você usará os seguintes componentes faturáveis do Google Cloud:

Para gerar uma estimativa de custo baseada na projeção de uso deste tutorial, use a calculadora de preços. Novos usuários do Google Cloud podem estar qualificados para uma avaliação gratuita.

Ao concluir as tarefas descritas neste documento, é possível evitar o faturamento contínuo excluindo os recursos criados. Saiba mais em Limpeza.

Antes de começar

  1. Faça login na sua conta do Google Cloud. Se você começou a usar o Google Cloud agora, crie uma conta para avaliar o desempenho de nossos produtos em situações reais. Clientes novos também recebem US$ 300 em créditos para executar, testar e implantar cargas de trabalho.
  2. No console do Google Cloud, na página do seletor de projetos, selecione ou crie um projeto do Google Cloud.

    Acessar o seletor de projetos

  3. Verifique se a cobrança está ativada para o seu projeto do Google Cloud.

  4. Ative as APIs Firestore, Cloud Run, Pub/Sub, and Cloud Translation.

    Ative as APIs

  5. No console do Google Cloud, na página do seletor de projetos, selecione ou crie um projeto do Google Cloud.

    Acessar o seletor de projetos

  6. Verifique se a cobrança está ativada para o seu projeto do Google Cloud.

  7. Ative as APIs Firestore, Cloud Run, Pub/Sub, and Cloud Translation.

    Ative as APIs

  8. No console do Google Cloud, abra o app no Cloud Shell.

    Acessar o Cloud Shell

    O Cloud Shell oferece acesso por linha de comando aos seus recursos de nuvem diretamente no navegador. Abra o Cloud Shell no navegador e clique em Continuar para fazer o download do código de amostra e carregá-lo no diretório de app.

  9. No Cloud Shell, configure a ferramenta gcloud para usar seu projeto do Google Cloud:
    # Configure gcloud for your project
    gcloud config set project YOUR_PROJECT_ID

Como entender o back-end do Cloud Run

Você define uma única função PHP translateString e configura seu serviço do Cloud Run para responder a uma mensagem do Pub/Sub ao invocar esta função.

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. A função precisa importar várias dependências para se conectar ao Firestore e à Translation.

    use Google\Cloud\Firestore\FirestoreClient;
    use Google\Cloud\Firestore\Transaction;
    use Google\Cloud\Translate\TranslateClient;
    
  2. O Cloud Run começa inicializando os clientes do Firestore e do Pub/Sub. Em seguida, ele analisa os dados da mensagem do Pub/Sub para que o texto seja traduzido para o idioma de destino escolhido.

    $firestore = new FirestoreClient();
    $translate = new TranslateClient();
    
    $translation = [
        'original' => $data['text'],
        'lang' => $data['language'],
    ];
  3. A API Translation é usada para traduzir a string para o idioma escolhido.

    $result = $translate->translate($translation['original'], [
        'target' => $translation['lang'],
    ]);
  4. A função cria um nome exclusivo para a solicitação de tradução a fim de garantir que não armazenemos nenhuma tradução duplicada. Em seguida, a tradução é feita em uma transação do Firestore para garantir que execuções simultâneas não executem acidentalmente a mesma tradução duas vezes.

    $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'],
            ]);
        }
    );

Como criar e implantar o back-end do Cloud Run

  • Crie o aplicativo Cloud Run no diretório backend:

    gcloud builds submit backend/ \
      --tag gcr.io/PROJECT_ID/background-function
  • Implante o app do Cloud Run usando a tag de imagem da etapa anterior:

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

    Em que REGION é uma região do Google Cloud.

  • Quando a implantação terminar, você verá um URL para o app implantado na resposta ao comando. Exemplo:

    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

    Copie esse URL para a próxima etapa.

Como configurar a assinatura do editor

Seu app do Cloud Run receberá mensagens do Pub/Sub sempre que uma mensagem for publicada no tópico translate.

Uma verificação de autenticação integrada garante que a mensagem do Pub/Sub contenha um token de autorização válido de uma conta de serviço com permissão para invocar o back-end do Cloud Run.

As próximas etapas mostram como configurar o tópico do Pub/Sub, a assinatura e a conta de serviço para fazer chamadas autenticadas ao seu back-end do Cloud Run. Saiba mais sobre essa integração em Autenticação serviço a serviço.

  1. Crie o tópico translate para publicar novas solicitações de tradução:

    gcloud pubsub topics create translate
    
  2. Ative seu projeto para criar tokens de autenticação do 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

    Em que PROJECT_NUMBER é o número do projeto do Google Cloud, que pode ser encontrado ao executar gcloud projects describe PROJECT_ID | grep projectNumber.

  3. Crie ou selecione uma conta de serviço para representar a identidade da assinatura do Pub/Sub.

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

    Observação: você pode usar cloud-run-pubsub-invoker ou substituir por um nome exclusivo no projeto do Google Cloud.

  4. Conceda permissão à conta de serviço do invocador para invocar seu serviço 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

    Pode levar vários minutos para que as alterações do Gerenciamento de identidade e acesso sejam propagadas. Enquanto isso, é possível ver erros HTTP 403 nos registros de serviço.

  5. Crie uma assinatura do Pub/Sub com a conta de serviço:

    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

    Em que CLOUD_RUN_URL é o URL de HTTPS que você copiou depois da etapa sobre como criar e implantar seu back-end.

    A sinalização --push-account-service-account ativa a funcionalidade de push do Pub/Sub para Autenticação e autorização.

    Seu domínio do serviço do Cloud Run é registrado automaticamente para uso com assinaturas do Pub/Sub.

Noções básicas sobre o app

Há dois componentes principais para o app da Web:

  • Um servidor HTTP PHP para gerenciar solicitações da web. O servidor tem os dois endpoints a seguir:
    • /: lista todas as traduções existentes e mostra um formulário que os usuários podem enviar para solicitar novas traduções.
    • /request-translation: os envios de formulários são enviados para esse endpoint, que publica a solicitação para que o Pub/Sub seja traduzido de forma assíncrona.
  • Um modelo HTML preenchido com as traduções existentes pelo servidor PHP.

O servidor HTTP

  • No diretório app, index.php começa configurando Lumen e registrando os gerenciadores HTTP:

    $app = new Laravel\Lumen\Application(__DIR__);
    $app->router->group([
    ], function ($router) {
        require __DIR__ . '/routes/web.php';
    });
    $app->run();
  • O gerenciador de índice (/) recebe todas as traduções existentes do Firestore e processa um modelo com a 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]);
    });
  • O gerenciador de solicitações de tradução, registrado em /request-translation, analisa o envio do formulário HTML, valida a solicitação e publica uma mensagem no 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 '';
    });

O modelo HTML

O modelo HTML é a base da página HTML exibida ao usuário para que ele possa ver as traduções anteriores e solicitar novas. O modelo é preenchido pelo servidor HTTP com a lista de traduções existentes.

  • O elemento <head> do modelo HTML inclui metadados, folhas de estilo e JavaScript para a página:

    A página extrai recursos Material Design Lite (MDL) CSS e JavaScript. A MDL permite que você adicione uma aparência Material Design aos seus sites.

    A página usa JQuery para aguardar o carregamento do documento e definir um gerenciador de envio de formulário. Sempre que o formulário de solicitação de tradução é enviado, a página faz uma validação de formulário mínima para verificar se o valor não está vazio e envia uma solicitação assíncrona ao endpoint /request-translation.

    Por fim, uma snackbar MDL indica se a solicitação foi bem-sucedida ou se encontrou um erro.

  • O corpo HTML da página usa um layout de MDL e vários componentes de MDL para exibir uma lista de traduções e um formulário para solicitar traduções adicionais:
    <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>
    

Como executar o app no Cloud Shell

Antes de tentar implantar o app da Web, instale as dependências e execute-as localmente.

  1. Primeiro, instale as dependências com o Compositor. A extensão gRPC para PHP é obrigatória e está pré-instalada no Cloud Shell.

    composer install -d app
    
  2. Em seguida, execute o servidor da Web integrado ao PHP para veicular seu aplicativo:

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

    A sinalização APP_DEBUG=true exibirá todas as exceções que ocorrerem.

  3. No Cloud Shell, clique em Visualização da Web e selecione Visualizar na porta 8080. Uma nova janela será aberta com o app em execução.

Como implantar o app da Web

Use o ambiente padrão do App Engine para criar e implantar um aplicativo que será executado de maneira confiável, sob grande carga e com grandes quantidades de dados.

Este tutorial usa o ambiente padrão do App Engine para implantar o front-end HTTP.

O app.yaml configura o aplicativo do App Engine:

runtime: php73

env_variables:
  APP_DEBUG: true
  LOG_CHANNEL: stderr
  APP_STORAGE: /tmp
  • No mesmo diretório que o arquivo app.yaml, implante o aplicativo no ambiente padrão do App Engine:
    gcloud app deploy

Como testar o app

Depois de implantar o aplicativo do App Engine e da função do Cloud, tente solicitar uma tradução.

  1. Para abrir o app no navegador,digite este URL:

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

    Substitua:

    Há uma página com uma lista vazia de traduções e um formulário para solicitar novas traduções.

  2. No campo Texto para tradução, digite algum texto a ser traduzido, por exemplo, Hello, World.
  3. Na lista suspensa, selecione um idioma para o qual você quer traduzir o texto.
  4. Clique em Enviar.
  5. Para atualizar a página, clique em Atualizar . Há uma nova linha na lista de tradução. Se você não ver uma tradução, aguarde mais alguns segundos e tente novamente. Caso você ainda não veja uma tradução, consulte a próxima seção sobre como depurar o app.

Como depurar o aplicativo

Se não foi possível conectar ao seu aplicativo do App Engine, ou caso você não veja novas traduções, verifique o seguinte:

  1. Verifique se os comandos de implantação gcloud foram concluídos com êxito e não geraram erros. Se houver erros (por exemplo, message=Build failed), corrija-os e tente criar e implantar o app do Cloud Run e implantar o aplicativo do App Engine novamente.
  2. No console do Google Cloud, acesse a página do Explorador de registros.

    Acessar a página da Análise de registros

    1. Na lista suspensa Recursos selecionados recentemente, clique em Aplicativo do GAEe, em seguida, selecione Todos os module_id. Você verá uma lista de solicitações de quando visitou seu aplicativo. Caso contrário, verifique se você selecionou Todos os module_id na lista suspensa. Se você vir mensagens de erro impressas no console do Google Cloud, verifique se o código do aplicativo corresponde ao código na seção sobre como entender o app da Web.
    2. Na lista suspensa Recursos selecionados recentemente, clique em Cloud Run Revision e, em seguida, selecione Todos os registros. Você verá uma solicitação POST enviada ao URL do app implantado. Caso contrário, verifique se o Cloud Run e o aplicativo do App Engine estão usando o mesmo tópico do Pub/Sub, e se existe uma assinatura do Pub/Sub para enviar ao endpoint do Cloud Run.

Limpar

Para evitar cobranças na sua conta do Google Cloud pelos recursos usados no tutorial, exclua o projeto que os contém ou mantenha o projeto e exclua os recursos individuais.

Excluir o projeto do Google Cloud

  1. No Console do Google Cloud, acesse a página Gerenciar recursos.

    Acessar "Gerenciar recursos"

  2. Na lista de projetos, selecione o projeto que você quer excluir e clique em Excluir .
  3. Na caixa de diálogo, digite o ID do projeto e clique em Encerrar para excluí-lo.

Exclua os recursos do tutorial

  1. Exclua o aplicativo do App Engine que você criou neste tutorial:

    1. No Console do Google Cloud, acesse a página Versões do App Engine.

      Acessar "Versões"

    2. Marque a caixa de seleção da versão não padrão do app que você quer excluir.
    3. Para excluir a versão do app, clique em Excluir.

  2. Exclua o serviço do Cloud Run que você implantou neste tutorial:

    gcloud run services delete background-processing-function

    Também é possível excluir os serviços do Cloud Run no Console do Google Cloud.

  3. Exclua outros recursos do Google Cloud criados neste tutorial:

A seguir