バックグラウンド処理

多くのアプリでは、ウェブ リクエストとは関連のないバックグラウンド処理を行う必要があります。このチュートリアルでは、ユーザーが翻訳するテキストを入力した後、以前の翻訳の一覧を表示するウェブアプリを作成します。翻訳は、ユーザーのリクエストをブロックしないようにバックグラウンド プロセスで行われます。

次の図は、翻訳リクエストのプロセスを示しています。

アーキテクチャの図

チュートリアル アプリが動作する際のイベントの順序は次のとおりです。

  1. ウェブページにアクセスすると、Firestore に保存されている以前の翻訳の一覧が表示されます。
  2. HTML フォームに入力してテキストの翻訳をリクエストします。
  3. 翻訳リクエストは Pub/Sub にパブリッシュされます。
  4. その Pub/Sub トピックに登録されている Cloud Run サービスが起動されます。
  5. Cloud Run サービスが Cloud Translation を使用してテキストを翻訳します。
  6. Cloud Run サービスは、その結果を Firestore に保存します。

このチュートリアルは、Google Cloud でのバックグラウンド処理の詳細に関心をお持ちの方を対象としています。Pub/Sub、Firestore、Cloud Run の使用経験は必要ありません。ただし、Java と HTML の経験があれば、すべてのコードを理解するうえで役立ちます。

目標

  • Cloud Run サービスを理解し、デプロイする。
  • アプリを試してみます。

料金

このドキュメントでは、Google Cloud の次の課金対象のコンポーネントを使用します。

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを生成できます。 新しい Google Cloud ユーザーは無料トライアルをご利用いただける場合があります。

このドキュメントに記載されているタスクの完了後、作成したリソースを削除すると、それ以上の請求は発生しません。詳細については、クリーンアップをご覧ください。

始める前に

  1. Google Cloud アカウントにログインします。Google Cloud を初めて使用する場合は、アカウントを作成して、実際のシナリオでの Google プロダクトのパフォーマンスを評価してください。新規のお客様には、ワークロードの実行、テスト、デプロイができる無料クレジット $300 分を差し上げます。
  2. Google Cloud Console の [プロジェクト セレクタ] ページで、Google Cloud プロジェクトを選択または作成します。

    プロジェクト セレクタに移動

  3. Google Cloud プロジェクトで課金が有効になっていることを確認します

  4. Firestore, Pub/Sub, and Cloud Translation API を有効にします。

    API を有効にする

  5. Google Cloud CLI をインストールします。
  6. gcloud CLI を初期化するには:

    gcloud init
  7. Google Cloud Console の [プロジェクト セレクタ] ページで、Google Cloud プロジェクトを選択または作成します。

    プロジェクト セレクタに移動

  8. Google Cloud プロジェクトで課金が有効になっていることを確認します

  9. Firestore, Pub/Sub, and Cloud Translation API を有効にします。

    API を有効にする

  10. Google Cloud CLI をインストールします。
  11. gcloud CLI を初期化するには:

    gcloud init
  12. gcloud コンポーネントを更新します。
    gcloud components update
  13. 開発環境を準備します。

    Java 設定ガイドに移動

アプリの準備

  1. ターミナル ウィンドウで、サンプルアプリ リポジトリのクローンをローカル マシンに作成します。

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

    または、zip 形式のサンプルをダウンロードし、ファイルを抽出してもかまいません。

  2. バックグラウンド処理のサンプルコードを含むディレクトリに移動します。

    cd getting-started-java/background

アプリについて

ウェブアプリには次に示す 2 つの主要なコンポーネントがあります。

  • ウェブ リクエストを処理するための Java HTTP サーバー。サーバーには、次の 2 つのエンドポイントがあります。
    • /translate
      • GET(ウェブブラウザを使用): ユーザーが送信して処理された翻訳リクエストの、最新の 10 件を表示します。
      • POST(Pub/Sub サブスクリプションを使用): Cloud Translation API を使用して翻訳リクエストを処理し、結果を Firestore に保存します。
    • /create: 新しい翻訳リクエストを送信するためのフォーム。
  • Web フォームによって送信された翻訳リクエストを処理するサービス クライアント。連携するクライアントは 次の 3 つです。
    • Pub/Sub: ユーザーがウェブフォームを送信すると、Pub/Sub クライアントはリクエストの詳細を含むメッセージを公開します。このチュートリアルで作成されたサブスクリプションにより、これらのメッセージが、翻訳を実行するために作成した Cloud Run エンドポイントに中継されます。
    • Translation: このクライアントは、翻訳を実行して Pub/Sub リクエストを処理します。
    • Firestore: 翻訳が完了すると、このクライアントはリクエスト データを翻訳とともに Firestore に保存します。このクライアントは、メインの /translate エンドポイントで最新のリクエストも読み出します。

