Traitement en arrière-plan avec .NET

De nombreuses applications doivent exécuter un traitement en arrière-plan dans d'autres contextes que celui d'une requête Web. Ce tutoriel indique comment créer une application Web qui permet aux utilisateurs de saisir du texte à traduire, puis qui affiche la liste des traductions précédentes. La traduction est effectuée dans un traitement en arrière-plan pour éviter de bloquer la requête de l'utilisateur.

Le schéma suivant illustre le processus de requête de traduction.

Schéma de l'architecture

Voici, dans l'ordre, les étapes du fonctionnement de l'application du tutoriel :

  1. L'application accède à la page Web pour consulter la liste des traductions précédentes stockées dans Firestore.
  2. Elle demande la traduction d'un texte en saisissant un formulaire HTML.
  3. La requête de traduction est publiée dans Pub/Sub.
  4. Un service Cloud Run abonné à ce sujet Pub/Sub est déclenché.
  5. Le service Cloud Run utilise Cloud Translation pour traduire le texte.
  6. Le service Cloud Run stocke le résultat dans Firestore.

Ce tutoriel est destiné à toute personne intéressée par le traitement en arrière-plan avec Google Cloud. Il ne nécessite aucune connaissance particulière de Pub/Sub, Firestore, App Engine ou Cloud Functions. Cependant, pour comprendre l'intégralité du code, il peut être utile de se familiariser avec .NET, JavaScript et HTML.

Objectifs

  • Comprendre et déployer les services Cloud Run
  • Tester l'application

Coûts

Ce tutoriel utilise les composants facturables suivants de Google Cloud :

Obtenez une estimation des coûts en fonction de votre utilisation prévue à l'aide du simulateur de coût. Les nouveaux utilisateurs de Google Cloud peuvent bénéficier d'un essai gratuit.

Une fois que vous avez terminé ce tutoriel, vous pouvez éviter de continuer à payer des frais en supprimant les ressources que vous avez créées. Consultez la page Effectuer un nettoyage pour en savoir plus.

Avant de commencer

  1. Connectez-vous à votre compte Google.

    Si vous n'en possédez pas déjà un, vous devez en créer un.

  2. Dans Cloud Console, sur la page de sélection du projet, sélectionnez ou créez un projet Cloud.

    Accéder à la page de sélection du projet

  3. Vérifiez que la facturation est activée pour votre projet Google Cloud. Découvrez comment vérifier que la facturation est activée pour votre projet.

  4. Activer les API Firestore, Cloud Run, Pub/Sub, and Cloud Translation.

    Activer les API

  5. Installez et initialisez le SDK Cloud.
  6. Mettez à jour les composants gcloud :
    gcloud components update
  7. Préparez votre environnement de développement.

    Configurer un environnement de développement .NET

Préparer l'application

  1. Dans votre fenêtre de terminal, clonez le dépôt de l'exemple d'application sur votre machine locale :

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

    Vous pouvez également télécharger l'exemple en tant que fichier ZIP et l'extraire.

  2. Accédez au répertoire contenant l'exemple de code de tâche en arrière-plan :

    cd getting-started-dotnet/BackgroundProcessing

Comprendre le service TranslateWorker

  • Le service commence par l'importation de plusieurs dépendances comme Firestore et Translation.

    using Google.Cloud.Firestore;
    using Google.Cloud.Translation.V2;
    
  • Les clients Firestore et Translation sont initialisés afin qu'ils puissent être réutilisés pour les appels de gestionnaire. De cette façon, vous n'avez pas à initialiser de nouveaux clients à chaque appel, ce qui ralentirait l'exécution.

    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 traduit la chaîne dans la langue que vous avez sélectionnée.

    var result = await _translator.TranslateTextAsync(sourceText, "es");
    
  • Le constructeur du contrôleur reçoit les clients Firestore et Pub/Sub.

    La méthode Post analyse le message Pub/Sub pour obtenir le texte à traduire. L'ID du message est utilisé comme nom unique pour la requête de traduction afin de s'assurer qu'il ne stocke aucune traduction en double.

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

Déployer le service TranslateWorker

  • Dans le répertoire BackgroundProcessing, exécutez le script PowerShell pour créer et déployer le service dans Cloud Run :

    PublishTo-CloudRun.ps1

Comprendre le script PublishTo-CloudRun.ps1

