Como criar workers persistentes

Informar um problema Ver fonte Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

Os workers persistentes podem acelerar seu build. Se você tiver ações repetidas no build que tenham um alto custo de inicialização ou se beneficiariam do armazenamento em cache entre ações, implemente seu próprio worker persistente para realizar essas ações.

O servidor do Bazel se comunica com o worker usando stdin/stdout. Ele aceita o uso de buffers de protocolo ou strings JSON.

A implementação do worker tem duas partes:

Como criar o worker

Um worker persistente atende a alguns requisitos:

  • Ele lê WorkRequests do stdin.
  • Ele grava WorkResponses (e apenas WorkResponses) no stdout.
  • Ela aceita a flag --persistent_worker. O wrapper precisa reconhecer a flag de linha de comando --persistent_worker e só se tornar persistente se essa flag for transmitida. Caso contrário, ele precisa fazer uma compilação única e sair.

Se o programa atender a esses requisitos, ele poderá ser usado como um worker persistente.

Solicitações de trabalho

Um WorkRequest contém uma lista de argumentos para o worker, uma lista de pares caminho-resumo que representam as entradas a que o worker pode acessar (isso não é aplicado, mas você pode usar essas informações para armazenamento em cache) e um ID de solicitação, que é 0 para workers simplex.

OBSERVAÇÃO: embora a especificação do buffer de protocolo use "snake case" (request_id), o protocolo JSON usa "camel case" (requestId). Este documento usa camel case nos exemplos JSON, mas snake case ao falar sobre o campo, independente do protocolo.

{
  "arguments" : ["--some_argument"],
  "inputs" : [
    { "path": "/path/to/my/file/1", "digest": "fdk3e2ml23d"},
    { "path": "/path/to/my/file/2", "digest": "1fwqd4qdd" }
 ],
  "requestId" : 12
}

O campo opcional verbosity pode ser usado para solicitar saída de depuração extra do worker. Cabe totalmente ao worker decidir o que e como gerar a saída. Valores mais altos indicam uma saída mais detalhada. A transmissão da flag --worker_verbose para o Bazel define o campo verbosity como 10, mas valores menores ou maiores podem ser usados manualmente para diferentes quantidades de saída.

O campo opcional sandbox_dir é usado apenas por workers que oferecem suporte ao multiplex sandboxing.

Respostas de trabalho

Um WorkResponse contém um ID de solicitação, um código de saída zero ou diferente de zero e uma mensagem de saída que descreve os erros encontrados no processamento ou na execução da solicitação. Um worker precisa capturar o stdout e o stderr de qualquer ferramenta que ele chame e informar esses dados pelo WorkResponse. Escrever no stdout do processo de worker não é seguro, porque isso interfere no protocolo do worker. Gravá-lo no stderr do processo de worker é seguro, mas o resultado é coletado em um arquivo de registro por worker, em vez de ser atribuído a ações individuais.

{
  "exitCode" : 1,
  "output" : "Action failed with the following message:\nCould not find input
    file \"/path/to/my/file/1\"",
  "requestId" : 12
}

De acordo com a norma para protobufs, todos os campos são opcionais. No entanto, o Bazel exige que o WorkRequest e o WorkResponse correspondente tenham o mesmo ID de solicitação. Portanto, o ID precisa ser especificado se for diferente de zero. Este é um WorkResponse válido.

{
  "requestId" : 12,
}

Um request_id de 0 indica uma solicitação "singleplex", usada quando ela não pode ser processada em paralelo com outras solicitações. O servidor garante que um determinado worker receba solicitações com apenas request_id 0 ou apenas request_id maior que zero. As solicitações singleplex são enviadas em série. Por exemplo, se o servidor não enviar outra solicitação até receber uma resposta (exceto solicitações de cancelamento, consulte abaixo).

