Processamento em segundo plano com .NET


Para muitos apps, é 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 serviço do Cloud Run inscrito nesse tópico Pub/Sub é acionado.
  5. O serviço do Cloud Run usa o Cloud Translation para traduzir o texto.
  6. O serviço do 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 Pub/Sub, Firestore, App Engine ou Cloud Functions. No entanto, para entender todo o código, é útil ter alguma experiência com .NET, JavaScript e HTML.

Objetivos

  • Entenda e implante os serviços do Cloud Run.
  • 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. Instale a CLI do Google Cloud.
  6. Para inicializar a CLI gcloud, execute o seguinte comando:

    gcloud init
  7. 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

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

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

    Ative as APIs

  10. Instale a CLI do Google Cloud.
  11. Para inicializar a CLI gcloud, execute o seguinte comando:

    gcloud init
  12. Atualize os componentes gcloud:
    gcloud components update
  13. Prepare seu ambiente de desenvolvimento.

    Como configurar um ambiente de desenvolvimento .NET

Como preparar o aplicativo

  1. Na janela de terminal, clone o repositório do app de amostra em sua máquina local:

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

    Outra alternativa é fazer o download da amostra como um arquivo ZIP e extraí-lo.

  2. Mude para o diretório que contém o código de exemplo da tarefa em segundo plano:

    cd getting-started-dotnet/BackgroundProcessing

Como entender o serviço TranslateWorker

  • O serviço começa importando várias dependências, como Firestore e Translation.

    using Google.Cloud.Firestore;
    using Google.Cloud.Translation.V2;
    
  • Os clientes do Firestore e do Translation são inicializados para que possam ser reutilizados entre chamadas do gerenciador. Dessa forma, você não precisa inicializar novos clientes para cada chamada, o que atrasaria a execução.

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<FirestoreDb>(provider =>
            FirestoreDb.Create(GetFirestoreProjectId()));
        services.AddSingleton<TranslationClient>(
            TranslationClient.Create());
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }
    
  • A API Translation traduz a string para o idioma selecionado.

    var result = await _translator.TranslateTextAsync(sourceText, "es");
    
  • O construtor do controlador recebe os clientes do Firestore e do Pub/Sub.

    O método Post analisa a mensagem do Pub/Sub para ver o texto a ser traduzido. Ele usa o ID da mensagem como um nome exclusivo para a solicitação de tradução para garantir que não armazene nenhuma tradução duplicada.

    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...");
            }
        }
    }
    

Como implantar o serviço TranslateWorker

  • No diretório BackgroundProcessing, execute o script do PowerShell para criar e implantar o serviço no Cloud Run:

    PublishTo-CloudRun.ps1

Como entender o script PublishTo-CloudRun.ps1

O script PublishTo-CloudRun.ps1 publica o serviço no Cloud Run e protege o serviço TranslateWorker contra abuso. Se o serviço permitisse todas as conexões de entrada, qualquer pessoa poderia postar solicitações de tradução para o controlador e, portanto, incorrer em custos. Portanto, você configura o serviço para aceitar apenas solicitações POST do Pub/Sub.

O script faz o seguinte:

  1. Cria o aplicativo localmente usando dotnet publish.
  2. Cria um contêiner que executa o aplicativo usando o Cloud Build.
  3. Implanta o aplicativo no Cloud Run.
  4. Permite que o projeto crie tokens de autenticação do Pub/Sub.
  5. Cria uma conta de serviço para representar a identidade da assinatura do Pub/Sub.
  6. Concede à conta do serviço permissão para chamar o serviço TranslateWorker.
  7. Cria um tópico e uma assinatura do 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
    }
    
    

Como entender o serviço TranslateUI

O serviço TranslateUI renderiza uma página da Web que exibe traduções recentes e aceita solicitações de novas traduções.

  • A classe StartUp configura um aplicativo ASP.NET e cria clientes do Pub/Sub e do 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?}");
                });
            }
    
        }
    }
    
  • O gerenciador de índices Index recebe todas as traduções existentes do Firestore e preenche um ViewModel com a 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 });
            }
        }
    }
  • Novas traduções são solicitadas através do envio de um formulário HTML. O gerenciador de tradução de solicitação valida a solicitação e publica uma mensagem no Pub/Sub:

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

Como implantar o serviço TranslateUI

  • No diretório BackgroundProcessing, execute o script do PowerShell para criar e implantar o serviço no Cloud Run:

    ./PublishTo-CloudRun.ps1

Como entender o script PublishTo-CloudRun.ps1

O script PublishTo-CloudRun.ps1 publica o aplicativo no Cloud Run.

O script faz o seguinte:

  1. Cria o aplicativo localmente usando dotnet publish.
  2. Cria um contêiner que executa o aplicativo usando o Cloud Build.
  3. Implanta o aplicativo no 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
    
    

Como testar o app

Após executar com êxito o script PublishTo-CloudRun.ps1, tente solicitar uma tradução.

  1. O comando final no script PublishTo-CloudRun.ps1 informa o URL do seu serviço de IU. Na janela do seu terminal, encontre o URL do serviço TranslateUI:

    gcloud beta run services describe translate-ui --region $region --format="get(status.address.hostname)"
  2. No seu navegador, acesse o URL que você recebeu na etapa anterior.

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

  3. No campo Texto a ser traduzido, digite algum texto a ser traduzido, por exemplo, Hello, World.

  4. Selecione Enviar.

  5. Para atualizar a página, clique em Atualizar . Há uma nova linha na lista de tradução. Se você não vir uma tradução, aguarde mais alguns segundos e tente novamente. Se você ainda não vir uma tradução, consulte a próxima seção sobre como depurar o aplicativo.

Como depurar o aplicativo

Se você não conseguir se conectar ao seu serviço do Cloud Run ou não vir novas traduções, verifique o seguinte:

  • Verifique se o script PublishTo-CloudRun.ps1 foi concluído com êxito e não gerou erros. Se houver erros (por exemplo, message=Build failed), corrija-os e tente executar novamente.

  • Verifique se há erros nos registros:

    1. No console do Google Cloud, acesse a página do Cloud Run.

      Acessar a página do Cloud Run

    2. Clique no nome do serviço, translate-ui.

    3. Clique em Registros.

Limpeza

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 serviços do Cloud Run.

  • Exclua os serviços do Cloud Run que você criou neste tutorial:

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

A seguir