Procesamiento en segundo plano con .NET


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. Se activa un servicio de Cloud Run suscrito a ese tema Pub/Sub.
  5. El servicio de Cloud Run usa Cloud Translation para traducir el texto.
  6. El servicio 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 previa en el uso de Pub/Sub, Firestore, App Engine o Cloud Functions. Sin embargo, para comprender el código completo, resulta útil contar con cierta experiencia en .NET, JavaScript y HTML.

Objetivos

  • Comprender e implementar los servicios de Cloud Run
  • 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. En la página del selector de proyectos de la consola de Google Cloud, selecciona o crea un proyecto de Google Cloud.

    Ir al selector de proyectos

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

  4. Habilita las API de Firestore, Cloud Run, Pub/Sub, and Cloud Translation.

    Habilita las API

  5. Instala Google Cloud CLI.
  6. Para inicializar la CLI de gcloud, ejecuta el siguiente comando:

    gcloud init
  7. En la página del selector de proyectos de la consola de Google Cloud, selecciona o crea un proyecto de Google Cloud.

    Ir al selector de proyectos

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

  9. Habilita las API de Firestore, Cloud Run, Pub/Sub, and Cloud Translation.

    Habilita las API

  10. Instala Google Cloud CLI.
  11. Para inicializar la CLI de gcloud, ejecuta el siguiente comando:

    gcloud init
  12. Actualiza los componentes de gcloud:
    gcloud components update
  13. Prepara tu entorno de desarrollo.

    Configura un entorno de desarrollo .NET

Prepara la app

  1. En la ventana de la terminal, clona el repositorio de la app de muestra en la máquina local:

    git clone https://github.com/GoogleCloudPlatform/getting-started-dotnet.git

    De manera opcional, puedes descargar la muestra como un archivo zip y extraerla.

  2. Cambia al directorio que contiene el código de muestra de la tarea en segundo plano:

    cd getting-started-dotnet/BackgroundProcessing

Información sobre el servicio TranslateWorker

  • Primero, el servicio importa varias dependencias, como Firestore y Translation.

    using Google.Cloud.Firestore;
    using Google.Cloud.Translation.V2;
    
  • Los clientes de Firestore y Translation se inicializan para que puedan reutilizarse entre las invocaciones del controlador. De esa forma, no es necesario inicializar nuevos clientes para cada invocación, lo que ralentizaría la ejecución.

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<FirestoreDb>(provider =>
            FirestoreDb.Create(GetFirestoreProjectId()));
        services.AddSingleton<TranslationClient>(
            TranslationClient.Create());
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }
    
  • La API de Translation traduce la string al idioma que seleccionaste.

    var result = await _translator.TranslateTextAsync(sourceText, "es");
    
  • El constructor del controlador recibe los clientes de Firestore y Pub/Sub.

    El método Post analiza el mensaje de Pub/Sub para obtener el texto que se traducirá. Usa el ID del mensaje como un nombre único para la solicitud de traducción a fin de garantizar que no almacene traducciones duplicadas.

    using Google.Cloud.Firestore;
    using Google.Cloud.Translation.V2;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Logging;
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace TranslateWorker.Controllers
    {
        /// <summary>
        /// The message Pubsub posts to our controller.
        /// </summary>
        public class PostMessage
        {
            public PubsubMessage message { get; set; }
            public string subscription { get; set; }
        }
    
        /// <summary>
        /// Pubsub's inner message.
        /// </summary>
        public class PubsubMessage
        {
            public string data { get; set; }
            public string messageId { get; set; }
            public Dictionary<string, string> attributes { get; set; }
        }
    
        [Route("api/[controller]")]
        [ApiController]
        public class TranslateController : ControllerBase
        {
            private readonly ILogger<TranslateController> _logger;
            private readonly FirestoreDb _firestore;
            private readonly TranslationClient _translator;
            // The Firestore collection where we store translations.
            private readonly CollectionReference _translations;
    
            public TranslateController(ILogger<TranslateController> logger,
                FirestoreDb firestore,
                TranslationClient translator)
            {
                _logger = logger ?? throw new ArgumentNullException(nameof(logger));
                _firestore = firestore ?? throw new ArgumentNullException(
                    nameof(firestore));
                _translator = translator ?? throw new ArgumentNullException(
                    nameof(translator));
                _translations = _firestore.Collection("Translations");
            }
    
            /// <summary>
            /// Handle a posted message from Pubsub.
            /// </summary>
            /// <param name="request">The message Pubsub posts to this process.</param>
            /// <returns>NoContent on success.</returns>
            [HttpPost]
            public async Task<IActionResult> Post([FromBody] PostMessage request)
            {
                // Unpack the message from Pubsub.
                string sourceText;
                try
                {
                    byte[] data = Convert.FromBase64String(request.message.data);
                    sourceText = Encoding.UTF8.GetString(data);
                }
                catch (Exception e)
                {
                    _logger.LogError(1, e, "Bad request");
                    return BadRequest();
                }
                // Translate the source text.
                _logger.LogDebug(2, "Translating {0} to Spanish.", sourceText);
                var result = await _translator.TranslateTextAsync(sourceText, "es");
                // Store the result in Firestore.
                Translation translation = new Translation()
                {
                    TimeStamp = DateTime.UtcNow,
                    SourceText = sourceText,
                    TranslatedText = result.TranslatedText
                };
                _logger.LogDebug(3, "Saving translation {0} to {1}.",
                    translation.TranslatedText, _translations.Path);
                await _translations.Document(request.message.messageId)
                    .SetAsync(translation);
                // Return a success code.
                return NoContent();
            }
    
            /// <summary>
            /// Serve a root page so Cloud Run knows this process is healthy.
            /// </summary>
            [Route("/")]
            public IActionResult Index()
            {
                return Content("Serving translate requests...");
            }
        }
    }
    

