Cómo usar Pub/Sub con Ruby

Muchas aplicaciones necesitan realizar procesamientos en segundo plano fuera del contexto de una solicitud web. En este caso, la aplicación de muestra de Bookshelf le envía tareas a un trabajador independiente en segundo plano para que las ejecute. El trabajador recibe información desde la API de Google Libros y actualiza la información de los libros en la base de datos. Este ejemplo demuestra 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 administrar los eventos del ciclo de vida.

Esta página forma parte de un instructivo de varias páginas. Para comenzar desde el principio y leer las instrucciones de configuración, ve a la app de Bookshelf para Ruby.

Cómo instalar dependencias

Ve al directorio getting-started-ruby/6-task-queueing y, luego, ingresa este comando:

bundle install

Cómo crear un tema y una suscripción de Cloud Pub/Sub

La aplicación de Bookshelf usa Cloud Pub/Sub para que una cola de solicitudes con procesamiento en segundo plano obtenga datos de la API de Google Libros a fin de agregar un libro a Bookshelf.

  1. Crea un tema de Cloud Pub/Sub nuevo con el siguiente comando de Cloud SDK:

    gcloud pubsub topics create [YOUR-PUBSUB-TOPIC]
    
  2. Crea una suscripción a Cloud Pub/Sub nueva para el tema que creaste en el paso anterior, con el siguiente comando:

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

Configuraciones

  1. Copia el archivo de configuración de ejemplo:

    cp config/settings.example.yml config/settings.yml
    
  2. Abre settings.yml para editarlo. Reemplaza [YOUR_PROJECT_ID], [YOUR_PUBSUB_TOPIC] y [YOUR_PUBSUB_SUBSCRIPTION] por el ID de tu proyecto de Google Cloud, el tema de Cloud Pub/Sub y la suscripción a Cloud Pub/Sub, respectivamente.

    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. Configura las otras variables con los mismos valores que usaste en la sección Cómo autenticar usuarios de este instructivo.

    Por ejemplo, supongamos que el ID de cliente de tu aplicación web es XYZCLIENTID y el secreto de cliente es XYZCLIENTSECRET. Asimismo, supongamos que el nombre de tu proyecto es my-project y que el nombre de tu depósito de Cloud Storage es my-bucket. Entonces, la sección predeterminada de tu archivo settings.yml se vería así:

    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. Copia el archivo de configuración de la base de datos de ejemplo:

    cp config/database.example.yml config/database.yml
    
  5. Configura la app de muestra para que use la misma base de datos que configuraste en la sección Cómo usar datos estructurados de este instructivo:

    Cloud SQL

    Edita el archivo database.yml. Quita los comentarios de las líneas en la sección de Cloud SQL del archivo. Reemplaza los [PLACEHOLDERS] por los valores específicos de tu instancia y tu base de datos de Cloud SQL.

     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]
    
    • Reemplaza [MYSQL_USER] y [MYSQL_PASS] por el nombre de usuario y la contraseña de la instancia de Cloud SQL que creaste anteriormente.

    • Reemplaza [MYSQL_DATABASE] por el nombre de la base de datos que creaste anteriormente.

    • Reemplaza [YOUR_INSTANCE_CONNECTION_NAME] por Instance Connection Name, correspondiente a tu instancia de Cloud SQL.

    Ejecuta las migraciones:

    bundle exec rake db:migrate
    

    PostgreSQL

    Edita el archivo database.yml. Quita los comentarios a las líneas en la sección de PostgreSQL del archivo. Reemplaza los marcadores de posición your-postgresql-* por los valores específicos de tu instancia y base de datos de PostgreSQL. Por ejemplo, supongamos que la dirección IPv4 es 173.194.230.44, el nombre de usuario es postgres y la contraseña es pword123. Además, supongamos que el nombre de la base de datos es bookshelf. Entonces, la sección de PostgreSQL de tu archivo database.yml se vería así:

    # PostgreSQL Sample Database Configuration
    # ----------------------------------------
      adapter: postgresql
      encoding: unicode
      pool: 5
      username: postgres
      password: pword123
      host: 173.194.230.44
      database: bookshelf
    

    Crea las bases de datos y tablas obligatorias:

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

    Cloud Datastore

    Edita el archivo database.yml. Quita los comentarios de la única línea en la sección de Cloud Datastore del archivo. Reemplaza your-project-id por el ID del proyecto. Por ejemplo, supongamos que el ID del proyecto es my-project; la parte de Cloud Datastore del archivo database.yml se vería así:

    # Google Cloud Datastore Sample Database Configuration
    # ----------------------------------------------------
    dataset_id: my-project
    

    Ejecuta una tarea de rake para copiar los archivos de proyecto de muestra de Cloud Datastore:

    bundle exec rake backend:datastore
    

