Skip to content

Task 1-2: Synchronized gPackageId để tránh Race Condition

Phase: 1 Priority: High Module: ConnectedClient Depends on: Không có Reference: docs/BountyHunter-ControlServer/details/feature-iot-bridge/SPEC.md

Background

gPackageIdint instance field trong ConnectedClient, được mutate từ 2 thread khác nhau mà không có synchronization: (1) socket-reading thread đọc/ghi trong queryMachineState()queryGameResult(), và (2) JMS command thread ghi trong startGame(). Java int không đảm bảo atomic read-modify-write, nên 2 threads có thể đọc cùng giá trị, increment riêng lẻ, và write cùng 1 giá trị mới — tạo ra packet ID trùng lặp. Khi máy nhận 2 packets với cùng ID, nó sẽ reject lệnh thứ 2 hoặc behave không xác định theo binary protocol spec.

Tasks

Note: AtomicInteger là lựa chọn tốt hơn synchronized method vì nó lock-free (dùng CAS) và không block socket-reading thread khi JMS thread đang increment. Tuy nhiên, nếu startGame() cần đọc gPackageId VÀ build packet trong 1 atomic operation, cần xem xét synchronized block rộng hơn. Kiểm tra xem executeGameAction() (nếu tồn tại) có cần synchronize toàn bộ sequence không.

  • [ ] Đổi khai báo gPackageId từ int sang AtomicInteger:
// BEFORE:
private int gPackageId = 1;

// AFTER:
private final AtomicInteger gPackageId = new AtomicInteger(1);
  • [ ] Tìm và thay thế tất cả call sites sử dụng gPackageId:
// BEFORE (pattern phổ biến):
gPackageId++;
buildPacket(gPackageId, ...);

// AFTER — getAndIncrement() trả về giá trị TRƯỚC khi tăng:
int currentId = gPackageId.getAndIncrement();
buildPacket(currentId, ...);
  • [ ] Kiểm tra startGame(), queryMachineState(), queryGameResult() — mỗi method phải dùng 1 lần getAndIncrement() duy nhất per packet gửi
  • [ ] Nếu gPackageId được reset về 0 hoặc 1 ở bất kỳ đâu, đổi sang gPackageId.set(1)
  • [ ] Xem xét executeGameAction(): nếu có thể bị gọi đồng thời từ nhiều JMS messages, cần synchronized block bao gồm toàn bộ "build packet → send" sequence để tránh interleaved bytes trên socket output stream:
private synchronized void executeGameAction(int command, Object params) {
    int packetId = gPackageId.getAndIncrement();
    byte[] packet = buildPacket(packetId, command, params);
    sendDataToMachine(packet);
}
  • [ ] Review out.write() (OutputStream của socket) — OutputStream không thread-safe, nên toàn bộ write sequence cũng cần synchronized

Verification / Acceptance Criteria

  • [ ] Stress test: 2 threads đồng thời gọi startGame()moveLeft() 1000 lần mỗi thread → tất cả packet IDs là unique, không có giá trị trùng lặp
  • [ ] Packet IDs tăng dần (monotonically increasing) và không bị reset giữa chừng trừ khi intentional
  • [ ] Không có ClassCastException hoặc compilation error sau khi đổi sang AtomicInteger (int → AtomicInteger requires updating all usages)
  • [ ] Unit test: concurrent startGame() calls từ executor pool → verify gPackageId.get() equals số lần gọi + 1

Files to Modify

  • src/main/java/.../ConnectedClient.java