Cómo crear trabajadores persistentes

Informa un problema Ver código fuente

Los trabajadores persistentes pueden hacer que tu compilación sea más rápida. Si tienes acciones repetidas en tu compilación que tienen un costo de inicio alto o que te beneficiarían del almacenamiento en caché entre acciones, es posible que desees implementar tu propio trabajador persistente para realizar estas acciones.

El servidor de Bazel se comunica con el trabajador mediante stdin o stdout. Admite el uso de búferes de protocolo o strings JSON.

La implementación del trabajador tiene dos partes:

Convirtiendo al trabajador

Un trabajador persistente mantiene algunos requisitos:

  • Lee WorkRequests desde su stdin.
  • Escribe WorkResponses (y solo WorkResponse) en su stdout.
  • Acepta la marca --persistent_worker. El wrapper debe reconocer la marca --persistent_worker de la línea de comandos y solo ser persistente si se pasa esa marca; de lo contrario, debe realizar una compilación única y salir.

Si tu programa cumple con estos requisitos, se puede usar como un trabajador persistente.

Solicitudes de trabajo

Un WorkRequest contiene una lista de argumentos para el trabajador, una lista de pares de resumen de ruta que representan las entradas a las que puede acceder el trabajador (esto no se aplica, pero puedes usar esta información para almacenar en caché) y un ID de solicitud, que es 0 para los trabajadores singleplex.

NOTA: Si bien la especificación del búfer de protocolo usa el “caso de snake” (request_id), el protocolo JSON usa el “caso de camellos” (requestId). En este documento, se usa el formato de mayúsculas y minúsculas en los ejemplos de JSON, pero el caso de Snake cuando se habla sobre el campo independientemente del protocolo.

{
  "arguments" : ["--some_argument"],
  "inputs" : [
    { "path": "/path/to/my/file/1", "digest": "fdk3e2ml23d"},
    { "path": "/path/to/my/file/2", "digest": "1fwqd4qdd" }
 ],
  "requestId" : 12
}

El campo opcional verbosity se puede usar para solicitar al trabajador resultados de depuración adicionales. El trabajador debe decidir qué y cómo generar. Los valores más altos indican una salida más detallada. Pasar la marca --worker_verbose a Bazel establece el campo verbosity en 10, pero se pueden usar valores más pequeños o más grandes de forma manual para diferentes cantidades de resultados.

Solo los trabajadores que admiten la zona de pruebas multiplex usan el campo opcional sandbox_dir.

Respuestas de trabajo

Una WorkResponse contiene un ID de solicitud, un código de salida cero o distinto de cero y una string de salida que describe cualquier error que se encuentre en el procesamiento o la ejecución de la solicitud. El campo output contiene una descripción breve. Los registros completos se pueden escribir en el stderr del trabajador. Debido a que los trabajadores solo pueden escribir WorkResponses en stdout, es común que el trabajador redireccione el stdout de las herramientas que usa a stderr.

{
  "exitCode" : 1,
  "output" : "Action failed with the following message:\nCould not find input
    file \"/path/to/my/file/1\"",
  "requestId" : 12
}

Según la norma para los protobufs, todos los campos son opcionales. Sin embargo, Bazel requiere que el WorkRequest y el WorkResponse correspondiente tengan el mismo ID de solicitud, por lo que se debe especificar el ID de solicitud si no es cero. Este es un WorkResponse válido.

{
  "requestId" : 12,
}

Una request_id de 0 indica una solicitud “singleplex”, que se usa cuando esta solicitud no se puede procesar en paralelo con otras solicitudes. El servidor garantiza que un trabajador determinado recibe solicitudes solo con request_id 0 o solo con request_id mayor que cero. Las solicitudes de singleplex se envían en serie, por ejemplo, si el servidor no envía otra solicitud hasta que reciba una respuesta (excepto las solicitudes de cancelación, consulta a continuación).

Notas

  • Cada búfer de protocolo está precedido por su longitud en formato varint (consulta MessageLite.writeDelimitedTo()).
  • Las solicitudes y respuestas JSON no están precedidas por un indicador de tamaño.
  • Las solicitudes JSON mantienen la misma estructura que el protobuf, pero usan JSON estándar y usan mayúsculas y minúsculas para todos los nombres de campo.
  • Para mantener las mismas propiedades de compatibilidad entre versiones anteriores y futuras que el protobuf, los trabajadores JSON deben tolerar campos desconocidos en estos mensajes y usar los valores predeterminados de protobuf para los valores faltantes.
  • Bazel almacena solicitudes como protobufs y las convierte en JSON mediante el formato JSON de protobuf.

Cancelación

De manera opcional, los trabajadores pueden permitir que se cancelen las solicitudes de trabajo antes de que finalicen. Esto es particularmente útil en relación con la ejecución dinámica, en la que la ejecución local puede interrumpirse regularmente mediante una ejecución remota más rápida. Para permitir la cancelación, agrega supports-worker-cancellation: 1 al campo execution-requirements (consulta a continuación) y establece la marca --experimental_worker_cancellation.

