Tạo trình thực thi liên tục

Worker liên tục có thể giúp quá trình xây dựng của bạn diễn ra nhanh hơn. Nếu có các hành động lặp lại trong quá trình xây dựng có chi phí khởi động cao hoặc sẽ được hưởng lợi từ việc lưu vào bộ nhớ đệm giữa các hành động, thì bạn có thể muốn triển khai worker liên tục của riêng mình để thực hiện các hành động này.

Máy chủ Bazel giao tiếp với worker bằng stdin/stdout. Máy chủ này hỗ trợ việc sử dụng bộ đệm giao thức hoặc chuỗi JSON.

Quá trình triển khai worker có 2 phần:

Tạo worker

Worker liên tục phải tuân thủ một số yêu cầu:

  • Worker này đọc WorkRequests từ stdin.
  • Worker này ghi WorkResponses (và chỉ WorkResponses) vào stdout.
  • Worker này chấp nhận cờ --persistent_worker. Trình bao bọc phải nhận ra cờ dòng lệnh --persistent_worker và chỉ tự làm cho mình trở nên liên tục nếu cờ đó được truyền, nếu không, trình bao bọc phải thực hiện quá trình biên dịch một lần và thoát.

Nếu chương trình của bạn tuân thủ các yêu cầu này, thì chương trình đó có thể được dùng làm worker liên tục !

Yêu cầu công việc

Một WorkRequest chứa một danh sách các đối số cho worker, một danh sách các cặp đường dẫn-tiêu hoá đại diện cho các đầu vào mà worker có thể truy cập (không bắt buộc, nhưng bạn có thể sử dụng thông tin này để lưu vào bộ nhớ đệm) và một mã yêu cầu, là 0 đối với các worker đơn hợp.

LƯU Ý: Mặc dù thông số kỹ thuật về bộ đệm giao thức sử dụng "snake case" (request_id), nhưng giao thức JSON sử dụng "camel case" (requestId). Tài liệu này sử dụng camel case trong các ví dụ JSON, nhưng snake case khi nói về trường bất kể giao thức.

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

Bạn có thể sử dụng trường verbosity không bắt buộc để yêu cầu thêm thông tin đầu ra gỡ lỗi từ worker. Worker hoàn toàn có quyền quyết định nội dung và cách xuất. Giá trị càng cao thì thông tin đầu ra càng chi tiết. Việc truyền cờ --worker_verbose đến Bazel sẽ đặt trường verbosity thành 10, nhưng bạn có thể sử dụng các giá trị nhỏ hơn hoặc lớn hơn theo cách thủ công cho các lượng thông tin đầu ra khác nhau.

Trường sandbox_dir không bắt buộc chỉ được dùng bởi các worker hỗ trợ hộp cát đa hợp.

Phản hồi công việc

Một WorkResponse chứa một mã yêu cầu, một mã thoát bằng 0 hoặc khác 0 và một thông báo đầu ra mô tả mọi lỗi gặp phải trong quá trình xử lý hoặc thực thi yêu cầu. Worker phải nắm bắt stdoutstderr của mọi công cụ mà worker gọi và báo cáo thông qua WorkResponse. Việc ghi vào stdout của quy trình worker là không an toàn vì sẽ gây trở ngại cho giao thức worker. Việc ghi vào stderr của quy trình worker là an toàn, nhưng kết quả được thu thập trong một tệp nhật ký trên mỗi worker thay vì được gán cho các hành động riêng lẻ.

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

Theo quy tắc chung đối với protobuf, tất cả các trường đều không bắt buộc. Tuy nhiên, Bazel yêu cầu WorkRequestWorkResponse tương ứng phải có cùng mã yêu cầu, vì vậy, bạn phải chỉ định mã yêu cầu nếu mã này khác 0. Đây là một WorkResponse hợp lệ.

{
  "requestId" : 12,
}

request_id bằng 0 cho biết yêu cầu "đơn hợp", được dùng khi yêu cầu này không thể được xử lý song song với các yêu cầu khác. Máy chủ đảm bảo rằng một worker nhất định nhận được các yêu cầu chỉ có request_id bằng 0 hoặc chỉ có request_id lớn hơn 0. Các yêu cầu đơn hợp được gửi theo tuần tự, ví dụ: nếu máy chủ không gửi yêu cầu khác cho đến khi nhận được phản hồi (ngoại trừ yêu cầu huỷ, hãy xem bên dưới).

