拡張機能のライフサイクル イベントを処理する

拡張機能のインスタンスには、次のいずれかのライフサイクル イベントが発生した場合にトリガーされる Cloud Tasks の関数を含めることができます。

  • 拡張機能のインスタンスがインストールされた
  • 拡張機能のインスタンスが新しいバージョンに更新された
  • 拡張機能インスタンスの構成が変更された

この機能の最も重要なユースケースの 1 つは、データのバックフィルです。たとえば、Cloud Storageバケットにアップロードされた画像のサムネイル プレビューを生成する拡張機能を作成しているとします。拡張機能の主な処理は、onFinalize Cloud Storage イベントによってトリガーされる関数で行われます。ただし、拡張機能のインストールにアップロードされた画像のみが対象になります。onInstall ライフサイクル イベントによってトリガーされる関数を拡張機能に含めることで、拡張機能のインストール時に既存の画像のサムネイル プレビューを生成することもできます。

ライフサイクル イベント トリガーには他にも次のような用途があります。

  • インストール後の設定の自動化(データベース レコードの作成、インデックス作成など)
  • 下位互換性のない変更を公開する場合は、更新時にデータを自動的に移行します。

実行期間の短いライフサイクル イベント ハンドラ

タスクを Cloud Functions の最大継続時間(第 1 世代 API では 9 分)内に完了できる場合は、ライフサイクル イベント ハンドラをタスクキューの onDispatch イベントでトリガーされる単一の関数として記述します。

export const myTaskFunction = functions.tasks.taskQueue()
  .onDispatch(async () => {
    // Complete your lifecycle event handling task.
    // ...

    // When processing is complete, report status to the user (see below).
  });

次に、拡張機能の extension.yaml ファイルで次の操作を行います。

  1. taskQueueTrigger プロパティを設定して、関数を拡張機能のリソースとして登録します。taskQueueTrigger を空のマップ({})に設定した場合、拡張機能はデフォルト設定を使用して Cloud Tasks キューをプロビジョニングします。また、必要に応じてこれらの設定を調整することもできます。

    resources:
      - name: myTaskFunction
        type: firebaseextensions.v1beta.function
        description: >-
          Describe the task performed when the function is triggered by a lifecycle
          event
        properties:
          location: ${LOCATION}
          taskQueueTrigger: {}
    
  2. 関数を 1 つ以上のライフサイクル イベントのハンドラとして登録します。

    resources:
      - ...
    lifecycleEvents:
      onInstall:
        function: myTaskFunction
        processingMessage: Resizing your existing images
      onUpdate:
        function: myOtherTaskFunction
        processingMessage: Setting up your extension
      onConfigure:
        function: myOtherTaskFunction
        processingMessage: Setting up your extension
    
    

    関数は、onInstallonUpdateonConfigure の任意のイベントに登録できます。これらのイベントはすべて省略可能です。

  3. 推奨: 拡張機能が機能するために処理タスクが必要ない場合は、ユーザーが構成したパラメータを追加し、ユーザーが有効にするかどうかを選択できるようにします。

    たとえば、次のようなパラメータを追加します。

    params:
      - param: DO_BACKFILL
        label: Backfill existing images
        description: >
          Should existing, unresized images in the Storage bucket be resized as well?
        type: select
        options:
          - label: Yes
            value: true
          - label: No
            value: false
    

    関数でパラメータが false に設定されている場合、早期に終了します。

    export const myTaskFunction = functions.tasks.taskQueue()
      .onDispatch(async () => {
        if (!process.env.DO_BACKFILL) {
          await runtime.setProcessingState(
            "PROCESSING_COMPLETE",
            "Existing images were not resized."
          );
          return;
        }
        // Complete your lifecycle event handling task.
        // ...
      });
    

長時間実行タスクの実行

Cloud Functions の最大継続時間内にタスクを完了できない場合は、Admin SDK の TaskQueue.enqueue() メソッドを使用してジョブをキューに登録し、タスクをサブタスクに分割して、各サブタスクを順番に実行します。

たとえば、Cloud Firestore データをバックフィルするとします。クエリカーソルを使用して、ドキュメント コレクションをチャンクに分割できます。チャンクを処理した後、次のように開始オフセットを進めて、別の関数呼び出しをキューに追加します。

import { getFirestore } from "firebase-admin/firestore";
import { getFunctions } from "firebase-admin/functions";

exports.backfilldata = functions.tasks.taskQueue().onDispatch(async (data) => {
  // When a lifecycle event triggers this function, it doesn't pass any data,
  // so an undefined offset indicates we're on our first invocation and should
  // start at offset 0. On subsequent invocations, we'll pass an explicit
  // offset.
  const offset = data["offset"] ?? 0;

  // Get a batch of documents, beginning at the offset.
  const snapshot = await getFirestore()
    .collection(process.env.COLLECTION_PATH)
    .startAt(offset)
    .limit(DOCS_PER_BACKFILL)
    .get();
  // Process each document in the batch.
  const processed = await Promise.allSettled(
    snapshot.docs.map(async (documentSnapshot) => {
      // Perform the processing.
    })
  );

  // If we processed a full batch, there are probably more documents to
  // process, so enqueue another invocation of this function, specifying
  // the offset to start with.
  //
  // If we processed less than a full batch, we're done.
  if (processed.length == DOCS_PER_BACKFILL) {
    const queue = getFunctions().taskQueue(
      "backfilldata",
      process.env.EXT_INSTANCE_ID
    );
    await queue.enqueue({
      offset: offset + DOCS_PER_BACKFILL,
    });
  } else {
      // Processing is complete. Report status to the user (see below).
  }
});

