Como usar o Cloud Pub/Sub com Ruby

Para muitos aplicativos, é necessário fazer o processamento em segundo plano fora do contexto de uma solicitação da Web. Neste exemplo, tarefas são enviadas pelo app Bookshelf para serem executadas por um worker separado em segundo plano. O worker coleta as informações da API Google Books e atualiza as informações do livro no banco de dados. Veja neste exemplo como configurar serviços separados no App Engine, executar o processamento de um worker no ambiente flexível do App Engine e processar eventos de ciclo de vida.

Esta página faz parte de um tutorial com várias páginas. Para começar do início e ver as instruções de configuração, consulte Aplicativo Bookshelf em Ruby.

Como instalar dependências

Acesse o diretório getting-started-ruby/6-task-queueing e digite o comando a seguir.

bundle install

Como criar um tópico e uma assinatura do Cloud Pub/Sub

O aplicativo Bookshelf usa o Cloud Pub/Sub para uma fila de processamento de solicitações em segundo plano a fim de receber dados da API Google Books sobre um livro adicionado ao Bookshelf.

  1. Crie um novo tópico do Cloud Pub/Sub com o seguinte comando do SDK do Cloud, em que [YOUR_PUBSUB_TOPIC] representa o nome do tópico do Cloud Pub/Sub.

    gcloud pubsub topics create [YOUR_PUBSUB_TOPIC]
    
  2. Crie uma nova assinatura do Cloud Pub/Sub para o tópico criado na etapa anterior. Substitua [YOUR_PUBSUB_SUBSCRIPTION] pelo nome que você quer dar a esta nova assinatura do Cloud Pub/Sub.

    gcloud pubsub subscriptions create --topic [YOUR_PUBSUB_TOPIC] [YOUR_PUBSUB_SUBSCRIPTION]
    

Como definir as configurações

  1. Copie o arquivo de configurações de exemplo.

    cp config/settings.example.yml config/settings.yml
    
  2. Abra o arquivo settings.yml para edição. Substitua [YOUR_PROJECT_ID] pelo código do projeto do Google Cloud Platform.

    default: &default
      project_id: [YOUR_PROJECT_ID]
      gcs_bucket: [YOUR_BUCKET_NAME]
      pubsub_topic: [YOUR_PUBSUB_TOPIC]
      pubsub_subscription: [YOUR_PUBSUB_SUBSCRIPTION]
      oauth2:
        client_id: [YOUR_CLIENT_ID]
        client_secret: [YOUR_CLIENT_SECRET]
  3. Defina as outras variáveis para os mesmos valores que você usou na parte Como autenticar usuários deste tutorial.

    Por exemplo, suponha que o ID do cliente do aplicativo da Web seja XYZCLIENTID e a chave secreta do cliente seja XYZCLIENTSECRET. Suponha também que o nome do projeto seja my-project e o nome do intervalo do Cloud Storage seja my-bucket. Então, a seção padrão do seu arquivo settings.yml ficaria assim:

    default: &default
      project_id: my-project
      gcs_bucket: my-bucket
      pubsub_topic: your-pubsub-topic
      pubsub_subscription: your-pubsub-subscription
      oauth2:
        client_id: XYZCLIENTID
        client_secret: XYZCLIENTSECRET
    
  4. Copie o arquivo de exemplo de configuração do banco de dados.

    cp config/database.example.yml config/database.yml
    
  5. Configure o aplicativo de amostra para usar o mesmo banco de dados que você configurou durante a parte Como usar dados estruturados deste tutorial.

    Cloud SQL

    • Edite o arquivo database.yml. Remova os comentários das linhas da parte do Cloud SQL no arquivo.

       mysql_settings: &mysql_settings
         adapter: mysql2
         encoding: utf8
         pool: 5
         timeout: 5000
         username: [MYSQL_USER]
         password: [MYSQL_PASS]
         database: [MYSQL_DATABASE]
         socket: /cloudsql/[YOUR_INSTANCE_CONNECTION_NAME]
      
      • Substitua [MYSQL_USER] e [MYSQL_PASS] pelo nome de usuário e pela senha da instância do Cloud SQL criados anteriormente.

      • Substitua [MYSQL_DATABASE] pelo nome do banco de dados criado anteriormente.

      • Substitua [YOUR_INSTANCE_CONNECTION_NAME] pelo Instance Connection Name da instância do Cloud SQL.

    • Execute as migrações.

      bundle exec rake db:migrate
      

    PostgreSQL

    • Edite o arquivo database.yml. Remova os comentários das linhas da parte do PostgreSQL no arquivo. Substitua os marcadores your-postgresql-* pelos valores do banco de dados e da instância do PostgreSQL. Por exemplo, suponha que seu endereço IPv4 seja 173.194.230.44, seu nome de usuário seja postgres, sua senha seja pword123 e o nome do seu banco de dados seja bookshelf. Então, a parte do PostgreSQL do seu arquivo database.yml seria assim:

      # PostgreSQL Sample Database Configuration
      # ----------------------------------------
        adapter: postgresql
        encoding: unicode
        pool: 5
        username: postgres
        password: pword123
        host: 173.194.230.44
        database: bookshelf
      
    • Crie as tabelas e o banco de dados necessários.

      bundle exec rake db:create
      bundle exec rake db:migrate
      

    Cloud Datastore

    • Edite o arquivo database.yml. Remova os comentários da linha na parte do Cloud Datastore no arquivo. Substitua your-project-id pelo código do projeto do Google Cloud Platform. Por exemplo, se o código do projeto for my-project, a parte do Cloud Datastore do arquivo database.yml será assim:

      # Google Cloud Datastore Sample Database Configuration
      # ----------------------------------------------------
      dataset_id: my-project
      
    • Execute uma tarefa "rake" para copiar os arquivos do projeto de amostra para o Cloud Datastore.

      bundle exec rake backend:datastore
      

