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

Báo cáo vấn đề Xem nguồn Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

Worker liên tục có thể giúp quá trình tạo bản dựng diễn ra nhanh hơn. Nếu có các thao tác lặp lại trong bản 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 trên nhiều thao tác, thì bạn nên triển khai trình chạy liên tục của riêng mình để thực hiện các thao tác này.

Máy chủ Bazel giao tiếp với worker bằng cách sử dụ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.

Việc triển khai worker có hai phần:

Tạo worker

Một worker liên tục phải đáp ứng một số yêu cầu:

  • Nó đọc WorkRequests từ stdin.
  • Nó ghi WorkResponses (và chỉ WorkResponse) vào stdout.
  • Thành phần 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ự duy trì nếu cờ đó được truyền, nếu không, trình bao bọc phải thực hiện một lần biên dịch và thoát.

Nếu chương trình của bạn đáp ứng 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

WorkRequest chứa danh sách các đối số cho worker, danh sách các cặp đường dẫn-tiêu hóa đại diện cho các đầu vào mà worker có thể truy cập (điều 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ã nhận dạng yêu cầu, là 0 đối với các worker đơn công.

LƯU Ý: Mặc dù quy cách bộ đệm giao thức sử dụng "kiểu viết có dấu gạch dưới" (request_id), giao thức JSON 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 các ví dụ về JSON, nhưng sử dụng kiểu viết có dấu gạch dưới 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ể dùng trường verbosity (không bắt buộc) để yêu cầu đầu ra gỡ lỗi bổ sung từ worker. Worker hoàn toàn có quyền quyết định nội dung và cách xuất dữ liệu. Giá trị càng cao thì đầ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ể 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 đầu ra khác nhau.

Chỉ những worker hỗ trợ hộp cát ghép kênh mới sử dụng trường sandbox_dir không bắt buộc.

Phản hồi của cơ thể

WorkResponse chứa mã yêu cầu, mã thoát bằng 0 hoặc khác 0 và 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. Một worker nên ghi lại 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ì việc này 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ý cho mỗi worker thay vì được gán cho từng thao tác 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 đố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 giản", được dùng khi yêu cầu này không thể 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 chỉ nhận được các yêu cầu có request_id bằng 0 hoặc chỉ nhận được các yêu cầu có request_id lớn hơn 0. Các yêu cầu đơn công được gửi nối tiếp, 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ỷ, xem bên dưới).

Lưu ý

  • Mỗi vùng đệm giao thức đều có độ dài ở định dạng varint (xem MessageLite.writeDelimitedTo().
  • Các yêu cầu và phản hồi JSON không có chỉ báo kích thước ở phía trước.
  • Các yêu cầu JSON duy trì cấu trúc giống như protobuf, nhưng sử dụng JSON tiêu chuẩn và sử dụng kiểu viết lạc đà 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 xuôi giống như protobuf, các worker 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 và 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 các yêu cầu đó thành JSON bằng cách sử dụng định dạng JSON của protobuf

Hủy

Các worker có thể tuỳ ý cho phép huỷ các yêu cầu công việc trước khi hoàn thành. Đ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 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ó trường cancel được đặt (và tương tự, phản hồi huỷ là một 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 nhân viên đơn công hoặc request_id khác 0 của WorkRequest đã gửi trước đó đối với nhân viên đa công. Máy chủ có thể gửi yêu cầu huỷ cho những yêu cầu mà worker đã phản hồi. Trong trường hợp này, 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, bất kể thông báo đó có bị huỷ hay không. Sau khi máy chủ gửi yêu cầu huỷ, trình thực thi có thể phản hồi bằng một WorkResponserequest_id được đặt và trường was_cancelled được đặt thành true. Bạn cũng có thể gửi WorkResponse thông thường, 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 của mình. Máy chủ có thể xoá các tệp, kể 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 mà worker sẽ thực hiện. Việc tạo một 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 chứa thông tin 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à worker tạo ra.

Đề cập đến worker

Quy tắc sử dụng worker cần có một trường tham chiếu đến chính worker đó, vì vậy, bạn sẽ cần tạo một phiên bản của quy tắc \*\_binary để xác định worker của mình. Nếu worker của bạn có tên 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" (trình thực thi), đề cập đến tệp nhị phân của trình thực thi. 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 đề cập đến tệp nhị phân của 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 của bạn 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 tạo).

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

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

  • Trường "arguments". Thao tác 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à một đối số flag-file (đứng trước là @). Các worker đọc các đối số từ flagfile đã 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 flagfile này.

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

    Các trường "arguments" (đối số) và "execution-requirements" (yêu cầu thực thi) là bắt buộc đối với tất cả các thao tác được gửi đến nhân viên. Ngoài ra, những thao tác mà các worker JSON cần thực hiện phải có "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ì chúng là 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 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 thao tác sẽ đượ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" như mô tả ở trên, ngoài thuộc tính "srcs" đại diện cho đầu vào, thuộc tính "output" đại diện cho đầu ra và thuộc tính "args" đại diện cho các đố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"]
 )

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

Ví dụ

Cơ sở mã Bazel sử dụng các worker trình biên dịch Java, ngoài ví dụ về worker JSON được 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 giàn giáo của chúng để biến mọi công cụ dựa trên Java thành một worker bằng cách truyền vào lệnh gọi lại chính xác.

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

Các cộng tác viên bên ngoài đã triển khai worker bằng nhiều ngôn ngữ; hãy xem Các cách 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!