Os workers persistentes podem acelerar o build. Se você tiver ações repetidas no build com um custo de inicialização alto 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:
- O worker.
- A regra que usa o worker.
Como criar o worker
Um worker persistente precisa atender a alguns requisitos:
- Ele lê
WorkRequests
do
stdin
. - Ele grava
WorkResponses
(e apenas
WorkResponse
s) nostdout
. - Ele 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 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 uma saída de depuração extra
do worker. É totalmente por conta do worker 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 a
sandboxes múltiplas.
Respostas do trabalho
Um WorkResponse
contém um ID de solicitação, um código de saída igual 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. Os registros completos podem
ser gravados no stderr
do worker. Como os trabalhadores só podem gravar
WorkResponses
em stdout
, é comum que eles redirecionem o stdout
de todas as 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 exige
que WorkRequest
e WorkResponse
correspondente tenham o mesmo ID
de solicitação. Portanto, o ID de solicitação precisa ser especificado se for diferente de zero. Esse é um
WorkResponse
válido.
{
"requestId" : 12,
}
Um request_id
de 0 indica uma solicitação "simplex", usada quando essa solicitação
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 simples são enviadas em série, por
exemplo, se o servidor não enviar outra solicitação até receber uma
resposta (exceto para solicitações de cancelamento, consulte abaixo).
Observações
- Cada buffer de protocolo é precedido pelo comprimento no formato
varint
(consulteMessageLite.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 para versões anteriores e futuras que o 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 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 é uma WorkRequest
com o campo cancel
definido. Da mesma forma, uma resposta de cancelamento é uma WorkResponse
com o campo was_cancelled
definido. O único outro campo que precisa estar em uma solicitação de cancelamento ou resposta
de cancelamento é request_id
, que indica qual solicitação 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 worker 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 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 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 pode limpar os arquivos,
incluindo os 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 próprio worker.
Portanto, você vai precisar criar uma instância de uma regra \*\_binary
para definir
o 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ê vai definir uma regra que usa 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 ser executado na
plataforma de execução em vez da plataforma de destino (ou seja, o worker é usado
como ferramenta durante o build).
Requisitos de ação no trabalho
A regra que usa o worker cria ações para ele realizar. Essas ações 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. Sua regra pode gravar argumentos não de inicialização para o worker neste arquivo de flag.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 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 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 o teste de integração de workers do Bazel.
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.