Processar os eventos de ciclo de vida da extensão

Sua extensão pode incluir funções do Cloud Tasks que são acionadas quando uma instância de extensão passa por um dos seguintes eventos de ciclo de vida:

  • Uma instância da extensão está instalada
  • Uma instância da extensão foi atualizada para uma nova versão
  • A configuração de uma instância de extensão é alterada

Um dos casos de uso mais importantes desse recurso é o preenchimento de dados. Por exemplo, suponha que você esteja criando uma extensão que gere visualizações em miniatura das imagens enviadas a um bucket do Cloud Storage. O principal trabalho da sua extensão seria feito em uma função acionada pelo evento onFinalize Cloud Storage. No entanto, apenas as imagens enviadas após a instalação da extensão serão processadas. Ao incluir na sua extensão uma função acionada pelo evento de ciclo de vida onInstall, você também pode gerar visualizações em miniatura de qualquer imagem existente quando a extensão estiver instalada.

Alguns outros casos de uso de acionadores de eventos do ciclo de vida incluem:

  • Automatizar a configuração pós-instalação (criação de registros do banco de dados, indexação etc.)
  • Se você precisar publicar alterações incompatíveis com versões anteriores, migre os dados automaticamente

Manipuladores de eventos de ciclo de vida de curta duração

Se a tarefa puder ser executada completamente dentro da duração máxima do Cloud Functions (9 minutos usando a API de primeira geração), será possível escrever o manipulador de eventos de ciclo de vida como uma única função que é acionado no evento onDispatch da fila de tarefas:

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

Em seguida, no arquivo extension.yaml da sua extensão, faça o seguinte:

  1. Registre sua função como um recurso de extensão com o conjunto de propriedades taskQueueTrigger. Se você definir taskQueueTrigger como o mapa vazio ({}), sua extensão provisionará uma fila do Cloud Tasks usando as configurações padrão. É possível ajustar essas configurações.

    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. Registre sua função como um manipulador para um ou mais eventos de 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
    
    

    É possível registrar funções para qualquer um dos seguintes eventos: onInstall, onUpdate e onConfigure. Todos esses eventos são opcionais.

  3. Recomendado: se a tarefa de processamento não for necessária para a extensão funcionar, adicione um parâmetro configurado pelo usuário que permita aos usuários escolher se ela será ativada.

    Por exemplo, adicione um parâmetro como o seguinte:

    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
    

    Na sua função, se o parâmetro estiver definido como false, saia antecipadamente:

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

Como executar tarefas de longa duração

Se não for possível concluir sua tarefa dentro da duração máxima do Cloud Functions, divida-a em subtarefas e realize cada subtarefa em sequência enfileirando os jobs com o método TaskQueue.enqueue() do SDK Admin.

Por exemplo, suponha que você queira preencher dados de Cloud Firestore. É possível dividir a coleção de documentos em partes usando cursores de consulta. Depois de processar um bloco, avance o deslocamento inicial e enfileire outra invocação de função, conforme mostrado abaixo:

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

Adicione a função ao extension.yaml, conforme descrito na seção anterior.

Status do relatório

Quando todas as funções de processamento forem concluídas, seja com êxito ou com um erro, informe o status da tarefa usando os métodos de ambiente de execução da extensão do SDK Admin. Os usuários podem conferir esse status na página de detalhes da extensão no console do Firebase.

Conclusão e erros não fatais

Para relatar erros de conclusão e não fatal (erros que não colocam a extensão em um estado não funcional), use o método de ambiente de execução da extensão setProcessingState() do SDK Admin:

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

// ...

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

É possível definir os seguintes estados:

Estados não fatais
PROCESSING_COMPLETE

Use para informar a conclusão de uma tarefa. Exemplo:

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

Use para informar sucesso parcial. Exemplo:

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

Use para relatar erros que impedem a conclusão da tarefa, mas não deixam a extensão inutilizável. Exemplo:

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

Para informar erros que deixam a extensão inutilizável, chame setFatalError().

NONE

Use para limpar o status da tarefa. Opcionalmente, você pode usar para limpar a mensagem de status do console (por exemplo, depois de um certo tempo desde a configuração do PROCESSING_COMPLETE). Exemplo:

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

Erros fatais

Se ocorrer um erro que impede o funcionamento da extensão, por exemplo, se uma tarefa obrigatória de configuração falhar, informe o erro fatal com setFatalError():

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

// ...

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

Como ajustar a fila de tarefas

Se você definir a propriedade taskQueueTrigger como {}, sua extensão vai provisionar uma fila do Cloud Tasks com as configurações padrão, quando uma instância de extensão for instalada. Como alternativa, é possível ajustar os limites de simultaneidade da fila de tarefas e o comportamento de repetição fornecendo 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

Consulte Configurar filas do Cloud Tasks nos documentos do Google Cloud para conferir detalhes sobre esses parâmetros.

Não tente especificar parâmetros da fila de tarefas passando-os para taskQueue(). Essas configurações são ignoradas em favor da configuração em extension.yaml e os padrões de configuração.

Por exemplo, esta abordagem não vai 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(
    // ...
  );

A propriedade taskQueueTrigger em extension.yaml é a única maneira de configurar as filas de tarefas de uma extensão.

Exemplos

As extensões oficiais storage-resize-images, firestore-bigquery-export e firestore-translate-text usam de eventos de ciclo de vida para preencher dados.