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.
Writing JSON to a temporary file and using os.replace avoids publishing a partially written file. Two processes can still overwrite each other’s updates when both read an old state. VAS encountered this during concurrent skill installations.
Trong bài nàyOn this page
1. Hai lượt cài thành công nhưng thiếu một bản ghi1. Two successful installs, one missing record
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.
VAS stores installation records in .vas-manifest.json inside the destination directory. The CLI uses these records to manage installations. In the reproduced failure, concurrent installs of pdf and docx both produced directories and reported success, but the manifest retained only one entry.
The JSON remained valid. Parsing it successfully or checking that directories exist was insufficient. Uninstalling the skill whose receipt disappeared was refused because the corresponding management record was missing.
2. Atomic replace không bảo vệ toàn bộ phép cập nhật2. Atomic replacement does not protect the whole update
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:
This illustrative schedule starts with an empty manifest. Both writers read before the other commits:
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.
Each replacement can be complete while writer B still loses A’s update by committing an old snapshot. Locking only save_manifest is insufficient if both readers ran before acquiring the lock. Protect the full read → decide → modify → write sequence.
Install and uninstall must consider payload and manifest state within their transaction. Locking only installation misses an overlapping uninstall that can write back a snapshot containing a receipt another operation removed.
3. Khóa một đích cài cho cả giao dịch3. Lock one destination for the transaction
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ó:
VAS opens a stable .vas.lock file through destination_lock. The POSIX branch uses fcntl.flock; the Windows branch uses msvcrt.locking. Both installation and removal acquire the lock before reading the manifest. Separate destinations use separate locks instead of serializing the entire catalog.
This outline illustrates ordering, not a standalone script. apply_operation represents payload, receipt and error handling in the existing transaction logic:
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.
The writer still creates a temporary file in the same directory, flushes and fsyncs it, then calls os.replace. The lock coordinates writers; complete replacement handles file publication. This workflow needs both mechanisms.
Do not delete .vas.lock after every operation: an existing holder and a writer opening a recreated path could lock different underlying objects. Closing the handle releases the lock; the file’s existence does not mean a process holds it. VAS also rejects symlinked or unsuitable lock and manifest files.
4. Kiểm thử bằng các tiến trình thật4. Test using real processes
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.
tests/test_cli_concurrency.py creates a temporary catalog and destinations, then runs the CLI in separate processes. The first worker pauses after reading the manifest. The second must wait until the first finishes before reading the new state. This controls the overlap instead of repeatedly running commands and hoping to observe a race.
- Install + install retains both receipts and payloads.
- Uninstall + uninstall does not restore removed receipts.
- Install + uninstall in both orders preserves the correct final state.
- Independent destinations do not block one another.
- A waiting process can proceed after the lock holder exits.
- The lock file is reused and its name cannot be owned by a skill.
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:
Run from the VAS repository root with Python 3. This suite uses temporary fixtures and does not require installing into personal skill directories:
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ác5. Keep the limits when adapting the pattern
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.
Every writer must follow the same locking protocol; an outside tool writing JSON directly can still corrupt the state. This is local-process coordination for an installation destination, not automatically a database transaction, distributed lock or recovery guarantee for every power failure. Network filesystems and durability requirements need separate evaluation.
The practical lesson is to verify the complete final state, not just exit codes or JSON syntax. Use systematic-debugging to form scheduling hypotheses, then retain the reproducer as a regression test.