Implementa el servicio TranslateWorker

  • En el directorio BackgroundProcessing, ejecuta la secuencia de comandos de PowerShell para compilar e implementar el servicio en Cloud Run:

    PublishTo-CloudRun.ps1

Información sobre la secuencia de comandos PublishTo-CloudRun.ps1

La secuencia de comandos PublishTo-CloudRun.ps1 publica el servicio en Cloud Run y evita que el servicio de TranslateWorker no se abuse de él. Si el servicio permitió todas las conexiones entrantes, cualquiera podría publicar solicitudes de traducción en el controlador y, por lo tanto, se generan costos. Por lo tanto, configura el servicio para que acepte solo solicitudes POST de Pub/Sub.

La secuencia de comandos hace lo siguiente:

  1. Compila la aplicación de manera local con dotnet publish.
  2. Compila un contenedor que ejecuta la aplicación con Cloud Build.
  3. Implementa la app en Cloud Run.
  4. Permite que el proyecto cree tokens de autenticación de Pub/Sub.
  5. Crea una cuenta de servicio para representar la identidad de suscripción de Pub/Sub.
  6. Otorga permiso a la cuenta de servicio para invocar el servicio TranslateWorker.
  7. Crea un tema y una suscripción de Pub/Sub

    # 1. Build the application locally.
    dotnet publish -c Release
    
    # Collect some details about the project that we'll need later.
    $projectId = gcloud config get-value project
    $projectNumber = gcloud projects describe $projectId --format="get(projectNumber)"
    $region = "us-central1"
    
    # 2. Use Google Cloud Build to build the worker's container and publish to Google
    # Container Registry.
    gcloud builds submit --tag gcr.io/$projectId/translate-worker `
        TranslateWorker/bin/Release/netcoreapp2.1/publish
    
    # 3. Run the container with Google Cloud Run.
    gcloud beta run deploy translate-worker --region $region --platform managed `
        --image gcr.io/$projectId/translate-worker --no-allow-unauthenticated
    $url = gcloud beta run services describe translate-worker --platform managed `
        --region $region --format="get(status.address.hostname)"
    
    # 4. Enable the project to create pubsub authentication tokens.
    gcloud projects add-iam-policy-binding $projectId `
         --member=serviceAccount:service-$projectNumber@gcp-sa-pubsub.iam.gserviceaccount.com `
         --role=roles/iam.serviceAccountTokenCreator
    
    # 5. Create a service account to represent the Cloud Pub/Sub subscription identity.
    $serviceAccountExists = gcloud iam service-accounts describe `
        cloud-run-pubsub-invoker@$projectId.iam.gserviceaccount.com 2> $null
    if (-not $serviceAccountExists) {
        gcloud iam service-accounts create cloud-run-pubsub-invoker `
            --display-name "Cloud Run Pub/Sub Invoker"
    }
    
    # 6. For Cloud Run, give this service account permission to invoke
    # translate-worker service.
    gcloud beta run services add-iam-policy-binding translate-worker `
         --member=serviceAccount:cloud-run-pubsub-invoker@$projectId.iam.gserviceaccount.com `
         --role=roles/run.invoker --region=$region
    
    # 7. Create a pubsub topic and subscription, if they don't already exist.
    $topicExists = gcloud pubsub topics describe translate-requests 2> $null
    if (-not $topicExists) {
        gcloud pubsub topics create translate-requests
    }
    $subscriptionExists = gcloud pubsub subscriptions describe translate-requests 2> $null
    if ($subscriptionExists) {
        gcloud beta pubsub subscriptions modify-push-config translate-requests `
            --push-endpoint $url/api/translate `
            --push-auth-service-account cloud-run-pubsub-invoker@$projectId.iam.gserviceaccount.com
    } else {
        gcloud beta pubsub subscriptions create translate-requests `
            --topic translate-requests --push-endpoint $url/api/translate `
            --push-auth-service-account cloud-run-pubsub-invoker@$projectId.iam.gserviceaccount.com
    }
    
    

Información sobre el servicio TranslateUI

El servicio TranslateUI renderiza una página web que muestra traducciones recientes y acepta solicitudes de traducciones nuevas.

  • La clase StartUp configura una app de ASP.NET y crea clientes de Pub/Sub y Firestore.

    using Google.Apis.Auth.OAuth2;
    using Google.Cloud.Firestore;
    using Google.Cloud.PubSub.V1;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using System;
    using System.Net.Http;
    
    namespace TranslateUI
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddSingleton<FirestoreDb>(
                    provider => FirestoreDb.Create(GetFirestoreProjectId()));
                services.AddSingleton<PublisherClient>(
                    provider => PublisherClient.CreateAsync(new TopicName(
                        GetProjectId(), GetTopicName())).Result);
                services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseExceptionHandler("/Home/Error");
                    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                    app.UseHsts();
                }
    
                app.UseHttpsRedirection();
                app.UseStaticFiles();
                app.UseCookiePolicy();
    
                app.UseMvc(routes =>
                {
                    routes.MapRoute(
                        name: "default",
                        template: "{controller=Home}/{action=Index}/{id?}");
                });
            }
    
        }
    }
    
  • El controlador de índices Index obtiene todas las traducciones existentes de Firestore y completa un ViewModel con la lista:

    using Google.Cloud.Firestore;
    using Google.Cloud.PubSub.V1;
    using Google.Protobuf;
    using Microsoft.AspNetCore.Mvc;
    using System.Diagnostics;
    using System.Linq;
    using System.Threading.Tasks;
    using TranslateUI.Models;
    
    namespace TranslateUI.Controllers
    {
        public class HomeController : Controller
        {
            private readonly FirestoreDb _firestore;
            private readonly PublisherClient _publisher;
            private CollectionReference _translations;
    
            public HomeController(FirestoreDb firestore, PublisherClient publisher)
            {
                _firestore = firestore;
                _publisher = publisher;
                _translations = _firestore.Collection("Translations");
            }
    
            [HttpPost]
            [HttpGet]
            public async Task<IActionResult> Index(string SourceText)
            {
                // Look up the most recent 20 translations.
                var query = _translations.OrderByDescending("TimeStamp")
                    .Limit(20);
                var snapshotTask = query.GetSnapshotAsync();
    
                if (!string.IsNullOrWhiteSpace(SourceText))
                {
                    // Submit a new translation request.
                    await _publisher.PublishAsync(new PubsubMessage()
                    {
                        Data = ByteString.CopyFromUtf8(SourceText)
                    });
                }
    
                // Render the page.
                var model = new HomeViewModel()
                {
                    Translations = (await snapshotTask).Documents.Select(
                        doc => doc.ConvertTo<Translation>()).ToList(),
                    SourceText = SourceText
                };
                return View(model);
            }
    
            [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
            public IActionResult Error()
            {
                return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
            }
        }
    }
  • Para solicitar una traducción nueva, se debe enviar un formulario HTML. El controlador de solicitudes de traducción valida la solicitud y publica un mensaje en Pub/Sub:

    // Submit a new translation request.
    await _publisher.PublishAsync(new PubsubMessage()
    {
        Data = ByteString.CopyFromUtf8(SourceText)
    });
    

Implementa el servicio TranslateUI

  • En el directorio BackgroundProcessing, ejecuta la secuencia de comandos de PowerShell para compilar e implementar el servicio en Cloud Run:

    ./PublishTo-CloudRun.ps1

Información sobre la secuencia de comandos PublishTo-CloudRun.ps1

La secuencia de comandos PublishTo-CloudRun.ps1 publica la app en Cloud Run.

La secuencia de comandos hace lo siguiente:

  1. Compila la aplicación de manera local con dotnet publish.
  2. Compila un contenedor que ejecuta la app mediante Cloud Build.
  3. Implementa la app en Cloud Run.

    # 1. Build the application locally.
    dotnet publish -c Release
    # 2. Use Google Cloud Build to build the UI's container and publish to Google
    # Container Registry.
    gcloud builds submit --tag gcr.io/$projectId/translate-ui `
        TranslateUI/bin/Release/netcoreapp2.1/publish
    
    # 3. Run the container with Google Cloud Run.
    gcloud beta run deploy translate-ui --region $region --platform managed `
        --image gcr.io/$projectId/translate-ui --allow-unauthenticated
    
    

Prueba la app

Después de ejecutar correctamente la secuencia de comandos PublishTo-CloudRun.ps1, intenta solicitar una traducción.

  1. El comando final en la secuencia de comandos PublishTo-CloudRun.ps1 te indica la URL de tu servicio de IU. En la ventana de la terminal, busca la URL del servicio TranslateUI:

    gcloud beta run services describe translate-ui --region $region --format="get(status.address.hostname)"
  2. En tu navegador, ve a la URL que obtuviste del paso anterior.

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

  3. En el campo Texto para traducir, ingresa el texto que desees traducir, por ejemplo, Hello, World..

  4. Haga 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 al servicio de Cloud Run o no ves nuevas traducciones, verifica lo siguiente:

  • Comprueba que la secuencia de comandos PublishTo-CloudRun.ps1 se completó con éxito y que no se generó ningún error. Si hubo errores (por ejemplo, message=Build failed), corrígelos y vuelve a intentarlo.

  • Comprueba si hay errores en los registros.

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

      Ir a la página de Cloud Run

    2. Haz clic en el nombre del servicio, translate-ui.

    3. Haga clic en Registros.

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 servicios de Cloud Run.

  • Borra los servicios de Cloud Run que creaste en este instructivo:

    gcloud beta run services delete --region=$region translate-ui
    gcloud beta run services delete --region=$region translate-worker

¿Qué sigue?