Os workers permanentes podem tornar seu build mais rápido. Se você tiver ações repetidas na versão que tenham um alto custo de inicialização ou que se beneficiem do armazenamento em cache de ação cruzada, implemente seu próprio worker permanente 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 tornar o worker
Um worker permanente cumpre alguns requisitos:
- Ele lê
WorkRequests
a partir da
stdin
. - Ele grava
WorkResponses
(e apenas
WorkResponse
s) nostdout
. - Ele aceita a sinalização
--persistent_worker
. O wrapper precisa reconhecer a sinalização de linha de comando--persistent_worker
e só se tornar permanente se essa sinalização for transmitida. Caso contrário, o app precisa fazer uma compilação única e sair.
Se o programa mantiver 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 por e-mail que representam as entradas que o worker pode acessar (não é aplicada, mas é possível usar essas informações para armazenamento em cache) e um ID de solicitação, que é 0 para workers únicos.
OBSERVAÇÃO: embora a especificação do buffer de protocolo use "caso de snake" (request_id
),
o protocolo JSON usa "concatenação" (requestId
). Este documento usa letras concatenadas
nos exemplos em JSON, mas usa letras maiúsculas 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 opcional verbosity
pode ser usado para solicitar outra saída de depuração
do worker. Depende exclusivamente do worker e de como gerar resultados. 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 multiplex.
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
string de saída que descreve todos 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 workers só podem gravar
WorkResponses
em stdout
, é comum que eles redirecionem stdout
de qualquer ferramenta usada 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 o padrão para protobufs, todos os campos são opcionais. No entanto, o Bazel exige que o WorkRequest
e o WorkResponse
correspondentes tenham o mesmo ID de solicitação. Portanto, o ID da solicitação precisa ser especificado se não for zero. Esta é uma
WorkResponse
válida.
{
"requestId" : 12,
}
Um request_id
de 0 indica uma solicitação de "singleplex", 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 do Singleplex são enviadas em série, por
exemplo, se o servidor não enviar outra solicitação até que receba uma
resposta (exceto para solicitações de cancelamento, veja abaixo).
Observações
- Cada buffer de protocolo é precedido pelo tamanho no formato
varint
(consulteMessageLite.writeDelimitedTo()
). - 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 letras concatenadas para todos os nomes de campo.
- Para manter as mesmas propriedades de compatibilidade com versões anteriores e posteriores, 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 de protobuf
Cancelamento
Os workers podem permitir que as solicitações de trabalho sejam canceladas antes de serem concluídas.
Isso é particularmente útil na conexão com a execução dinâmica, em que a execução local
pode ser interrompida com frequência 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. Da mesma forma, uma resposta de cancelamento é um WorkResponse
com o conjunto was_cancelled
. O único outro campo que precisa estar em uma solicitação ou
resposta de cancelamento é request_id
, indicando qual solicitação cancelar. O campo request_id
será 0 para workers únicos ou o request_id
diferente de 0 de um WorkRequest
enviado anteriormente para workers multiplex. O servidor pode enviar solicitações de cancelamento
para as que o worker já respondeu. Nesse caso, a solicitação
de cancelamento precisa ser ignorada.
Cada mensagem WorkRequest
sem cancelamento precisa ser respondida exatamente uma vez, independentemente de ter sido ou não cancelada. Depois que o servidor enviar uma solicitação de cancelamento, o worker poderá
responder com um WorkResponse
usando o conjunto request_id
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 for enviada para uma WorkRequest
, o worker não poderá tocar nos
arquivos no diretório de trabalho. O servidor pode limpar os arquivos,
incluindo os temporários.
Como 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 ele produz.
Como se referir ao worker
A regra que usa o worker precisa conter um campo que se refere ao próprio worker.
Portanto, é preciso criar uma instância de uma regra \*\_binary
para definir o worker. Se o worker for chamado 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, defina uma regra que usa o worker. Essa regra precisa definir um atributo que faça referência ao binário do worker.
Se o binário de 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 criação.
Requisitos da ação de trabalho
A regra que usa o worker cria ações para que ele execute. Essas ações têm alguns requisitos.
O campo "arguments". Isso usa uma lista de strings, incluindo a última, argumentos transmitidos ao worker na inicialização. O último elemento na lista "argumentos" é um argumento
flag-file
(precedido de @). Os workers leem os argumentos do flagfile especificado por WorkWork. Sua regra pode gravar argumentos não inicializados para o worker neste arquivo de sinalização.O campo "execution-requirements", que leva um dicionário contendo
"supports-workers" : "1"
,"supports-multiplex-workers" : "1"
ou ambos.Os campos "argumentos" e "requisitos de execução" são obrigatórios para todas as ações enviadas aos workers. Além disso, as ações que precisam ser executadas pelos 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 do 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 desse worker.Os arquivos temporários gerados durante a ação precisam ser salvos no diretório do worker. Isso permite o uso do sandbox.
Pressupondo uma definição de regra com o atributo "worker" descrito acima, além de um atributo "srcs" representando as entradas, um atributo "output" representando as saídas e um atributo "args" representando 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 de compilador Java, além de um exemplo de worker JSON usado em nossos testes de integração.
Use o scaffolding (em inglês) para transformar qualquer ferramenta baseada em Java em um worker ao transmitir o callback correto.
Para ver um exemplo de uma regra que usa um worker, veja o teste de integração do worker do Bazel.
Os colaboradores externos implementaram workers em várias linguagens. Dê uma olhada nas implementações da Polyglot de workers permanentes do Bazel. Veja muitos outros exemplos no GitHub.