Cómo crear trabajadores persistentes

Denuncia un problema Ver fuente Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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 alto costo de inicio o se beneficiarían del almacenamiento en caché de acciones cruzadas, te recomendamos que implementes tu propio trabajador persistente para realizar estas acciones.

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

La implementación del trabajador tiene dos partes:

Cómo crear el trabajador

Un trabajador persistente cumple 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 de línea de comandos --persistent_worker y solo debe hacerse 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 trabajador persistente.

Solicitudes de trabajo

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

NOTA: Si bien la especificación del búfer de protocolo usa "mayúsculas y minúsculas" (request_id), el protocolo JSON usa "mayúsculas y minúsculas" (requestId). En este documento, se usan mayúsculas y minúsculas en los ejemplos de JSON, pero mayúsculas y minúsculas cuando se habla del 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 resultados de depuración adicionales del trabajador. Depende completamente del trabajador qué generar y cómo hacerlo. 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 salida.

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

Respuestas de trabajo

Un WorkResponse contiene un ID de solicitud, un código de salida cero o distinto de cero y un mensaje de salida que describe cualquier error que se haya encontrado durante el procesamiento o la ejecución de la solicitud. Un trabajador debe capturar el stdout y el stderr de cualquier herramienta a la que llame y, luego, informarlos a través de WorkResponse. No es seguro escribirlo en el stdout del proceso de trabajador, ya que interferirá con el protocolo del trabajador. Escribirlo en el stderr del proceso de trabajador es seguro, pero el resultado se recopila en un archivo de registro por trabajador en lugar de atribuirse a acciones individuales.

{
  "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 de los protobufs, todos los campos son opcionales. Sin embargo, Bazel requiere que WorkRequest y el WorkResponse correspondiente tengan el mismo ID de solicitud, por lo que este debe especificarse si no es cero. Este es un WorkResponse válido.

{
  "requestId" : 12,
}

Un request_id de 0 indica una solicitud de "singleplex", que se usa cuando esta solicitud no se puede procesar en paralelo con otras solicitudes. El servidor garantiza que un trabajador determinado reciba solicitudes con solo request_id 0 o solo request_id mayor que cero. Las solicitudes de monoplex se envían de forma serial, por ejemplo, si el servidor no envía otra solicitud hasta que recibe 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 de 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 campos.
  • Para mantener las mismas propiedades de retrocompatibilidad que protobuf, los trabajadores de JSON deben tolerar campos desconocidos en estos mensajes y usar los valores predeterminados de protobuf para valores faltantes.
  • Bazel almacena las solicitudes como protobufs y las convierte a JSON con 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 especialmente útil en relación con la ejecución dinámica, en la que una ejecución remota más rápida puede interrumpir con frecuencia la ejecución local. Para permitir la cancelación, agrega supports-worker-cancellation: 1 al campo execution-requirements (consulta a continuación) y configura la marca --experimental_worker_cancellation.

Una solicitud de cancelación es un WorkRequest con el campo cancel configurado (y, de manera similar, una respuesta de cancelación es un WorkResponse con el campo was_cancelled configurado). 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 se debe cancelar. El campo request_id será 0 para los trabajadores de monoplexación o el request_id que no sea 0 de un WorkRequest enviado anteriormente para los trabajadores de multiplexación. Es posible que el servidor envíe solicitudes de cancelación para solicitudes a las que el trabajador ya respondió. En ese caso, se debe ignorar la solicitud de cancelación.

Cada mensaje WorkRequest que no sea de cancelación debe responderse exactamente una vez, independientemente de que se haya cancelado o no. Una vez que el servidor haya enviado una solicitud de cancelación, el trabajador puede responder con un WorkResponse con el request_id establecido y el campo was_cancelled establecido como verdadero. También se acepta enviar un 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.

Cómo crear la regla que usa el trabajador

También deberás crear una regla que genere acciones que debe realizar el trabajador. Crear una regla de Starlark que use un trabajador es igual que 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 en sí, por lo que deberás crear una instancia de una regla \*\_binary para definirlo. 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 del trabajador. Luego, definirás una regla que use el trabajador. Esta regla debe definir un atributo que se refiriera al binario del trabajador.

Si el objeto binario del trabajador que compilaste está en un paquete llamado “work”, que se encuentra en el nivel superior de la compilación, esta podría ser la definición del atributo:

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

cfg = "exec" indica que el trabajador se debe compilar 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 de la acción de trabajo

La regla que usa el trabajador crea las acciones que debe realizar el trabajador. Estas acciones tienen algunos requisitos.

  • El campo "arguments" Esta función toma una lista de cadenas, todas excepto la última, que son argumentos que se pasan al trabajador al inicio. El último elemento de la lista "arguments" es un argumento flag-file (precedido de @). Los trabajadores leen los argumentos del archivo de marca especificado por WorkRequest. Tu regla puede escribir argumentos que no sean de inicio para el trabajador en este archivo marcador.

  • 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 necesario 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 ser útil si reutilizas el ejecutable para varios tipos de acciones y deseas 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 hay una definición de regla con el atributo "worker" que se describió antes, además de un atributo "srcs" que representa las entradas, un atributo "output" que representa las salidas 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 trabajador JSON de ejemplo que se usa en nuestras pruebas de integración.

Puedes usar scaffolding 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 varios idiomas. Consulta las implementaciones de Polyglot de trabajadores persistentes de Bazel. Puedes encontrar muchos más ejemplos en GitHub.