Case study / Python

Ghi JSON khi nhiều tiến trình Python chạy đồng thời

Ghi JSON qua tệp tạm rồi dùng os.replace giúp tránh công bố một tệp đang ghi dở. Tuy nhiên, hai tiến trình vẫn có thể ghi đè kết quả của nhau nếu cùng đọc trạng thái cũ. VAS đã gặp đúng tình huống này khi cài nhiều skill đồng thời.

Trong bài này

1. Hai lượt cài thành công nhưng thiếu một bản ghi

VAS lưu thông tin quản lý skill trong .vas-manifest.json tại thư mục đích. Bản ghi giúp CLI đối chiếu bản cài và thực hiện thao tác quản lý tiếp theo. Trong lỗi đã được tái hiện, hai lượt cài pdf và docx đều có thư mục và báo thành công, nhưng manifest chỉ còn một skill.

JSON vẫn hợp lệ. Vì vậy, kiểm tra “parse được JSON” hoặc “thư mục tồn tại” đều chưa đủ để phát hiện lỗi. Gỡ skill mất receipt bị từ chối vì CLI không có bản ghi quản lý tương ứng.

2. Atomic replace không bảo vệ toàn bộ phép cập nhật

Lịch chạy minh họa dưới đây bắt đầu từ manifest rỗng. Hai writer đều đọc trước khi writer kia ghi:

A: read {}                         B: read {}
A: add pdf                         B: add docx
A: replace file with {"pdf": ...}
                                   B: replace file with {"docx": ...}
Final JSON: {"docx": ...}

Mỗi lần thay tệp đều có thể nguyên vẹn, nhưng writer B dùng snapshot cũ nên làm mất cập nhật của A. Chỉ khóa quanh save_manifest vẫn chưa đủ nếu cả hai đã đọc trước khi lấy khóa. Phạm vi cần bảo vệ là đọc → quyết định → sửa → ghi.

Với install/uninstall, phải xét cả trạng thái payload và manifest trong giao dịch. Khóa riêng cho install bỏ sót trường hợp uninstall đồng thời ghi lại snapshot chứa một receipt vừa bị xóa.

3. Khóa một đích cài cho cả giao dịch

Trong VAS, destination_lock mở tệp .vas.lock ổn định tại đích. Nhánh POSIX dùng fcntl.flock; nhánh Windows dùng msvcrt.locking. Cả install và uninstall lấy khóa trước khi đọc manifest. Các đích khác nhau có khóa riêng nên không phải chờ một khóa toàn catalog.

Đoạn sau minh họa thứ tự, không phải script độc lập. apply_operation đại diện cho xử lý payload, receipt và lỗi theo logic giao dịch hiện có:

with destination_lock(destination):
    manifest = load_manifest(destination)
    apply_operation(destination, manifest)
    save_manifest(destination, manifest)

Hàm ghi vẫn tạo tệp tạm cùng thư mục, flush, fsync rồi os.replace. Khóa điều phối các writer; ghi thay thế nguyên vẹn giải quyết việc công bố nội dung tệp. Cần cả hai cơ chế trong luồng này.

Không xóa .vas.lock sau mỗi lần dùng: writer đang giữ inode cũ và writer mở tệp vừa tạo lại có thể không còn khóa cùng một đối tượng. Khóa được thả khi handle đóng; sự tồn tại của tệp không có nghĩa đang có tiến trình giữ khóa. VAS cũng từ chối lock hoặc manifest là symlink hay tệp không phù hợp.

4. Kiểm thử bằng các tiến trình thật

Bộ test tests/test_cli_concurrency.py tạo catalog và đích cài tạm, rồi chạy CLI trong các tiến trình riêng. Worker đầu được giữ tại điểm đã đọc manifest. Worker thứ hai phải chờ; sau khi worker đầu hoàn tất, nó mới được đọc trạng thái mới. Cách này kiểm soát điểm giao nhau thay vì chỉ chạy lặp và hy vọng gặp race.

  • Install + install: còn đủ hai receipt và payload.
  • Uninstall + uninstall: không làm sống lại receipt đã gỡ.
  • Install + uninstall theo cả hai thứ tự: giữ đúng trạng thái cuối.
  • Hai thư mục đích khác nhau: không khóa lẫn nhau.
  • Tiến trình giữ khóa dừng: tiến trình chờ có thể tiếp tục.
  • Tệp khóa được tái sử dụng và không bị skill chiếm tên.

Chạy từ gốc repository VAS bằng Python 3. Các fixture của suite này nằm trong thư mục tạm, không cần cài vào thư mục skill cá nhân:

python3 -B -m unittest discover -s tests -p 'test_cli_concurrency.py'

5. Giới hạn cần giữ khi áp dụng nơi khác

Mọi writer phải tuân thủ cùng cơ chế khóa; một công cụ tự ghi JSON ngoài giao thức vẫn có thể phá trạng thái. Đây là giải pháp điều phối tiến trình cho đích cài local, không tự trở thành transaction database, khóa phân tán hoặc bảo đảm phục hồi mọi sự cố mất điện. Filesystem mạng và yêu cầu độ bền dữ liệu cần đánh giá riêng.

Bài học thực hành là kiểm tra tính đúng của toàn bộ trạng thái cuối, không chỉ mã thoát hoặc cú pháp JSON. Dùng systematic-debugging để đặt giả thuyết về lịch chạy, rồi giữ ca tái hiện làm regression test.

Mã nguồn và tài liệu