Cómo ejecutar la app en tu máquina local

  1. Inicia el servidor web local y dos procesos trabajadores:

    bundle exec foreman start --formation web=1,worker=2
    
  2. En el navegador web, ingresa la siguiente dirección:

    http://localhost:8080
    

Ahora, agrega algunos libros famosos a la estantería. Puedes ver cómo los trabajadores actualizan la información de los libros en segundo plano.

La RubyGem Foreman inicia el servidor web de Rails y ejecuta dos procesos trabajadores.

El trabajador establece una suscripción a Pub/Sub para detectar eventos. Una vez que existe la suscripción, los eventos que se publican en el tema se agregarán a una cola, incluso si no hay trabajadores detectando eventos en ese instante. Cuando un trabajador se conecta, Pub/Sub envía todos los eventos en cola.

Obtendrás más información sobre el funcionamiento del sistema en las secciones siguientes.

Cuando estés listo para continuar, presiona Ctrl + C para salir del servidor web local y los procesos trabajadores.

Cómo implementar la app en el entorno flexible de App Engine

  1. Compila recursos de JavaScript para producción:

    RAILS_ENV=production bundle exec rake assets:precompile
    
  2. Implementa el trabajador:

    gcloud app deploy worker.yaml
    
  3. Implementa la app de muestra:

    gcloud app deploy
    
  4. En el navegador web, ingresa la siguiente dirección. Reemplaza [YOUR_PROJECT_ID] por el ID del proyecto:

    https://[YOUR_PROJECT_ID].appspot.com
    

Si actualizas tu app, podrás implementar la versión actualizada mediante el mismo comando que usaste para implementar la app por primera vez. La implementación nueva crea una versión nueva de tu app y la convierte a la versión predeterminada. Las versiones anteriores de la app se conservan, al igual que sus instancias de VM asociadas. Ten en cuenta que todas estas instancias de VM y versiones de la app son recursos facturables.

Para reducir costos, borra las versiones no predeterminadas de la app.

Para borrar una versión de una app, haz lo siguiente:

  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.

Para obtener toda la información acerca de la limpieza de los recursos facturables, consulta la sección Limpieza en el paso final de este instructivo.

Estructura de la aplicación

Este diagrama muestra los componentes de la app y la manera en que se conectan entre sí.

Estructura de muestra de Auth

Comprensión del código

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

Cómo agregar tareas a una cola

Para recopilar información de la API de Google Libros sobre los libros agregados a Bookshelf, la clase Book agrega una tarea a la cola:

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

El código anterior crea una devolución de llamada de Active Record y especifica lookup_book_details como el método que se llamará después de crear un libro y guardarlo en la base de datos. Si al libro le falta información, se agrega un trabajo a la cola para que busque los detalles del libro.

LookupBookDetailsJob es un trabajo de Active Job.

El código pasa el libro que se actualizará, self, a LookupBookDetailsJob.perform_later. Esto agrega el trabajo a una cola para que busque los detalles del libro.

Backend de Pub/Sub Active Job

Active Job se puede configurar para usar un backend personalizado, como Delayed Job o Resque, para agregar tareas a una cola. La app de Bookshelf de muestra tiene su propio backend personalizado, que se especifica en la clase Application:

config.active_job.queue_adapter = :pub_sub_queue

Un backend de Active Job, que también se llama "adaptador", debe proporcionar un método enqueue. Cuando un trabajo se agrega a la cola con perform_later, se pasa al método enqueue del backend de Active Job configurado.

Para agregar un trabajo a la cola, la app de muestra crea una suscripción a un tema de Pub/Sub y publica el ID de un libro en el tema. Una vez que exista la suscripción, los mensajes se agregarán a la cola, incluso si no hay trabajadores detectando eventos en ese instante. Cuando un trabajador se conecta, Pub/Sub envía todos los eventos en cola.

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

El código anterior usa la RubyGem google-cloud-pubsub para interactuar con Pub/Sub. La biblioteca cliente de Google Cloud es un cliente idiomático de Ruby que interactúa con los servicios de Google Cloud Platform.

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

Para procesar los libros que se agregaron a una cola, una suscripción a Pub/Sub detecta los mensajes que se publican en el tema lookup_book_details_queue. Esto se explica en la sección sobre el trabajador.

