Workers permanentes podem tornar seu build mais rápido. Se você tiver ações repetidas no build com alto custo de inicialização ou que se beneficiem do armazenamento em cache entre ações, implemente seu próprio worker permanente para realizar essas ações.
O servidor do Bazel se comunica com o worker usando stdin
/stdout
e
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.
Tornar o worker
Os workers permanentes cumprem alguns requisitos:
- Ele lê
WorkRequests
do
stdin
. - Ele grava WorkResponses (e apenas
WorkResponse
s) no respectivostdout
. - Ela aceita a sinalização
--persistent_worker
. O wrapper precisa reconhecer a sinalização de linha de comando--persistent_worker
e se tornar persistente se essa sinalização for transmitida. Caso contrário, ele precisará fazer uma compilação única e sair.
Se o programa atender a esses requisitos, ele poderá ser usado como um worker permanente.
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 é
aplicado, mas é possível usar essas informações para armazenamento 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 "camelCase" (requestId
). Este documento usa a concatenação
nos exemplos do JSON, mas o uso de snake case
é usado para 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 a saída de depuração extra
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 valores menores ou maiores podem ser usados
manualmente para diferentes quantidades de saída.
O campo opcional sandbox_dir
é usado apenas por workers compatíveis com o sandbox de multiplex.
Respostas de trabalho
Um WorkResponse
contém um ID da solicitação, um código de saída zero ou diferente de zero e uma mensagem de saída descrevendo todos os erros encontrados no processamento ou na execução da solicitação. Um worker precisa capturar o stdout
e o stderr
de qualquer ferramenta
chamada e informá-los pelo WorkResponse
. Não é seguro gravá-lo no stdout
do
processo do worker, porque isso interferirá no protocolo do worker.
Gravá-lo no stderr
do processo do worker é seguro, mas o resultado é
coletado em um arquivo de registro por worker, em vez de 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 WorkRequest
e o WorkResponse
correspondente tenham o mesmo ID de solicitação. Portanto, ele precisará ser especificado se for diferente de zero. Esse é 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 para solicitações de cancelamento, confira abaixo).
Notes
- Cada buffer de protocolo é precedido pelo tamanho 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 mantêm a mesma estrutura que o protobuf, mas usam o JSON padrão e o padrão CamelCase para todos os nomes de campo.
- Para manter as mesmas propriedades de compatibilidade com versões anteriores e posteriores 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 (em inglês).
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
(veja abaixo) e defina a sinalização --experimental_worker_cancellation
.
Uma solicitação de cancelamento é um WorkRequest
com o campo cancel
definido (e
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
, indicando qual solicitação será cancelada. O campo request_id
será 0 para workers singleplex ou o 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
que não é de cancelamento precisa ser respondida exatamente uma vez, independentemente de ter 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 verdadeiro. O envio de um WorkResponse
normal também é aceito, mas os campos output
e exit_code
serão ignorados.
Depois que uma resposta for enviada para um WorkRequest
, o worker não poderá tocar nos
arquivos do diretório de trabalho. O servidor é sem custo financeiro para limpar os arquivos, inclusive os temporários.
Criar a regra que usa o worker
Também será necessário 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 worker e há alguns requisitos para as ações que ela produz.
Como se referir ao worker
A regra que usa o worker precisa conter um campo que se refira ao próprio worker. Portanto, será necessário criar uma instância de uma regra \*\_binary
para defini-lo. Se o worker for chamado de MyWorker.Java
, esta poderá 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 refira 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 do build, essa 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 e não na plataforma de destino (ou seja, o worker é usado como ferramenta durante a compilação).
Requisitos de ação de trabalho
A regra que usa o worker cria ações para ele executar. Essas ações têm alguns requisitos.
O campo "arguments". Esse comando usa uma lista de strings. Todas, exceto a última, são argumentos transmitidos ao worker na inicialização. O último elemento na lista "argumentos" é um argumento
flag-file
(com @). Os workers leem os argumentos do arquivo de sinalização especificado por WorkRequest. A regra pode gravar argumentos que não sejam de inicialização para o worker nesse arquivo de sinalização.O campo "execution-requirements", que usa um dicionário que contém
"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 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 desse 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 ver outro exemplo, consulte Como implementar workers permanentes.
Exemplos
A base de código do Bazel usa workers do compilador Java e um exemplo de worker JSON usado nos nossos testes de integração.
É possível usar a scaffolding delas para transformar qualquer ferramenta baseada em Java em um worker transmitindo o callback correto.
Para ver um exemplo de regra que usa um worker, dê uma olhada no teste de integração de workers do Bazel.
Os colaboradores externos implementaram workers em várias linguagens. Consulte as Implementações da Polyglot sobre workers permanentes do Bazel. Encontre muitos outros exemplos no GitHub (link em inglês).