Cloud Run コードについて

  • Cloud Run アプリは、Firestore、Translation、Pub/Sub と依存関係があります。

    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-firestore</artifactId>
      <version>3.0.16</version>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-translate</artifactId>
      <version>2.1.1</version>
    </dependency>
    
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-pubsub</artifactId>
      <version>1.113.7</version>
    </dependency>
  • Firestore、Translation、Pub/Sub のグローバル クライアントは、呼び出し間で再利用できるように初期化されます。これにより、実行速度の低下の要因となる、呼び出しごとに新しいクライアントを初期化することが必要なくなります。

    @WebListener("Creates Firestore and TranslateServlet service clients for reuse between requests.")
    public class BackgroundContextListener implements ServletContextListener {
      @Override
      public void contextDestroyed(javax.servlet.ServletContextEvent event) {}
    
      @Override
      public void contextInitialized(ServletContextEvent event) {
        String firestoreProjectId = System.getenv("FIRESTORE_CLOUD_PROJECT");
        Firestore firestore = (Firestore) event.getServletContext().getAttribute("firestore");
        if (firestore == null) {
          firestore =
              FirestoreOptions.getDefaultInstance().toBuilder()
                  .setProjectId(firestoreProjectId)
                  .build()
                  .getService();
          event.getServletContext().setAttribute("firestore", firestore);
        }
    
        Translate translate = (Translate) event.getServletContext().getAttribute("translate");
        if (translate == null) {
          translate = TranslateOptions.getDefaultInstance().getService();
          event.getServletContext().setAttribute("translate", translate);
        }
    
        String topicId = System.getenv("PUBSUB_TOPIC");
        TopicName topicName = TopicName.of(firestoreProjectId, topicId);
        Publisher publisher = (Publisher) event.getServletContext().getAttribute("publisher");
        if (publisher == null) {
          try {
            publisher = Publisher.newBuilder(topicName).build();
            event.getServletContext().setAttribute("publisher", publisher);
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
    }
  • インデックス ハンドラ(/)は、Firestore から既存の翻訳をすべて取得し、リストを使用して HTML テンプレートへの入力を行います。

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
      Firestore firestore = (Firestore) this.getServletContext().getAttribute("firestore");
      CollectionReference translations = firestore.collection("translations");
      QuerySnapshot snapshot;
      try {
        snapshot = translations.limit(10).get().get();
      } catch (InterruptedException | ExecutionException e) {
        throw new ServletException("Exception retrieving documents from Firestore.", e);
      }
      List<TranslateMessage> translateMessages = Lists.newArrayList();
      List<QueryDocumentSnapshot> documents = Lists.newArrayList(snapshot.getDocuments());
      documents.sort(Comparator.comparing(DocumentSnapshot::getCreateTime));
    
      for (DocumentSnapshot document : Lists.reverse(documents)) {
        String encoded = gson.toJson(document.getData());
        TranslateMessage message = gson.fromJson(encoded, TranslateMessage.class);
        message.setData(decode(message.getData()));
        translateMessages.add(message);
      }
      req.setAttribute("messages", translateMessages);
      req.setAttribute("page", "list");
      req.getRequestDispatcher("/base.jsp").forward(req, resp);
    }
  • 新しい翻訳をリクエストするには、HTML フォームを送信します。/create に登録されているリクエスト翻訳ハンドラは、フォーム送信を解析してリクエストを検証し、メッセージを Pub/Sub にパブリッシュします。

    @Override
    public void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
      String text = req.getParameter("data");
      String sourceLang = req.getParameter("sourceLang");
      String targetLang = req.getParameter("targetLang");
    
      Enumeration<String> paramNames = req.getParameterNames();
      while (paramNames.hasMoreElements()) {
        String paramName = paramNames.nextElement();
        logger.warning("Param name: " + paramName + " = " + req.getParameter(paramName));
      }
    
      Publisher publisher = (Publisher) getServletContext().getAttribute("publisher");
    
      PubsubMessage pubsubMessage =
          PubsubMessage.newBuilder()
              .setData(ByteString.copyFromUtf8(text))
              .putAttributes("sourceLang", sourceLang)
              .putAttributes("targetLang", targetLang)
              .build();
    
      try {
        publisher.publish(pubsubMessage).get();
      } catch (InterruptedException | ExecutionException e) {
        throw new ServletException("Exception publishing message to topic.", e);
      }
    
      resp.sendRedirect("/");
    }
  • 作成した Pub/Sub サブスクリプションは、これらのリクエストを Cloud Run エンドポイントに転送し、Pub/Sub メッセージを解析して翻訳するテキストとターゲット言語を取得します。すると、翻訳 API が、選択した言語に文字列を翻訳します。

    String body = req.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
    
    PubSubMessage pubsubMessage = gson.fromJson(body, PubSubMessage.class);
    TranslateMessage message = pubsubMessage.getMessage();
    
    // Use Translate service client to translate the message.
    Translate translate = (Translate) this.getServletContext().getAttribute("translate");
    message.setData(decode(message.getData()));
    Translation translation =
        translate.translate(
            message.getData(),
            Translate.TranslateOption.sourceLanguage(message.getAttributes().getSourceLang()),
            Translate.TranslateOption.targetLanguage(message.getAttributes().getTargetLang()));
  • アプリは、Firestore に作成した新しいドキュメントに翻訳データを保存します。

    // Use Firestore service client to store the translation in Firestore.
    Firestore firestore = (Firestore) this.getServletContext().getAttribute("firestore");
    
    CollectionReference translations = firestore.collection("translations");
    
    ApiFuture<WriteResult> setFuture = translations.document().set(message, SetOptions.merge());
    
    setFuture.get();
    resp.getWriter().write(translation.getTranslatedText());

Cloud Run アプリのデプロイ

  1. Pub/Sub トピック名を選択し、uuidgen または uuidgenerator.net などのオンライン UUID ジェネレーターを使用して、Pub/Sub 検証トークンを生成します。このトークンは、Cloud Run エンドポイントが、作成した Pub/Sub サブスクリプションからのリクエストのみを受け入れるようにします。

    export PUBSUB_TOPIC=background-translate
    export PUBSUB_VERIFICATION_TOKEN=your-verification-token
    
  2. Pub/Sub トピックを作成します。

     gcloud pubsub topics create $PUBSUB_TOPIC
    
    • pom.xml ファイルの MY_PROJECT は Cloud プロジェクト ID に置き換えます。
  3. Jib Maven プラグインを使用して、コードのイメージをビルドし、GCR(イメージ リポジトリ)にデプロイします。

     mvn clean package jib:build
    
  4. Cloud Run にアプリをデプロイします。

    gcloud run deploy background --image gcr.io/MY_PROJECT/background \
          --platform managed --region us-central1 --memory 512M \
          --update-env-vars PUBSUB_TOPIC=$PUBSUB_TOPIC,PUBSUB_VERIFICATION_TOKEN=$PUBSUB_VERIFICATION_TOKEN
    

    ここで、MY_PROJECT は作成した Cloud プロジェクトの名前です。このコマンドにより、Pub/Sub サブスクリプションが翻訳リクエストをプッシュするエンドポイントが出力されます。このエンドポイントをメモします。これは、Pub/Sub サブスクリプションを作成するために必要で、ブラウザーでエンドポイントにアクセスして新しい翻訳を要求するためです。

アプリをテストする

Cloud Run サービスのデプロイ後、翻訳のリクエストを試みます。

  1. アプリをブラウザーで表示するには、前の手順で作成した Cloud Run エンドポイントに移動します。

    翻訳に関する空のリストと新しい翻訳をリクエストするためのフォームを掲載したページがあります。

  2. [+ Request Translation]をクリックし、リクエストフォームに記入して、[Submit]をクリックします。

  3. 送信すると自動的に /translate パスに戻りますが、新しい翻訳はまだ表示されていない場合があります。ページを更新するには、[Refresh] をクリックします。翻訳リストに新しい行が追加されます。翻訳が表示されない場合は、数秒待ってから再度試してください。それでも翻訳が表示されない場合は、アプリのデバッグに関する次のセクションをご覧ください。

アプリのデバッグ

Cloud Run サービスに接続できない場合、または新しい翻訳が表示されない場合は、次の点を確認します。

  • gcloud run deploy コマンドが正常に完了し、エラーが出力されていないことを確認します。エラーがある場合(message=Build failed など)は、それを修正して再度実行します。

  • ログのエラーを確認します。

    1. Google Cloud Console で、[Cloud Run] ページに移動します。

      [Cloud Run] ページに移動

    2. サービス名 background をクリックします。

    3. [ログ] をクリックします。

クリーンアップ

プロジェクトの削除

  1. Google Cloud コンソールで、[リソースの管理] ページに移動します。

    [リソースの管理] に移動

  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。

Cloud Run サービスの削除

  • このチュートリアルで作成した Cloud Run サービスを削除します。

    gcloud run services delete --region=$region background

次のステップ

  • Cloud Run の詳細を確認する
  • Cloud Run を使用してみる(フルマネージド環境や独自の Google Kubernetes Engine クラスタでステートレス コンテナを実行できます)。