API de Libros

La app de muestra usa la RubyGem del cliente de la API de Google para buscar detalles de los libros en la API de Libros.

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

Cuando se ejecuta un trabajo, el método LookupBookDetailsJob.perform recupera una lista de libros, basados en el título, desde la API de Libros.

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

Si el resultado de un volumen de libros un título, un autor y una imagen de portada del libro, se selecciona como la mejor coincidencia. De lo contrario, se usa el primer resultado:

# 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

Si se encuentra algún volumen relevante, los detalles del libro se actualizan y se guardan en la base de datos:

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

El trabajador

Un proceso trabajador administra los trabajos de búsqueda de libros. Para ejecutar el trabajador, puedes ejecutar este comando, como se especifica en Procfile:

bundle exec rake run_worker

La tarea de rake run_worker llama a PubSubQueueAdapter para iniciar un trabajador.

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

Cuando el trabajador se ejecuta, detecta mensajes en la suscripción de Cloud Pub/Sub al tema lookup_book_details_queue definido en tu archivo config/settings.yml. Cuando se recibe un mensaje, el libro asociado se recupera de la base de datos y LookupBookDetailsJob se ejecuta de inmediato para actualizar el libro.

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

Cómo ejecutar en Cloud Platform

El trabajador se implementa como un módulo independiente en la misma aplicación. Las aplicaciones en App Engine pueden tener varios servicios independientes. Significa que puedes implementar, configurar, escalar y actualizar partes de tu aplicación de manera independiente y sencilla. El frontend se implementa en el módulo predeterminado, mientras que el trabajador se implementa en el módulo de trabajador.

Si bien el trabajador no envía ninguna solicitud web a los usuarios ni ejecuta una aplicación web, te recomendamos proporcionar una verificación de estado HTTP cuando ejecutes en el entorno flexible de App Engine, a fin de garantizar que el servicio se ejecuta y responde. Sin embargo, es posible inhabilitar las verificaciones de estado.

Para proporcionar una verificación de estado, el trabajador inicia dos procesos en lugar de uno. El primer proceso es worker y el segundo es health_check, que ejecuta una aplicación de Rack simple que responde a solicitudes HTTP con una respuesta correcta para las verificaciones de estado:

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

La app usa Foreman para administrar varios procesos. Los procesos se configuran en Procfile:

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

Ahora Foreman se usa como entrypoint para el contenedor de Docker. Esto se especifica en app.yaml y worker.yaml.

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

Ten en cuenta que Procfile contiene una entrada para que el frontend web ejecute también la aplicación de Bookshelf de Rails. Dado que el servicio predeterminado (frontend) y el del trabajador comparten la misma base de código, la variable de entorno FORMATION se usa para controlar qué procesos se inician. En el siguiente diagrama se compara la implementación de un solo módulo, en el lado izquierdo, con la implementación de varios módulos, en el lado derecho:

Implementación de Pub/sub

Las variables de entorno se configuran mediante app.yaml y worker.yaml.

env_variables:
  FORMATION: web=1

El trabajador es un módulo independiente, por lo que necesita su propio archivo de configuración yaml.

env_variables:
  FORMATION: worker=5,health_check=1

Esta configuración es similar al archivo app.yaml que se usa para el frontend. Las diferencias clave son module: worker y la variable de entorno FORMATION, que configura Foreman para que ejecute cinco trabajadores y el frontend para la verificación de estado en lugar de la aplicación web de Bookshelf.

Limpieza

Sigue estos pasos para evitar que se apliquen cargos a tu cuenta de Google Cloud Platform por los recursos que usaste en este instructivo:

Borra el proyecto

La manera más fácil de eliminar la facturación es borrar el proyecto que creaste para el instructivo.

Para borrar el proyecto, haz lo siguiente:

  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.

Borra las versiones no predeterminadas de tu app

Si no quieres borrar tu proyecto, puedes borrar las versiones no predeterminadas de tu app para reducir los costos.

Para borrar una versión de una app, haz lo siguiente:

  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.

Borra tu instancia de Cloud SQL

Para borrar una instancia de Cloud SQL, haz lo siguiente:

  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.

Borra tu depósito de Cloud Storage

Para borrar un depósito de Cloud Storage, haz lo siguiente:

  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.

¿Qué sigue?

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

Prueba otras características de Google Cloud Platform tú mismo. Revisa nuestros instructivos.

¿Te sirvió esta página? Envíanos tu opinión: