Elaborazione in background con .NET


Molte app devono eseguire l'elaborazione in background al di fuori del contesto di una richiesta web. Questo tutorial crea un'app web che consente agli utenti di inserire testo per tradurre, quindi mostra un elenco di traduzioni precedenti. La traduzione viene eseguita in background per evitare di bloccare la richiesta dell'utente.

Il seguente diagramma illustra la procedura di richiesta di traduzione.

Diagramma dell'architettura.

Ecco la sequenza degli eventi per il funzionamento dell'app:

  1. Visita la pagina web per visualizzare un elenco delle traduzioni precedenti, archiviate in Firestore.
  2. Richiedi una traduzione del testo inserendo un modulo HTML.
  3. La richiesta di traduzione è pubblicata in Pub/Sub.
  4. Viene attivato un servizio Cloud Run sottoscritto a quell'argomento Pub/Sub.
  5. Il servizio Cloud Run utilizza Cloud Translation per tradurre il testo.
  6. Il servizio Cloud Run archivia il risultato in Firestore.

Questo tutorial è destinato a chiunque sia interessato a conoscere l'elaborazione in background con Google Cloud. Non è richiesta alcuna esperienza precedente con Pub/Sub, Firestore, App Engine o Cloud Functions. Tuttavia, per comprendere tutto il codice, è utile avere qualche esperienza con .NET, JavaScript e HTML.

Obiettivi

  • Capire ed eseguire il deployment dei servizi Cloud Run.
  • Prova l'app.

Costi

In questo documento utilizzi i seguenti componenti fatturabili di Google Cloud:

Per generare una stima dei costi basata sull'utilizzo previsto, utilizza il Calcolatore prezzi. I nuovi utenti di Google Cloud potrebbero essere idonei per una prova gratuita.

Al termine delle attività descritte in questo documento, puoi evitare la fatturazione continua eliminando le risorse che hai creato. Per ulteriori informazioni, consulta la sezione Pulizia.

Prima di iniziare

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. Make sure that billing is enabled for your Google Cloud project.

  4. Enable the Firestore, Cloud Run, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  5. Install the Google Cloud CLI.
  6. To initialize the gcloud CLI, run the following command:

    gcloud init
  7. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  8. Make sure that billing is enabled for your Google Cloud project.

  9. Enable the Firestore, Cloud Run, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  10. Install the Google Cloud CLI.
  11. To initialize the gcloud CLI, run the following command:

    gcloud init
  12. Aggiorna i componenti di gcloud:
    gcloud components update
  13. Prepara l'ambiente di sviluppo.

    Configurazione di un ambiente di sviluppo .NET

Preparazione dell'app in corso...

  1. Nella finestra del terminale, clona il repository dell'app di esempio sulla macchina locale:

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

    In alternativa, puoi scaricare l'esempio come file ZIP ed estrarlo.

  2. Passa alla directory che contiene il codice campione dell'attività in background:

    cd getting-started-dotnet/BackgroundProcessing

Informazioni sul servizio TranslateWorker

  • Il servizio inizia importando diverse dipendenze, ad esempio Firestore e Translation.

    using Google.Cloud.Firestore;
    using Google.Cloud.Translation.V2;
    
  • I client Firestore e Translation sono inizializzati in modo da poter essere riutilizzati tra le chiamate del gestore. In questo modo, non dovrai inizializzare nuovi client per ogni chiamata, cosa che comporterebbe un rallentamento dell'esecuzione.

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<FirestoreDb>(provider =>
            FirestoreDb.Create(GetFirestoreProjectId()));
        services.AddSingleton<TranslationClient>(
            TranslationClient.Create());
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }
    
  • L'API Translation traduce la stringa nella lingua selezionata.

    var result = await _translator.TranslateTextAsync(sourceText, "es");
    
  • Il costruttore del controller riceve i client Firestore e Pub/Sub.

    Il metodo Post analizza il messaggio Pub/Sub per far tradurre il testo. Utilizza l'ID messaggio come nome univoco per la richiesta di traduzione, per assicurare che non memorizzi traduzioni duplicate.

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

Deployment del servizio TranslateWorker in corso...

  • Nella directory BackgroundProcessing, esegui lo script di PowerShell per creare ed eseguire il deployment del servizio in Cloud Run:

    PublishTo-CloudRun.ps1

Informazioni sullo script PublishTo-CloudRun.ps1

