Controla los eventos de ciclo de vida de tu extensión

Tu extensión puede incluir funciones de Cloud Tasks que se activan cuando una instancia de extensión pasa por cualquiera de los siguientes eventos del ciclo de vida:

  • Cuando se instala una instancia de la extensión
  • Cuando se actualiza una instancia de la extensión a una versión nueva
  • Cuando se cambia la configuración de una instancia de extensión

Uno de los casos de uso más importantes de esta función es el reabastecimiento de datos. Por ejemplo, supongamos que vas a compilar una extensión que genera vistas previas en miniatura de las imágenes subidas a un bucket de Cloud Storage. El trabajo principal de la extensión se realizaría en una función activada por el evento Cloud Storage de onFinalize. Sin embargo, solo se procesarán las imágenes subidas después de que se instale la extensión. Cuando incluyes en tu extensión una función activada por el evento del ciclo de vida de onInstall, también puedes generar vistas previas en miniatura de cualquier imagen existente cuando se instale la extensión.

Estos son otros casos de uso de los activadores de eventos de ciclo de vida:

  • Automatizar la configuración posterior a la instalación (crear registros de base de datos, indexación, etcétera)
  • Migrar automáticamente los datos cuando se actualicen (si tienes que publicar cambios que son incompatibles con versiones anteriores)

Controladores de eventos de ciclo de vida de corta duración

Si tu tarea puede ejecutarse completamente dentro de la duración máxima de Cloud Functions (9 minutos con la API de primera generación), puedes escribir el controlador de eventos de ciclo de vida como una sola función. que se activa en el evento onDispatch de la lista de tareas en cola:

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

Luego, haz lo siguiente en el archivo extension.yaml de tu extensión:

  1. Registra tu función como un recurso de extensión con el conjunto de propiedades taskQueueTrigger. Si configuras taskQueueTrigger en la asignación vacía ({}), tu extensión aprovisionará una cola de Cloud Tasks con la configuración predeterminada. De manera opcional, puedes ajustar esta configuración.

    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. Registra tu función como controlador de uno o más eventos del ciclo de vida:

    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
    
    

    Puedes registrar funciones para cualquiera de los siguientes eventos: onInstall, onUpdate y onConfigure. Todos estos eventos son opcionales.

  3. Recomendación: Si no se requiere la tarea de procesamiento para que funcione la extensión, agrega un parámetro configurado por el usuario que permita a los usuarios elegir si desean habilitarla.

    Por ejemplo, agrega un parámetro como el siguiente:

    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
    

    También, si en la función el parámetro está configurado como false, sal antes:

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

Realiza tareas de larga duración

Si tu tarea no se puede completar dentro de la duración máxima de Cloud Functions, divídela en subtareas y realiza cada una en secuencia. Para ello, coloca trabajos en cola con el método TaskQueue.enqueue() del SDK de Admin.

Por ejemplo, supongamos que deseas reabastecer los datos de Cloud Firestore. Puedes dividir la colección de documentos en fragmentos con cursores de consulta. Después de procesar un fragmento, avanza el desplazamiento inicial y pon en cola otra invocación de función como se muestra a continuación:

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

Agrega la función a tu extension.yaml como se describió en la sección anterior.

Estado de los informes

Cuando se completen todas tus funciones de procesamiento, ya sea de forma correcta o con un error, informa el estado de la tarea con los métodos de tiempo de ejecución de la extensión del SDK de Admin. Los usuarios pueden ver este estado en la página de detalles de la extensión en Firebase console.

Finalización correcta y errores recuperables

Para informar que la extensión se completó correctamente y los errores recuperables (errores que no dejan la extensión en un estado no funcional), usa el método setProcessingState() de tiempo de ejecución de la extensión del SDK de Admin:

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

// ...

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

Puedes configurar los siguientes estados:

Estados recuperables
PROCESSING_COMPLETE

Se usa para informar que la tarea se completó correctamente. Ejemplo:

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

Se usa para informar el éxito parcial. Ejemplo:

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

Se usa para informar errores que impiden que la tarea se complete, pero que no dejan la extensión inutilizable. Ejemplo:

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

Para informar errores que dejan la extensión inutilizable, llama a setFatalError().

NONE

Se usa para borrar el estado de la tarea. De forma opcional, puedes usar esto para borrar el mensaje de estado de la consola (por ejemplo, después de que haya transcurrido un tiempo después de configurar PROCESSING_COMPLETE). Ejemplo:

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

Errores no recuperables

Si se produce un error que impide el funcionamiento de la extensión, por ejemplo, una tarea de configuración requerida con errores, informa el error no recuperable con setFatalError():

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

// ...

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

Ajusta la lista de tareas en cola

Si configuras la propiedad taskQueueTrigger en {}, tu extensión aprovisionará una cola de Cloud Tasks con la configuración predeterminada cuando se instale una instancia de extensión. Como alternativa, puedes ajustar los límites de simultaneidad de la lista de tareas en cola y reintentar el comportamiento proporcionando valores específicos:

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

Consulta Configura colas de Cloud Tasks en los documentos de Google Cloud para obtener detalles sobre estos parámetros.

No intentes especificar parámetros de la lista de tareas en cola pasándolos a taskQueue(). Estos parámetros de configuración se ignoran para dar lugar a la configuración en extension.yaml y los valores predeterminados de la configuración.

Por ejemplo, esto no funcionará:

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(
    // ...
  );

La propiedad taskQueueTrigger en extension.yaml es la única forma de configurar las listas de tareas en cola de una extensión.

Ejemplos

Las extensiones oficiales de storage-resize-images, firestore-bigquery-export y firestore-translate-text usan controladores del evento de ciclo de vida para reabastecer datos.