Lưu ý

  • Mỗi bộ đệm giao thức đều được đặt trước bằng độ dài của bộ đệm đó ở định dạng varint (xem MessageLite.writeDelimitedTo().
  • Yêu cầu và phản hồi JSON không được đặt trước bằng chỉ báo kích thước.
  • Yêu cầu JSON tuân thủ cùng một cấu trúc như protobuf, nhưng sử dụng JSON tiêu chuẩn và sử dụng camel case cho tất cả tên trường.
  • Để duy trì các thuộc tính tương thích ngược và tương thích chuyển tiếp giống như protobuf , worker JSON phải chấp nhận các trường không xác định trong các thông báo này và sử dụng giá trị mặc định của protobuf cho các giá trị bị thiếu.
  • Bazel lưu trữ các yêu cầu dưới dạng protobuf và chuyển đổi chúng thành JSON bằng định dạng JSON của protobuf

Hủy

Worker có thể tuỳ ý cho phép huỷ các yêu cầu công việc trước khi hoàn tất. Điều này đặc biệt hữu ích khi kết hợp với quá trình thực thi động, trong đó quá trình thực thi cục bộ có thể thường xuyên bị gián đoạn bởi quá trình thực thi từ xa nhanh hơn. Để cho phép huỷ, hãy thêm supports-worker-cancellation: 1 vào trường execution-requirements (xem bên dưới) và đặt cờ --experimental_worker_cancellation.

Yêu cầu huỷWorkRequest có trường cancel được đặt (và tương tự, phản hồi huỷWorkResponse có trường was_cancelled được đặt). Trường duy nhất khác phải có trong yêu cầu huỷ hoặc phản hồi huỷ là request_id, cho biết yêu cầu cần huỷ. Trường request_id sẽ là 0 đối với các worker đơn hợp hoặc request_id khác 0 của đã gửi trước đó cho các worker đa hợp.WorkRequest Máy chủ có thể gửi yêu cầu huỷ cho các yêu cầu mà worker đã phản hồi. Trong trường hợp đó, yêu cầu huỷ phải bị bỏ qua.

Mỗi thông báo không huỷ WorkRequest phải được trả lời chính xác một lần, cho dù thông báo đó có bị huỷ hay không. Sau khi máy chủ gửi yêu cầu huỷ, worker có thể phản hồi bằng WorkResponse với request_id được đặt và was_cancelled trường được đặt thành true. Việc gửi WorkResponse thông thường cũng được chấp nhận, nhưng các trường outputexit_code sẽ bị bỏ qua.

Sau khi gửi phản hồi cho WorkRequest, worker không được chạm vào các tệp trong thư mục làm việc của mình. Máy chủ có thể tự do dọn dẹp các tệp, bao gồm cả tệp tạm thời.

Tạo quy tắc sử dụng worker

Bạn cũng cần tạo một quy tắc tạo ra các hành động mà worker sẽ thực hiện. Việc tạo quy tắc Starlark sử dụng worker cũng giống như việc tạo bất kỳ quy tắc nào khác.

Ngoài ra, quy tắc cần chứa một tham chiếu đến chính worker và có một số yêu cầu đối với các hành động mà quy tắc tạo ra.

Tham chiếu đến worker

Quy tắc sử dụng worker cần chứa một trường tham chiếu đến chính worker , vì vậy, bạn cần tạo một thực thể của quy tắc \*\_binary để xác định worker. Nếu worker của bạn được gọi là MyWorker.Java, thì đây có thể là quy tắc được liên kết:

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

Thao tác này sẽ tạo nhãn "worker", tham chiếu đến tệp nhị phân worker. Sau đó, bạn sẽ xác định một quy tắc sử dụng worker. Quy tắc này phải xác định một thuộc tính mà tham chiếu đến tệp nhị phân worker.

Nếu tệp nhị phân worker mà bạn đã tạo nằm trong một gói có tên là "work" (ở cấp cao nhất của bản dựng), thì đây có thể là định nghĩa thuộc tính:

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

cfg = "exec" cho biết worker phải được tạo để chạy trên nền tảng thực thi thay vì trên nền tảng mục tiêu (tức là worker được dùng làm công cụ trong quá trình xây dựng).

Yêu cầu về hành động công việc

Quy tắc sử dụng worker sẽ tạo các hành động để worker thực hiện. Các hành động này có một vài yêu cầu.

  • Trường "arguments". Trường này lấy một danh sách các chuỗi, tất cả trừ chuỗi cuối cùng là các đối số được truyền đến worker khi khởi động. Phần tử cuối cùng trong danh sách "arguments" là đối số flag-file (được đặt trước bằng @). Worker đọc các đối số từ tệp cờ đã chỉ định trên cơ sở mỗi WorkRequest. Quy tắc của bạn có thể ghi các đối số không khởi động cho worker vào tệp cờ này.

  • Trường "execution-requirements", lấy một từ điển chứa "supports-workers" : "1", "supports-multiplex-workers" : "1", hoặc cả hai.

    Các trường "arguments" và "execution-requirements" là bắt buộc đối với tất cả các hành động được gửi đến worker. Ngoài ra, các hành động mà worker JSON phải thực thi cần bao gồm "requires-worker-protocol" : "json" trong trường yêu cầu thực thi. "requires-worker-protocol" : "proto" cũng là một yêu cầu thực thi hợp lệ, mặc dù không bắt buộc đối với các worker proto, vì đây là giá trị mặc định.

    Bạn cũng có thể đặt worker-key-mnemonic trong các yêu cầu thực thi. Điều này có thể hữu ích nếu bạn đang sử dụng lại tệp thực thi cho nhiều loại hành động và muốn phân biệt các hành động theo worker này.

  • Các tệp tạm thời được tạo trong quá trình thực hiện hành động phải được lưu vào thư mục của worker. Điều này cho phép tạo hộp cát.

Giả sử định nghĩa quy tắc có thuộc tính "worker" được mô tả ở trên, ngoài thuộc tính "srcs" đại diện cho các đầu vào, thuộc tính "output" đại diện cho các đầu ra và thuộc tính "args" đại diện cho các đối số khởi động worker, thì lệnh gọi đến ctx.actions.run có thể là:

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"]
 )

Để xem một ví dụ khác, hãy xem phần Triển khai worker liên tục.

Ví dụ

Cơ sở mã Bazel sử dụng các worker trình biên dịch Java, ngoài một worker JSON mẫu được dùng trong các bài kiểm tra tích hợp của chúng tôi.

Bạn có thể sử dụng giàn giáo của họ để biến bất kỳ công cụ dựa trên Java nào thành worker bằng cách truyền lệnh gọi lại chính xác.

Để xem ví dụ về quy tắc sử dụng worker, hãy xem bài kiểm tra tích hợp worker của Bazel.

Những người đóng góp bên ngoài đã triển khai worker bằng nhiều ngôn ngữ; hãy xem phần Triển khai đa ngôn ngữ của worker liên tục Bazel. Bạn có thể tìm thấy nhiều ví dụ khác trên GitHub!