Lo script PublishTo-CloudRun.ps1 pubblica il servizio su Cloud Run e protegge il servizio TranslateWorker da comportamenti illeciti. Se il servizio consente tutte le connessioni in entrata, chiunque può pubblicare richieste di traduzione al titolare e, di conseguenza, i costi. Pertanto, configuri il servizio in modo che accetti solo richieste POST da Pub/Sub.

Lo script esegue le seguenti operazioni:

  1. Crea l'app localmente utilizzando l'dotnet publish.
  2. Crea un container che esegue l'app utilizzando Cloud Build.
  3. Esegue il deployment dell'app in Cloud Run.
  4. Consente al progetto di creare token di autenticazione Pub/Sub.
  5. Crea un account di servizio per rappresentare l'identità della sottoscrizione Pub/Sub.
  6. Concede all'account di servizio l'autorizzazione per richiamare il servizio TranslateWorker.
  7. Crea un argomento e una sottoscrizione 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
    }
    
    

Informazioni sul servizio TranslateUI

Il servizio TranslateUI esegue il rendering di una pagina web che mostra traduzioni recenti e accetta richieste di nuove traduzioni.

  • La classe StartUp configura un'app ASP.NET e crea client Pub/Sub e 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?}");
                });
            }
    
        }
    }
    
  • Il gestore di indice Index riceve tutte le traduzioni esistenti da Firestore e compila un ViewModel con l'elenco:

    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 });
            }
        }
    }
  • Le nuove traduzioni vengono inviate inviando un modulo HTML. Il gestore di traduzione della richiesta convalida la richiesta e pubblica un messaggio in Pub/Sub:

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

Deployment del servizio TranslateUI in corso...

  • Nella directory BackgroundProcessing, esegui lo script di PowerShell per creare ed eseguire il deployment del servizio in Cloud Run:

    ./PublishTo-CloudRun.ps1

Informazioni sullo script PublishTo-CloudRun.ps1

Lo script PublishTo-CloudRun.ps1 pubblica l'app su Cloud Run.

Lo script esegue le seguenti operazioni:

  1. Crea l'app localmente utilizzando l'dotnet publish.
  2. Crea un container che esegue l'app utilizzando Cloud Build.
  3. Esegue il deployment dell'app in 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
    
    

Test dell'app

Dopo aver eseguito correttamente lo script PublishTo-CloudRun.ps1, prova a richiedere una traduzione.

  1. Il comando finale nello script PublishTo-CloudRun.ps1 indica l'URL del servizio UI. Nella finestra del terminale, individua l'URL del servizio TranslateUI:

    gcloud beta run services describe translate-ui --region $region --format="get(status.address.hostname)"
  2. Nel browser, vai all'URL visualizzato nel passaggio precedente.

    C'è una pagina con un elenco di traduzioni vuote e un modulo per richiedere nuove traduzioni.

  3. Nel campo Testo da tradurre, inserisci del testo da tradurre, ad esempio Hello, World.

  4. Fai clic su Invia.

  5. Per aggiornare la pagina, fai clic su Aggiorna . C'è una nuova riga nell'elenco delle traduzioni. Se non vedi una traduzione, attendi qualche secondo e riprova. Se la traduzione non viene ancora visualizzata, consulta la sezione successiva sul debug dell'app.

Debug dell'app

Se non riesci a connetterti al servizio Cloud Run o non vedi nuove traduzioni, controlla quanto segue:

  • Controlla che lo script PublishTo-CloudRun.ps1 sia stato completato correttamente e non abbia restituito errori. Se si sono verificati errori (ad esempio, message=Build failed), correggili e prova a ripetere l'esecuzione.

  • Verifica la presenza di errori nei log:

    1. Nella console Google Cloud, vai alla pagina Cloud Run.

      Vai alla pagina Cloud Run

    2. Fai clic sul nome del servizio, translate-ui.

    3. Fai clic su Log.

Esegui la pulizia

Per evitare che al tuo Account Google Cloud vengano addebitati costi relativi alle risorse utilizzate in questo tutorial, elimina il progetto che contiene le risorse oppure mantieni il progetto ed elimina le singole risorse.

Elimina il progetto Google Cloud

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

Eliminare i servizi Cloud Run.

  • Elimina i servizi Cloud Run creati in questo tutorial:

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

Passaggi successivi