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 do App Engine".

    Acessar a página Versões

  2. Clique na caixa de seleção ao lado da versão do aplicativo não padrão que você quer excluir.
  3. Clique no botão Excluir na parte superior da página 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 "Projetos".

    Acessar a página Projetos

  2. Na lista de projetos, selecione um e clique em Excluir projeto.
  3. Na caixa de diálogo, digite o código 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 do App Engine".

    Acessar a página Versões

  2. Clique na caixa de seleção ao lado da versão do aplicativo não padrão que você quer excluir.
  3. Clique no botão Excluir na parte superior da página 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 de SQL"

  2. Clique no nome a instância de SQL que você quer excluir.
  3. Para excluir a instância, clique em Excluir excluir no topo da página.

Excluir o intervalo do Google Cloud Storage

Para excluir um intervalo do Google Cloud Storage:

  1. No Console do GCP, acesse o navegador do Cloud Storage.

    Acessar o navegador do Cloud Storage

  2. Marque a caixa de seleção ao lado do intervalo que você quer excluir.
  3. Para excluir o intervalo, clique no botão Excluir no topo da página.

A seguir

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

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

Esta página foi útil? Conte sua opinião sobre:

Enviar comentários sobre…