前のセクションの説明に従って、extension.yaml に関数を追加します。

ステータスの報告

すべての処理関数が正常に完了するか、エラーが発生した場合、Admin SDK の拡張機能ランタイム メソッドを使用して、タスクのステータスを報告します。このステータスは、Firebase コンソールの拡張機能の詳細ページで確認できます。

正常な完了と致命的でないエラー

正常な完了と致命的でないエラー(拡張機能が非機能状態にならないエラー)を報告するには、Admin SDK の setProcessingState() 拡張機能ランタイム メソッドを使用します。

import { getExtensions } from "firebase-admin/extensions";

// ...

getExtensions().runtime().setProcessingState(processingState, message);

以下の状態を設定できます。

致命的でない状態
PROCESSING_COMPLETE

タスクが正常に完了したことを報告する場合に使用します。例:

getExtensions().runtime().setProcessingState(
  "PROCESSING_COMPLETE",
  `Backfill complete. Successfully processed ${numSuccess} documents.`
);
PROCESSING_WARNING

部分的な成功を報告する場合に使用します。例:

getExtensions().runtime().setProcessingState(
  "PROCESSING_WARNING",
  `Backfill complete. ${numSuccess} documents processed successfully.`
    + ` ${numFailed} documents failed to process. ${listOfErrors}.`
    + ` ${instructionsToFixTheProblem}`
);
PROCESSING_FAILED

タスクは実行できないものの、拡張機能が使用不能にならないエラーを報告する場合に使用します。例:

getExtensions().runtime().setProcessingState(
  "PROCESSING_FAILED",
  `Backfill failed. ${errorMsg} ${optionalInstructionsToFixTheProblem}.`
);

拡張機能が使用不能になるエラーを報告するには、setFatalError() を呼び出します。

NONE

タスクのステータスをクリアするために使用します。必要に応じて、この値を使用してコンソールからステータス メッセージを消去できます(たとえば、PROCESSING_COMPLETE を設定してから一定の時間が経過した後)。 例:

getExtensions().runtime().setProcessingState("NONE");

致命的なエラー

拡張機能の動作を妨げるエラー(必要な設定タスクの失敗など)が発生した場合は、setFatalError() を含む致命的なエラーを報告します。

import { getExtensions } from "firebase-admin/extensions";

// ...

getExtensions().runtime().setFatalError(`Post-installation setup failed. ${errorMessage}`);

タスクキューの調整

taskQueueTrigger プロパティを {} に設定すると、拡張機能インスタンスのインストール時に、デフォルト設定の Cloud Tasks キューがプロビジョニングされます。また、特定の値を指定することで、タスクキューの同時実行の上限と再試行の動作を調整できます。

resources:
  - name: myTaskFunction
    type: firebaseextensions.v1beta.function
    description: >-
      Perform a task when triggered by a lifecycle event
    properties:
      location: ${LOCATION}
      taskQueueTrigger:
        rateLimits:
          maxConcurrentDispatches: 1000
          maxDispatchesPerSecond: 500
        retryConfig:
          maxAttempts: 100  # Warning: setting this too low can prevent the function from running
          minBackoffSeconds: 0.1
          maxBackoffSeconds: 3600
          maxDoublings: 16
lifecycleEvents:
  onInstall: 
    function: myTaskFunction
    processingMessage: Resizing your existing images
  onUpdate:
    function: myTaskFunction
    processingMessage: Setting up your extension
  onConfigure:
    function: myOtherTaskFunction
    processingMessage: Setting up your extension

これらのパラメータの詳細については、Google Cloud ドキュメントの Cloud Tasks キューの構成をご覧ください。

タスクキューのパラメータを taskQueue() に渡して指定しないでください。これらの設定は無視され、extension.yaml の構成と構成のデフォルトが優先されます。

たとえば、次の場合は機能しません。

export const myBrokenTaskFunction = functions.tasks
  // DON'T DO THIS IN AN EXTENSION! THESE SETTINGS ARE IGNORED.
  .taskQueue({
    retryConfig: {
      maxAttempts: 5,
      minBackoffSeconds: 60,
    },
    rateLimits: {
      maxConcurrentDispatches: 1000,
      maxDispatchesPerSecond: 10,
    },
  })
  .onDispatch(
    // ...
  );

拡張機能のタスクキューを構成する唯一の方法は、extension.yamltaskQueueTrigger プロパティを使用することです。

公式の storage-resize-imagesfirestore-bigquery-exportfirestore-translate-text 拡張機能はすべてライフサイクル イベント ハンドラを使用して、データをバックフィルします。