Como executar o app na máquina local

  1. Inicie o servidor da Web local e dois processos de worker:

    bundle exec foreman start --formation web=1,worker=2
    
  2. No navegador da Web, digite http://localhost:8080.

Agora, adicione alguns livros ao Bookshelf. Veja os workers atualizando as informações dos livros em segundo plano.

O RubyGem da Foreman inicia o servidor da Web do Rails e executa dois processos de worker.

O worker cria uma assinatura do Cloud Pub/Sub para detectar eventos. Após a criação da assinatura, os eventos publicados no tópico são colocados em fila, mesmo que não haja nenhum worker detectando eventos. Quando um worker fica on-line, o Cloud Pub/Sub entrega todos os eventos enfileirados.

Quando estiver pronto para avançar, pressione Ctrl+C para sair do servidor da Web local e dos processos de worker.

Como implantar o app no ambiente flexível do App Engine

  1. Compile os recursos do JavaScript para produção.

    RAILS_ENV=production bundle exec rake assets:precompile
    
  2. Implante o worker.

    gcloud app deploy worker.yaml
    
  3. Implante o app de amostra.

    gcloud app deploy
    
  4. No navegador da Web, digite o endereço a seguir.

    https://[YOUR_PROJECT_ID].appspot.com
    

Atualize o app e implante a versão atualizada com o mesmo comando usado para implantá-lo pela primeira vez. A implantação cria uma nova versão do app e a define como padrão. As versões mais antigas são mantidas, bem como as instâncias de VM associadas. Lembre-se de que essas versões do aplicativo e instâncias de VM são recursos passíveis de cobrança.

Reduza os custos excluindo as versões não padrão do app.

Para excluir uma versão do app:

  1. No Console do GCP, acesse a página Versões para o App Engine.

    Acessar a página "Versões"

  2. Marque a caixa de seleção da versão não padrão do aplicativo que você quer excluir.
  3. Clique em Excluir para excluir a versão do aplicativo.

Para informações detalhadas sobre a remoção de recursos faturáveis, consulte a seção Como fazer a limpeza na etapa final deste tutorial.

Estrutura do app

Este diagrama mostra os componentes do app e como se encaixam.

Exemplo de estrutura de autenticação

Noções básicas sobre o código

Esta seção analisa o código do aplicativo e explica como ele funciona.

Tarefas de fila

Para coletar informações da API Google Books referentes aos livros adicionados ao Bookshelf, a classe Book adiciona uma tarefa à fila.

after_create :lookup_book_details

def lookup_book_details
  if [author, description, published_on, image_url].any? {|attr| attr.blank? }
    LookupBookDetailsJob.perform_later self
  end
end

O código acima cria um callback do Active Record (em inglês) e especifica que, depois que um livro é criado e salvo no banco de dados, lookup_book_details é chamado. Se o livro não contiver alguma informação, ele adicionará o job à fila e procurará os detalhes do livro.

LookupBookDetailsJob é um Active Job (em inglês).

O código passa o livro a ser atualizado, self, para LookupBookDetailsJob.perform_later. Isso adiciona um job para procurar os detalhes do livro na fila.

Back-end do Active Job do Cloud Pub/Sub

É possível configurar o Active Job para usar um back-end personalizado. Por exemplo, use delayed_job ou resque para adicionar tarefas à fila. O aplicativo de amostra Bookshelf tem o próprio back-end personalizado, que está especificado na classe Application.

config.active_job.queue_adapter = :pub_sub_queue