Observações

  • Cada buffer de protocolo é precedido pelo comprimento no formato varint (consulte MessageLite.writeDelimitedTo().
  • As solicitações e respostas JSON não são precedidas por um indicador de tamanho.
  • As solicitações JSON mantêm a mesma estrutura do protobuf, mas usam JSON padrão e camel case para todos os nomes de campo.
  • Para manter as mesmas propriedades de compatibilidade com versões anteriores e futuras do protobuf, os workers JSON precisam tolerar campos desconhecidos nessas mensagens e usar os padrões do protobuf para valores ausentes.
  • O Bazel armazena solicitações como protobufs e as converte em JSON usando o formato JSON do protobuf.

Cancelamento

Os trabalhadores podem permitir que os pedidos de trabalho sejam cancelados antes de serem concluídos. Isso é particularmente útil em conexão com a execução dinâmica, em que a execução local pode ser interrompida regularmente por uma execução remota mais rápida. Para permitir o cancelamento, adicione supports-worker-cancellation: 1 ao campo execution-requirements (veja abaixo) e defina a flag --experimental_worker_cancellation.

Uma solicitação de cancelamento é um WorkRequest com o campo cancel definido. Da mesma forma, uma resposta de cancelamento é um WorkResponse com o campo was_cancelled definido. O único outro campo que precisa estar em uma solicitação ou resposta de cancelamento é request_id, que indica qual solicitação cancelar. O campo request_id será 0 para workers simplex ou o request_id diferente de zero de um WorkRequest enviado anteriormente para workers multiplex. O servidor pode enviar solicitações de cancelamento para solicitações que o worker já respondeu. Nesse caso, a solicitação de cancelamento precisa ser ignorada.

Cada mensagem WorkRequest que não seja de cancelamento precisa ser respondida exatamente uma vez, tenha sido cancelada ou não. Depois que o servidor envia uma solicitação de cancelamento, o worker pode responder com um WorkResponse com o request_id definido e o campo was_cancelled definido como "true". Também é aceito enviar um WorkResponse regular, mas os campos output e exit_code serão ignorados.

Depois que uma resposta é enviada para um WorkRequest, o worker não pode tocar nos arquivos no diretório de trabalho. O servidor pode limpar os arquivos, incluindo os temporários.

Criando a regra que usa o trabalhador

Você também precisará criar uma regra que gere ações a serem realizadas pelo worker. Criar uma regra do Starlark que usa um worker é como criar qualquer outra regra.

Além disso, a regra precisa conter uma referência ao próprio trabalhador, e há alguns requisitos para as ações que ela produz.

Referência ao trabalhador

A regra que usa o worker precisa conter um campo que se refira ao próprio worker. Portanto, é necessário criar uma instância de uma regra \*\_binary para definir o worker. Se o worker se chamar MyWorker.Java, esta pode ser a regra associada:

java_binary(
    name = "worker",
    srcs = ["MyWorker.Java"],
)

Isso cria o rótulo "worker", que se refere ao binário do worker. Em seguida, você vai definir uma regra que usa o worker. Essa regra precisa definir um atributo que se refere ao binário do worker.

Se o binário do worker que você criou estiver em um pacote chamado "work", que está no nível superior da build, esta poderá ser a definição de atributo:

"worker": attr.label(
    default = Label("//work:worker"),
    executable = True,
    cfg = "exec",
)

cfg = "exec" indica que o worker precisa ser criado para ser executado na sua plataforma de execução, e não na plataforma de destino (ou seja, o worker é usado como ferramenta durante o build).

Requisitos de ação de trabalho

A regra que usa o worker cria ações para ele realizar. Essas ações têm alguns requisitos.

  • O campo "arguments". Isso usa uma lista de strings, todas, exceto a última, são argumentos transmitidos ao worker na inicialização. O último elemento na lista "arguments" é um argumento flag-file (precedido por @). Os workers leem os argumentos do arquivo de flags especificado por WorkRequest. Sua regra pode gravar argumentos que não são de inicialização para o worker nesse arquivo de flags.

  • O campo "execution-requirements", que usa um dicionário contendo "supports-workers" : "1", "supports-multiplex-workers" : "1" ou ambos.

    Os campos "arguments" e "execution-requirements" são obrigatórios para todas as ações enviadas aos workers. Além disso, as ações que precisam ser executadas por trabalhadores JSON precisam incluir "requires-worker-protocol" : "json" no campo requisitos de execução. "requires-worker-protocol" : "proto" também é um requisito de execução válido, mas não é necessário para workers proto, já que eles são o padrão.

    Também é possível definir um worker-key-mnemonic nos requisitos de execução. Isso pode ser útil se você estiver reutilizando o executável para vários tipos de ação e quiser distinguir as ações por esse worker.

  • Os arquivos temporários gerados durante a ação precisam ser salvos no diretório do worker. Isso ativa o sandbox.

Supondo uma definição de regra com o atributo "worker" descrito acima, além de um atributo "srcs" que representa as entradas, um atributo "output" que representa as saídas e um atributo "args" que representa os argumentos de inicialização do worker, a chamada para ctx.actions.run pode ser:

ctx.actions.run(
  inputs=ctx.files.srcs,
  outputs=[ctx.outputs.output],
  executable=ctx.executable.worker,
  mnemonic="someMnemonic",
  execution_requirements={
    "supports-workers" : "1",
    "requires-worker-protocol" : "json"},
  arguments=ctx.attr.args + ["@flagfile"]
 )

Para outro exemplo, consulte Implementar workers persistentes.

Exemplos

A base de código do Bazel usa workers do compilador Java, além de um exemplo de worker JSON usado nos nossos testes de integração.

É possível usar o scaffolding para transformar qualquer ferramenta baseada em Java em um worker transmitindo o callback correto.

Para um exemplo de regra que usa um worker, confira o teste de integração de worker do Bazel.

Colaboradores externos implementaram workers em várias linguagens. Confira as implementações políglotas de workers permanentes do Bazel. Você pode encontrar muitos outros exemplos no GitHub.