Transaction, locking và concurrency test
Concurrency test phải điều khiển interleaving và chứng minh invariant, không dựa vào xác suất scheduler tạo collision.
1. Transaction boundary và visibility
Mỗi transaction cần connection/session độc lập. Muốn test commit visibility, ghi và commit ở transaction A rồi đọc từ transaction B; đừng đọc lại bằng persistence context đã cache entity.
H2 không chứng minh PostgreSQL dialect, MVCC, locking, collation hay constraint semantics. Với behavior phụ thuộc database, dùng đúng engine và version family như production.
2. Optimistic và pessimistic locking
Optimistic locking: A và B đọc cùng version; A commit trước, B phải thất bại khi update version cũ. Assert lỗi conflict và bảo toàn invariant.
Pessimistic locking: A giữ lock, B cạnh tranh và phải block hoặc timeout theo cấu hình. Mọi future/wait trong test đều phải có timeout để CI không treo.
- Database lock/version test phải chạy trên database thật.
- Không assert chỉ “có exception”; assert row cuối cùng, version và state hợp lệ.
- Phân biệt deadlock, lock timeout và optimistic conflict vì remediation khác nhau.
3. Deterministic scheduling
var ready = new CountDownLatch(workers);
var start = new CountDownLatch(1);
// Worker: ready.countDown(); start.await(); executeConflict();
assertTrue(ready.await(2, TimeUnit.SECONDS));
start.countDown();
Latch hoặc barrier chỉ điều khiển checkpoint đã biết. Với race tinh vi, dùng hook/fake repository hoặc stress test có seed và số iteration được kiểm soát. sleep chỉ đoán timing và tạo flaky test.
4. Assert business invariant
Sau concurrency, không chỉ đếm exception. Kiểm tra tổng tiền được bảo toàn, balance không âm, một idempotency key chỉ tạo một effect, version tăng đúng và transaction thất bại không để partial state.
- Đọc lại bằng context/connection mới để tránh cache.
- Dùng unique test data để parallel test không tranh chấp ngẫu nhiên.
- Assert cả success path, conflict path và retry/duplicate behavior.
5. Failure modes trong production
- Deadlock: hai transaction lấy lock theo thứ tự khác nhau.
- Lost update: read-modify-write không có version hoặc lock bảo vệ.
- Phantom/non-repeatable read: giả định isolation level không đúng với database thật.
- Pool starvation: transaction giữ connection trong lúc chờ external call.
- Parallel contamination: test pass tuần tự nhưng dùng chung row/schema khi CI chạy song song.
- Unbounded wait: thiếu timeout khiến deadlock biến thành pipeline treo.
6. Checklist tự đánh giá
- Tôi có thể test commit visibility bằng hai transaction độc lập.
- Tôi phân biệt optimistic conflict, lock timeout và deadlock.
- Tôi điều khiển được concurrency bằng latch/barrier thay vì sleep.
- Tôi assert business invariant và partial-state behavior.
- Tôi dùng database thật khi test MVCC, lock, isolation hoặc versioning.
- Tôi đặt timeout cho mọi wait/future trong concurrency test.