Um back-end do Active Job, também chamado de adaptador, precisa fornecer um método enqueue. Quando um job é enfileirado com perform_later, ele é passado para o método enqueue do back-end do Active Job configurado.

O aplicativo de amostra adiciona um job à fila por meio da criação de uma assinatura em um tópico do Cloud Pub/Sub e da publicação do ID de um livro no tópico. Com a assinatura, as mensagens são enfileiradas mesmo sem um worker para detecção. Quando um worker fica on-line, o Cloud Pub/Sub entrega todos os eventos enfileirados.

require "google/cloud/pubsub"

module ActiveJob
  module QueueAdapters
    class PubSubQueueAdapter

      def self.pubsub
        @pubsub ||= begin
          project_id = Rails.application.config.x.settings["project_id"]
          Google::Cloud::Pubsub.new project_id: project_id
        end
      end

      def self.pubsub_topic
        @pubsub_topic ||= Rails.application.config.x.settings["pubsub_topic"]
      end

      def self.pubsub_subscription
        @pubsub_subscription ||= Rails.application.config.x.settings["pubsub_subscription"]
      end

      def self.enqueue job
        Rails.logger.info "[PubSubQueueAdapter] enqueue job #{job.inspect}"

        book  = job.arguments.first

        topic = pubsub.topic pubsub_topic

        topic.publish book.id.to_s
      end

O código acima usa o RubyGem google-cloud-pubsub para interagir com o Cloud Pub/Sub. A biblioteca de cliente do Google Cloud é um cliente Ruby idiomático para interagir com os serviços do Google Cloud Platform (GCP).

gem "google-cloud-pubsub", "~> 0.30"

Para processar livros adicionados a uma fila, uma assinatura do Cloud Pub/Sub detecta mensagens publicadas no tópico lookup_book_details_queue. Veja mais detalhes na seção sobre o worker.

API Books

O aplicativo de amostra usa o cliente de APIs do Google RubyGem para procurar detalhes de livros na API Books.

gem "google-api-client", "~> 0.19"

Quando um job é executado, o método LookupBookDetailsJob.perform recupera uma lista de livros com base no título de um livro da API Books.

require "google/apis/books_v1"

class LookupBookDetailsJob < ActiveJob::Base
  queue_as :default

  def perform book
    Rails.logger.info "[BookService] Lookup details for book" +
                      "#{book.id} #{book.title.inspect}"

    # Create Book API Client
    book_service = Google::Apis::BooksV1::BooksService.new

    # Lookup a list of relevant books based on the provided book title.
    book_service.list_volumes(book.title, order_by: "relevance") do |results, error|
      # Error ocurred soft-failure
      if error
        Rails.logger.error "[BookService] #{error.inspect}"
        break
      end

      # Book was not found
      if results.total_items.zero?
        Rails.logger.info "[BookService] #{book.title} was not found."
        break
      end

      # List of relevant books
      volumes = results.items

Se o resultado de um volume de livro inclui um título, um autor e uma imagem da capa, esse resultado é selecionado como a melhor correspondência. Caso contrário, o primeiro resultado é utilizado.

# To provide the best results, find the first returned book that
# includes title and author information as well as a book cover image.
best_match = volumes.find {|volume|
  info = volume.volume_info
  info.title && info.authors && info.image_links.try(:thumbnail)
}

volume = best_match || volumes.first

Se algum volume relevante for encontrado, os detalhes do livro serão atualizados e salvos no banco de dados.

if volume
  info   = volume.volume_info
  images = info.image_links

  publication_date = info.published_date
  publication_date = "#{$1}-01-01" if publication_date =~ /^(\d{4})$/
  publication_date = Date.parse publication_date

  book.author       = info.authors.join(", ") unless book.author.present?
  book.published_on = publication_date unless book.published_on.present?
  book.description  = info.description unless book.description.present?
  book.image_url    = images.try(:thumbnail) unless book.image_url.
                                                         present?
  book.save
end

O trabalho

Um processo de worker gerencia jobs de busca de livros. Para iniciar o worker, é possível executar o comando a seguir, conforme especificado em Procfile.

bundle exec rake run_worker

A tarefa "rake" run_worker chama PubSubQueueAdapter para iniciar um worker.

desc "Run task queue worker"
task run_worker: :environment do
  ActiveJob::QueueAdapters::PubSubQueueAdapter.run_worker!
end

Quando o worker está em execução, ele detecta mensagens na assinatura do Pub/Sub para o tópico lookup_book_details_queue definido em seu arquivo config/settings.yml. Quando uma mensagem é recuperada, o livro associado é recuperado do banco de dados e o LookupBookDetailsJob é executado imediatamente para atualizar o livro.

def self.run_worker!
  Rails.logger.info "Running worker to lookup book details"

  topic        = pubsub.topic pubsub_topic
  subscription = topic.subscription pubsub_subscription

  subscriber = subscription.listen do |message|
    message.acknowledge!

    Rails.logger.info "Book lookup request (#{message.data})"

    book_id = message.data.to_i
    book    = Book.find_by_id book_id

    LookupBookDetailsJob.perform_now book if book
  end

  # Start background threads that will call block passed to listen.
  subscriber.start

  # Fade into a deep sleep as worker will run indefinitely
  sleep
end

Como executar no GCP

O worker é implantado como um módulo separado dentro do mesmo aplicativo. Os aplicativos do App Engine podem ter vários serviços independentes. Isso significa que é possível implantar, configurar, escalonar e atualizar de modo independente partes do seu aplicativo. O front-end é implantado no módulo padrão e o worker no módulo de worker.

Mesmo que o worker execute um aplicativo da Web ou não exiba nenhuma solicitação da Web a usuários, recomendamos enfaticamente que você providencie uma verificação de integridade HTTP ao realizar a execução no ambiente flexível do App Engine para garantir que o serviço seja executado e responda. No entanto, é possível desativar a verificação de integridade.

Para realizar uma verificação de integridade, o worker inicia dois processos em vez de um. O primeiro processo é o worker e o segundo é o health_check, que executa um aplicativo Rack simples para enviar respostas bem-sucedidas às solicitações HTTP referentes a verificações de integridade.

# Respond to HTTP requests with non-500 error code
run lambda {|env| [200, {"Content-Type" => "text/plain"}, ["ok"]] }

O aplicativo usa o Foreman para gerenciar vários processos. Os processos são configurados em Procfile.

web: bundle exec rackup -p 8080
worker: bundle exec rake run_worker
health_check: bundle exec rackup -p 8080 health_check.ru

Agora, o Foreman é usado como entrypoint do contêiner do Docker. Isso é especificado nos arquivos app.yaml e worker.yaml.

entrypoint: bundle exec foreman start --formation "$FORMATION"

Observe que o Procfile contém uma entrada para o front-end web para executar o aplicativo Bookshelf Rails também. Como os serviços padrão (front-end) e do worker compartilham a mesma base de código, a variável de ambiente FORMATION controla quais processos são iniciados. O diagrama a seguir compara a implantação de um módulo, à esquerda, com a implantação de diversos módulos, à direita.

Implantação do Cloud Pub/Sub

As variáveis de ambiente são definidas por app.yaml e worker.yaml.

env_variables:
  FORMATION: web=1

O worker é um módulo separado, portanto precisa de um arquivo de configuração YAML próprio.

env_variables:
  FORMATION: worker=5,health_check=1

Essa configuração é semelhante ao arquivo app.yaml usado para o front-end. As principais diferenças são a configuração module: worker e a variável de ambiente FORMATION, que configura o Foreman para executar cinco workers e o front-end para a verificação de integridade, em vez do aplicativo da Web Bookshelf.

Como fazer a limpeza

Para evitar que os recursos usados neste tutorial sejam cobrados na conta do Google Cloud Platform, é possível fazer o seguinte:

Excluir o projeto

O jeito mais fácil de evitar cobranças é excluindo o projeto que você criou para o tutorial.

Para excluir o projeto:

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

    Acessar a página 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.

Excluir versões não padrão do app

Se você não quer excluir seu projeto, pode reduzir custos excluindo versões não padrão do app.

Para excluir uma versão do aplicativo:

  1. No Console do GCP, acesse a página Versões para o App Engine.

    Acessar a página "Versões"

  2. Marque a caixa de seleção da versão não padrão do aplicativo que você quer excluir.
  3. Clique em Excluir para excluir a versão do aplicativo.

Excluir a instância do Cloud SQL

Para excluir uma instância do Cloud SQL:

  1. No Console do GCP, acesse a página Instâncias de SQL.

    Acessar a página "Instâncias do SQL"

  2. Clique no nome da instância a instância do SQL que você quer excluir.
  3. Clique em Excluir para excluir a instância.

Excluir o intervalo do Google Cloud Storage

Para excluir um intervalo do Google Cloud Storage:

  1. No Console do GCP, acesse a página navegador do Cloud Storage.

    Acesse a página "Navegador do Cloud Storage"

  2. Clique na caixa de seleção do intervalo que você quer excluir.
  3. Clique em Excluir para excluir o intervalo.

A seguir

Saiba como executar a amostra do Bookshelf em Ruby no Compute Engine.

Teste outros recursos do Google Cloud Platform. Veja nossos tutoriais.