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

Báo cáo vấn đề Xem nguồn Nightly/3}

Worker ổn định có thể giúp bản dựng của bạn nhanh hơn. Nếu có nhiều hành động lặp đi lặp lại trong bản dựng khiến chi phí khởi động cao hoặc sẽ hưởng lợi từ việc lưu nhiều hành động vào bộ nhớ đệm, bạn nên triển khai trình thực thi liên tục của riêng mình để thực hiện những 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ợ sử dụng vùng đệm giao thức hoặc chuỗi JSON.

Quy trình triển khai trình thực thi gồm hai phần:

Tạo dựng nhân viên

Một worker ổn định đáp ứng được một số yêu cầu:

  • Phương thức này đọc WorkRequests trên stdin của nó.
  • Hàm này ghi WorkResponses (và chỉ WorkResponse) vào stdout.
  • Phương thức 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ự tạo sự cố định nếu cờ đó được truyền, nếu không phải thực hiện biên dịch một lần và thoát.

Nếu chương trình của bạn đáp ứng các yêu cầu này, bạn có thể sử dụng chương trình này như một trình thực thi liên tục!

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

WorkRequest chứa danh sách các đối số cho worker, danh sách các cặp đường dẫn-thông báo đại diện cho dữ liệu đầu vào mà worker có thể truy cập (việc này không được thực thi, nhưng bạn có thể sử dụng thông tin này để lưu vào bộ nhớ đệm) và mã yêu cầu (0 đối với worker singleplex).

LƯU Ý: Mặc dù quy cách vùng đệm giao thức sử dụng "kiểu viết con rắn" (request_id), nhưng giao thức JSON lại sử dụng "kiểu viết lạc đà" (requestId). Tài liệu này sử dụng kiểu viết lạc đà trong ví dụ về JSON, nhưng kiểu viết hoa rắn khi nói về trường bất kể giao thức là gì.

{
  "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ể dùng trường verbosity không bắt buộc để yêu cầu thêm kết quả gỡ lỗi từ worker. Điều này hoàn toàn tuỳ thuộc vào việc nhân viên xử lý nội dung gì và cách xuất dữ liệu ra sao. Giá trị càng cao thì kết quả càng chi tiết. Việc chuyể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 số lượng đầu ra khác nhau.

Trường sandbox_dir không bắt buộc chỉ được các worker hỗ trợ hộp cát đax sử dụng.

Trả lời cho bài tập

WorkResponse chứa mã yêu cầu, mã thoát bằng 0 hoặc khác 0 và một chuỗi đầ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. Trường output chứa nội dung mô tả ngắn; nhật ký đầy đủ có thể được ghi vào stderr của worker. Vì worker chỉ có thể ghi WorkResponses vào stdout, nên worker thường chuyển hướng stdout của mọi công cụ mà worker đó sử dụng sang stderr.

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

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

{
  "requestId" : 12,
}

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

Lưu ý

  • Mỗi vùng đệm giao thức bắt đầu bằng độ dài ở định dạng varint (xem MessageLite.writeDelimitedTo().
  • Yêu cầu và phản hồi JSON không được đặt sau chỉ báo kích thước.
  • Yêu cầu JSON giữ nguyên cấu trúc giống như protobuf, nhưng sử dụng JSON tiêu chuẩn và sử dụng cách viết hoa kiểu lạc đà cho tất cả các tên trường.
  • Để duy trì các thuộc tính tương thích ngược và tiến lên giống như protobuf, trình thực thi JSON phải chấp nhận các trường không xác định trong những thông báo này, đồng thời sử dụng các 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.

Huỷ

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 nối 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 một 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ỷ là một WorkRequest có nhóm trường cancel (và tương tự, phản hồi huỷ là một WorkResponse có nhóm trường was_cancelled). Trường duy nhất khác phải nằm trong yêu cầu huỷ hoặc phản hồi huỷ là request_id, cho biết yêu cầu nào cần huỷ. Trường request_id sẽ có giá trị là 0 đối với trình thực thi singleplex hoặc request_id không phải là 0 của WorkRequest đã gửi trước đó đối với trình thực thi Multiplex. 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 WorkRequest không huỷ 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 trong đó request_id được đặt và trường was_cancelled đượ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 một WorkRequest, worker không được chạm vào các tệp trong thư mục đang hoạt động. Máy chủ có thể dọn dẹp miễn phí các tệp, bao gồm cả cá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 thao tác để trình thực thi này thực hiện. Việc tạo quy tắc Starlark sử dụng worker cũng giống như tạo bất kỳ quy tắc nào khác.

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

Tham chiếu đến worker

Quy tắc sử dụng worker này 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 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 của worker. Sau đó, bạn sẽ xác định một quy tắc sử dụng worker này. Quy tắc này phải xác định một thuộc tính tham chiếu đến tệp nhị phân của worker.

Nếu tệp nhị phân worker bạn đã tạo nằm trong một gói có tên "work" (công việc), ở 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 rằng trình thực thi này nên được tạo để chạy trên nền tảng thực thi của bạn thay vì trên nền tảng mục tiêu (tức là trình thực thi này được dùng làm công cụ trong quá trình tạo bản dựng).

Yêu cầu đối với tác vụ công việc

Quy tắc sử dụng worker này tạo các thao tác để worker thực hiện. Những thao tác này có một số yêu cầu.

  • Trường "Arguments" (đối số). Thao tác này sẽ 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 "đối số" là một đối số flag-file (@ có trước). Worker đọc các đối số từ tệp cờ đượ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 từ điển chứa "supports-workers" : "1", "supports-multiplex-workers" : "1" hoặc cả hai.

    Tất cả thao tác được gửi đến worker là trường "đối số" và "yêu cầu thực thi" là bắt buộc. Ngoài ra, các thao tác mà trình thực thi JSON nên được 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 trình thực thi proto, vì đây là chế độ mặc định.

    Bạn cũng có thể thiết lập 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 thao tác và muốn phân biệt các thao tác của trình thực thi này.

  • Các tệp tạm thời được tạo trong quá trình thực hiện hành động đó nên được lưu vào thư mục của worker. Thao tác này sẽ bật chức năng 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 dữ liệu đầu vào, thuộc tính "output" đại diện cho kết quả và thuộc tính "args" đại diện cho đối số khởi động worker, 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"]
 )

Để biết một ví dụ khác, hãy xem phần Triển khai trình thực thi liên tục.

Ví dụ

Cơ sở mã Bazel sử dụng trình biên dịch Java, cùng với trình thực thi JSON mẫu dùng trong các kiểm thử tích hợp của chúng tôi.

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

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

Các cộng tác viên bên ngoài đã triển khai trình thực thi bằng nhiều ngôn ngữ; hãy xem Cách triển khai Polyglot của trình thực thi bền vững Bazel. Bạn có thể tìm thêm nhiều ví dụ khác trên GitHub!