Una solicitud de cancelación es una WorkRequest con el campo cancel configurado (y, de manera similar, una respuesta de cancelación es una WorkResponse con el conjunto de campos was_cancelled). El único otro campo que debe estar en una solicitud de cancelación o respuesta de cancelación es request_id, que indica qué solicitud cancelar. El campo request_id será 0 para los trabajadores singleplex, o bien el request_id que no sea 0 de un WorkRequest enviado previamente para los multiplex. El servidor puede enviar solicitudes de cancelación para solicitudes que el trabajador ya respondió, en cuyo caso la solicitud de cancelación debe ignorarse.

Cada mensaje WorkRequest que no se cancela debe responderse una vez, independientemente de si se canceló o no. Una vez que el servidor envió una solicitud de cancelación, el trabajador puede responder con un WorkResponse con el request_id configurado y el campo was_cancelled configurado como verdadero. También se acepta enviar una WorkResponse normal, pero se ignorarán los campos output y exit_code.

Una vez que se envía una respuesta para un WorkRequest, el trabajador no debe tocar los archivos en su directorio de trabajo. El servidor puede limpiar los archivos, incluidos los temporales.

Crea la regla que usa el trabajador

También deberás crear una regla que genere acciones que realizará el trabajador. Crear una regla de Starlark que use un trabajador es igual a crear cualquier otra regla.

Además, la regla debe contener una referencia al trabajador en sí, y existen algunos requisitos para las acciones que produce.

Cómo hacer referencia al trabajador

La regla que usa el trabajador debe contener un campo que haga referencia al trabajador, por lo que deberás crear una instancia de una regla \*\_binary para definir tu trabajador. Si tu trabajador se llama MyWorker.Java, esta podría ser la regla asociada:

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

Esto crea la etiqueta "worker", que hace referencia al objeto binario de trabajador. A continuación, definirás la regla que usa el trabajador. Esta regla debe definir un atributo que se refiera al objeto binario de trabajador.

Si el objeto binario de trabajador que compilaste está en un paquete llamado "work", que se encuentra en el nivel superior de la compilación, puede ser la definición del atributo:

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

cfg = "exec" indica que el trabajador debe compilarse para ejecutarse en tu plataforma de ejecución, en lugar de en la plataforma de destino (es decir, el trabajador se usa como herramienta durante la compilación).

Requisitos para las acciones laborales

La regla que usa el trabajador crea acciones para que realice el trabajador. Estas acciones tienen algunos requisitos.

  • El campo "arguments". Esto toma una lista de strings, todas excepto las últimas, que son argumentos que se pasan al trabajador en el inicio. El último elemento de la lista "arguments" es un argumento flag-file (@-preceded). Los trabajadores leen los argumentos del archivo de marcas especificado en cada WorkRequest. Tu regla puede escribir argumentos que no sean de inicio para el trabajador en este archivo de marcas.

  • El campo "execution-requirements", que toma un diccionario que contiene "supports-workers" : "1", "supports-multiplex-workers" : "1" o ambos.

    Los campos "arguments" y "execution-requirements" son obligatorios para todas las acciones que se envían a los trabajadores. Además, las acciones que deben ejecutar los trabajadores JSON deben incluir "requires-worker-protocol" : "json" en el campo de requisitos de ejecución. "requires-worker-protocol" : "proto" también es un requisito de ejecución válido, aunque no es obligatorio para los trabajadores proto, ya que son los predeterminados.

    También puedes establecer un worker-key-mnemonic en los requisitos de ejecución. Esto puede resultar útil si vuelves a usar el archivo ejecutable para varios tipos de acciones y quieres distinguir las acciones de este trabajador.

  • Los archivos temporales generados en el curso de la acción deben guardarse en el directorio del trabajador. Esto habilita la zona de pruebas.

Si suponemos que existe una definición de reglas con el atributo "worker" descrito anteriormente, además de un atributo "srcs" que representa las entradas, un atributo "output" que representa los resultados y un atributo "args" que representa los argumentos de inicio del trabajador, la llamada a ctx.actions.run podría ser la siguiente:

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 otro ejemplo, consulta Implementa trabajadores persistentes.

Ejemplos

La base de código de Bazel usa trabajadores del compilador de Java, además de un ejemplo de trabajador JSON que se usa en las pruebas de integración.

Puedes usar su andamiaje para convertir cualquier herramienta basada en Java en un trabajador pasando la devolución de llamada correcta.

Para ver un ejemplo de una regla que usa un trabajador, consulta la prueba de integración de trabajadores de Bazel.

Los colaboradores externos implementaron trabajadores en una variedad de lenguajes; consulta las implementaciones de Polyglot de trabajadores persistentes de Bazel. Puedes encontrar muchos más ejemplos en GitHub.