Le script PublishTo-CloudRun.ps1 publie le service sur Cloud Run et protège le service TranslateWorker contre toute utilisation abusive. Si le service a autorisé toutes les connexions entrantes, n'importe qui peut publier des requêtes de traduction dans le contrôleur et, par conséquent, entraîner des frais. Par conséquent, vous allez configurer le service pour qu'il n'accepte que les requêtes POST de Pub/Sub.

Le script a les effets suivants :

  1. Crée l'application en local à l'aide de dotnet publish.
  2. Génère un conteneur qui exécute l'application à l'aide de Cloud Build.
  3. Déploie l'application sur Cloud Run.
  4. Permet au projet de créer des jetons d'authentification Pub/Sub.
  5. Crée un compte de service pour représenter l'identité de l'abonnement Pub/Sub.
  6. Donne au compte de service l'autorisation d'appeler le service TranslateWorker.
  7. Crée un sujet et un abonnement 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
    }
    
    

Comprendre le service TranslateUI

Le service TranslateUI affiche une page Web qui affiche des traductions récentes et accepte les requêtes de nouvelles traductions.

  • La classe StartUp configure une application ASP.NET et crée des clients Pub/Sub et 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?}");
                });
            }
    
        }
    }
    
  • Le gestionnaire d'index Index récupère toutes les traductions existantes de Firestore et remplit un ViewModel avec la liste :

    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 });
            }
        }
    }
  • Pour demander de nouvelles traductions, envoyez un formulaire HTML. Le gestionnaire des requêtes de traduction valide la demande et publie un message dans Pub/Sub :

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

Déployer le service TranslateUI

  • Dans le répertoire BackgroundProcessing, exécutez le script PowerShell pour créer et déployer le service dans Cloud Run :

    ./PublishTo-CloudRun.ps1

Comprendre le script PublishTo-CloudRun.ps1

Le script PublishTo-CloudRun.ps1 publie l'application sur Cloud Run.

Le script a les effets suivants :

  1. Crée l'application en local à l'aide de dotnet publish.
  2. Génère un conteneur qui exécute l'application à l'aide de Cloud Build.
  3. Déploie l'application sur 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
    
    

Tester l'application

Après avoir exécuté avec succès le script PublishTo-CloudRun.ps1, essayez de demander une traduction.

  1. La commande finale dans le script PublishTo-CloudRun.ps1 vous indique l'URL de votre service d'interface utilisateur. Dans votre fenêtre de terminal, recherchez l'URL du service TranslateUI :

    gcloud beta run services describe translate-ui --region $region --format="get(status.address.hostname)"
  2. Dans votre navigateur, accédez à l'URL obtenue à l'étape précédente.

    Une page s'affiche, contenant une liste de traductions vide ainsi qu'un formulaire de requête de nouvelles traductions.

  3. Dans le champ Text to translate (Texte à traduire), saisissez un texte à traduire. Exemple : Hello, World..

  4. Cliquez sur Envoyer.

  5. Pour actualiser la page, cliquez sur Refresh (Actualiser). Une nouvelle ligne s'affiche dans la liste des traductions. Si vous ne voyez pas de traduction, attendez encore quelques secondes, puis réessayez. Si vous ne voyez toujours pas de traduction, consultez la section suivante sur le débogage de l'application.

Déboguer l'application

Si vous ne parvenez pas à vous connecter à votre service Cloud Run ou si vous ne voyez pas de nouvelles traductions, vérifiez les points suivants :

  • Vérifiez que le script PublishTo-CloudRun.ps1 a bien abouti et n'a généré aucune erreur. Si des erreurs se sont produites (par exemple, message=Build failed), corrigez-les et réessayez.

  • Recherchez les erreurs dans les journaux :

    1. Dans Google Cloud Console, accédez à la page Cloud Run.

      Accéder à la page Cloud Run

    2. Cliquez sur le nom du service, translate-ui.

    3. Cliquez sur Logs (Journaux).

Nettoyer

Pour éviter que les ressources utilisées dans ce tutoriel soient facturées sur votre compte Google Cloud Platform :

Supprimer le projet Cloud

  1. Dans Cloud Console, accédez à la page Gérer les ressources.

    Accéder à la page Gérer les ressources

  2. Dans la liste des projets, sélectionnez le projet que vous souhaitez supprimer, puis cliquez sur Supprimer .
  3. Dans la boîte de dialogue, saisissez l'ID du projet, puis cliquez sur Arrêter pour supprimer le projet.

Supprimez les services Cloud Run.

  • Supprimez les services Cloud Run que vous avez créés dans ce tutoriel :

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

Étape suivante