Como criar workers persistentes

Informar um problema Acessar a origem

Workers permanentes podem deixar sua compilação mais rápida. Se você tiver ações repetidas no build que tenham um alto custo de inicialização ou que se beneficiem do armazenamento em cache entre ações, convém implementar seu próprio worker persistente para realizar essas ações.

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

A implementação do worker tem duas partes:

Tornar o worker

Um worker persistente segue alguns requisitos:

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

Se o programa atender a esses requisitos, ele vai 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 caminho/digest 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 camelCase nos exemplos JSON, mas snake 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 uma saída de depuração extra do worker. Cabe ao worker escolher o que gerar 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 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. Registros completos podem ser gravados no stderr do worker. Como os workers 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 o WorkRequest e o WorkResponse correspondente tenham o mesmo ID de solicitação. Portanto, esse ID precisa ser especificado se for diferente de zero. Esse valor é 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. 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, quando o servidor não envia outra solicitação até receber uma resposta (exceto no caso de solicitações de cancelamento, conforme abaixo).

Observações

  • Cada buffer de protocolo é precedido pelo tamanho 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 que o protobuf, mas usam JSON padrão e letras concatenadas para todos os nomes de campo.
  • Para manter as mesmas propriedades de compatibilidade de versões anteriores e futuras que o protobuf, os workers JSON precisam tolerar campos desconhecidos nessas mensagens e usar os padrões de protobuf para os 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 solicitações de trabalho sejam canceladas antes de serem concluídas. Isso é particularmente útil em relação à 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. 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, indicando qual solicitação cancelar. O campo request_id será 0 para workers singleplex ou o request_id não 0 de um WorkRequest enviado anteriormente para workers multiplex. O servidor pode enviar solicitações de cancelamento para solicitações que já foram respondidas pelo worker. 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, tendo sido cancelada ou não. Depois que o servidor enviar uma solicitação de cancelamento, o worker poderá responder com uma 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 no diretório de trabalho. O servidor é livre para 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 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.

Fazer referência ao worker

A regra que usa o worker precisa conter um campo que faça referência 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, essa poderá ser a regra associada:

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

Isso cria o rótulo "worker", que se refere ao binário. 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 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 execução na plataforma de execução, e não na plataforma de destino, ou seja, o worker é usado como ferramenta durante a criação.

Requisitos para ações de trabalho

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

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

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

    Os campos "argumentos" e "execution-requirements" são necessá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.

Presumindo 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 persistentes.

Exemplos

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

Use os scaffolding deles para transformar qualquer ferramenta baseada em Java em um worker transmitindo o callback correto.

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

Os colaboradores externos implementaram workers em várias linguagens. Dê uma olhada em Implementações no Polyglot de workers permanentes do Bazel. Veja muitos outros exemplos no GitHub (em inglês).