Como criar workers persistentes

Informar um problema Ver código-fonte Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Os workers persistentes podem acelerar o build. Se você tiver ações repetidas no build com um alto custo de inicialização ou que 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 oferece suporte ao 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:

  • Leitura WorkRequests do stdin.
  • Ele grava WorkResponses (e apenas WorkResponses) no stdout.
  • Ele aceita a flag --persistent_worker. O wrapper precisa reconhecer Sinalização de linha de comando --persistent_worker e só se torna persistente se a sinalização é transmitida. Caso contrário, ela precisa fazer uma compilação única e sair.

Se o seu programa cumprir esses requisitos, ele poderá ser usado como um serviço trabalhador!

Solicitações de trabalho

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

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, independentemente 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 verbosity opcional pode ser usado para solicitar outras saídas de depuração do worker. Cabe inteiramente ao worker o que e como produzir. Valores mais altos indicam uma saída mais detalhada. Transmitir a sinalização --worker_verbose para O Bazel define o campo verbosity como 10, mas é possível usar valores menores ou maiores. manualmente para diferentes quantidades de saída.

O campo sandbox_dir opcional é usado apenas por workers compatíveis sandbox de multiplex.

Respostas de trabalho

Uma WorkResponse contém um ID de solicitação, um código de saída zero ou diferente de zero e uma string de saída que descreve os erros encontrados no processamento ou na execução da solicitação. O campo output contém uma breve descrição. registros completos podem ser gravadas no stderr do worker. Como os workers só podem gravar WorkResponses para stdout, é comum que o worker redirecione o stdout das ferramentas usadas para stderr.

{
  "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 requer o WorkRequest e o WorkResponse correspondente, para ter a mesma solicitação id , portanto, o ID da solicitação deverá ser especificado se for diferente de zero. Este é um endereço de e-mail WorkResponse:

{
  "requestId" : 12,
}

Um request_id de 0 indica um "singleplex". , usada quando esta solicitação não podem ser processados em paralelo com outras solicitações. O servidor garante que um determinado worker recebe solicitações com apenas request_id 0 ou apenas request_id maior que zero. As solicitações Singleplex são enviadas em série para exemplo se o servidor não enviar outra solicitação até que tenha recebido uma (exceto para solicitações de cancelamento, veja 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 tê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 posteriores como protobuf, os workers JSON precisam tolerar campos desconhecidos nessas mensagens, e usar os padrões protobuf para valores ausentes.
  • O Bazel armazena solicitações como protobufs e as converte em JSON usando o formato JSON do protobuf.

Cancelamento

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

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

Cada mensagem WorkRequest não cancelada precisa ser respondida exatamente uma vez, independentemente de ser ou não cancelada. Depois que o servidor enviar uma solicitação de cancelamento, o worker poderá responda com uma WorkResponse com a request_id definida e a was_cancelled. definido como verdadeiro. O envio de um WorkResponse regular também é aceito, 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 é sem custo financeiro para limpar os arquivos, incluindo arquivos temporários.

Criar a regra que usa o worker

Você também vai precisar criar uma regra que gere ações a serem realizadas pelo trabalhador. 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 worker e há alguns requisitos para as ações que ele produz.

Como se referir ao worker

A regra que usa o worker precisa conter um campo que se refira ao worker portanto, você precisa criar uma instância de uma regra \*\_binary para definir seu worker. Se o worker se chama 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 worker binário. Em seguida, você defina uma regra que use o worker. Essa regra precisa definir um atributo que se refira ao worker binário.

Se o binário do worker criado estiver em um pacote chamado "work", que está no nível superior do build, esta pode ser a definição do atributo:

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

cfg = "exec" indica que o worker precisa ser criado para execução no seu plataforma de execução em vez de 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. Esses têm alguns requisitos.

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

  • O campo "execution-requirements", que recebe 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 workers JSON precisam incluir "requires-worker-protocol" : "json" no campo de requisitos de execução. "requires-worker-protocol" : "proto" também é um requisito de execução válido, embora não seja 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 quer distinguir as ações por esse worker.

  • Os arquivos temporários gerados durante a ação devem ser salvos do worker. Isso ativa o sandbox.

Supondo uma definição de regra com "worker" o atributo descrito acima, além em "srcs", que representa as entradas, uma "saída" atributo que representa as saídas, e um "args" que representa o worker startup, 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 worker JSON de exemplo 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 conferir um exemplo de regra que usa um worker, consulte a teste de integração do worker.

Colaboradores externos implementaram workers em vários idiomas. Confira implementações poliglotas de workers persistentes do Bazel. Você pode encontrar muitos outros exemplos no GitHub.