Báo cáo chuyển đổi warehouse-service

batch · batch/job-daily-update-status · job

SCHEDULER@Scheduled(cron = "0 25 0/4 * * ?") dailyUpdateStatus

Vòng
1
Cổng G1–G6
PENDING_SONAR
Bước
sonar-check
Nhánh đích
feat/thongpd/migrate-batch2

Phân tích luồng sau chuyển — batch/job-daily-update-status — vòng 1

logic-reviewer viết dựa trên code đã implement (không phải thiết kế trong spec), đối chiếu với code cũ. Dev đọc file này cùng changes.diff: mỗi luồng cho biết code mới đi qua những đâu, khác code cũ ở bước nào, và chỗ nào cần đọc kỹ.

Viết tắt: BMS = easyposbackend1:src/main/java/vn/softdreams/easypos/service/impl/BatchesManagementServiceImpl.java; SCH = presentation/scheduler/BatchStatusUpdateScheduler.java; SVC = application/batch/BatchStatusUpdateService.java; REPO = infrastructure/relational/batch/adapter/BatchRelationalRepository.java; WR = infrastructure/relational/batch/adapter/BatchStatusWriter.java; EW = domain/batch/model/BatchExpiryWarning.java; SP = domain/batch/model/BatchStatusPolicy.java (đường dẫn dưới src/main/java/com/softdreams/easypos/warehouse/).

F-01 Cập nhật trạng thái hạn dùng của lô

  • Điểm vào: cũ BMS:113-128 → mới SCH:44-57 (updateStatusesOfBatches)
  • Quy tắc: R-01, R-02, R-03, R-04, R-05, R-06

Sơ đồ luồng mới (theo code thực tế)

sequenceDiagram
    participant SCH as BatchStatusUpdateScheduler.updateStatusesOfBatches
    participant L as DistributedLockManager.acquireLock
    participant S as BatchStatusUpdateService.updateStatuses (@Transactional)
    participant R as BatchRelationalRepository
    participant W as BatchStatusWriter
    Note over SCH: cờ app.scheduler.enabled + app.scheduler.batch-status-update.enabled; cron 0 25 0/4 * * ?
    SCH->>SCH: try { clientId = BatchExpiryWarningScheduler.clientId()
    SCH->>L: acquireLock(job-lock-daily-update-status, clientId, 10m)
    L-->>SCH: false → LOG.info(LOCK_KEY), return
    SCH->>S: updateStatuses() [proxy, mở transaction]
    S->>R: findBatchesWithExpiryDate()
    R->>W: findWithExpiryDate(): JPQL SELECT b ... WHERE expDate IS NOT NULL (entity managed) → BatchStatusCandidate
    S->>S: today = LocalDate.ofInstant(clock.instant(), systemDefault).atStartOfDay
    loop mỗi lô
      S->>S: BatchExpiryWarning(unit, value) + BatchStatusPolicy.statusOf (NPE nếu unit null / value null với unit 1-3)
      S->>S: status khác batch.status() → thêm BatchStatusChange
    end
    S->>S: changes rỗng → return (commit rỗng)
    S->>R: updateStatuses(changes, ZonedDateTime.ofInstant(clock.instant()))
    R->>W: find (cache cấp 1) → setStatus, updater=0, updateTime, LegacyCleanText.clean(code/lotNo/normalizedName)
    S-->>SCH: commit → Hibernate UPDATE dynamic dòng dirty
    SCH->>SCH: catch RuntimeException → LOG.error (sau rollback)

Đối chiếu từng bước

# Luồng cũ (repo:path:dòng) Luồng mới (path:dòng) Kết luận
1 Cron 0 25 0/4 * * ?, chỉ cờ chung app.scheduler.enabled — BMS:113; easyposbackend1:.../config/SchedulerConfiguration.java:9-10 SCH:21,44; config/SchedulingConfiguration.java:8-9; application.yml batch-status-update.enabled mặc định false KHÁC có chủ đích (D-01)
2 Transaction cấp class mở trước khoá — BMS:55-57,113-115 SCH không có transaction; khoá trước, transaction mở ở SVC:40 KHÁC có chủ đích (D-09)
3 clientId = IPv4 BaseCommon.getIpv4()/"local.server.external"; whitelist config.json — BMS:116,401-407 SCH:46 → BatchExpiryWarningScheduler.clientId() (InetAddress.getLocalHost()), không whitelist KHÁC có chủ đích (D-02)
4 SETNX 10 phút, không nhả; null → chạy; có chủ → log chủ khoá — BMS:117-127 SCH:47-50, không releaseLock; false → log hằng LOCK_KEY KHÁC có chủ đích (D-03) cho nhánh race; log khác: KHÁC (F-logic-1)
5 findAllByExpDateIsNotNull(), mọi công ty, entity managed — BMS:414 SVC:42 → REPO:181-184 → WR:21,31-37 JPQL expDate IS NOT NULL, entity managed KHỚP
6 today = ZonedDateTime.now().toLocalDate().atStartOfDay(systemDefault) sau khi nạp — BMS:415 SVC:43-44 từ Clock.systemDefaultZone() sau khi nạp KHỚP
7 getWarningDate: switch Integer (null → NPE), 1/2/3 → ngày/tháng/năm (value null → NPE), khác → 0 — BMS:551-568 EW:55-67 (unit == DAY unboxing null → NPE; Period.ofDays(null) → NPE; unit lạ → ZERO kể cả value null) KHỚP
8 today < warningDate → 1; !today.isAfter(exp) → 2; còn lại 0 — BMS:420-426 SP:29-32 KHỚP
9 Đọc exp_date ZonedDateTime (Hibernate 5.6, jdbc.time_zone VN, đặt múi JVM) WR:59 → BatchDateTimes.toZonedDateTime (Timestamp đọc theo jdbc.time_zone VN của profile dev/uat/prod → instant → múi JVM) KHỚP
10 Set status mọi lô; Hibernate chỉ UPDATE dòng status đổi (Integer.equals, NULL → khác) — BMS:417-429 SVC:49-51 lọc !Integer.valueOf(status).equals(batch.status()) KHỚP
11 Lỗi ở lô bất kỳ → không ghi gì (ghi lúc commit) — BMS:409-430 Tính hết trước (SVC:46-52), NPE ném trước updateStatuses KHỚP
12 Không dòng đổi → saveAll không UPDATE SVC:53-55 return sớm, commit rỗng KHỚP
13 @PreUpdate dòng dirty: updater = auditor (0), update_time = thời điểm flush từng dòng, CleanTextListener trên code/lot_no/normalized_name — easyposbackend1:.../domain/Batches.java:13-16; AbstractAuditingEntity.java:49-59; LF-30, LF-31 WR:43-53: status, updater=0, updateTime = một now lấy sau khi tính, LegacyCleanText.clean ba cột KHỚP (status/updater/làm sạch); update_time: KHÁC có chủ đích (D-06)
14 @DynamicUpdate → UPDATE chỉ cột đổi lúc commit BatchJpaEntity @DynamicUpdate, không persist/merge/flush tay KHỚP (UNVERIFIED tới khi chạy MSSQL thật, K-13)
15 Lỗi thoát dailyUpdateStatus → Spring log, rollback — BMS:113-128 SVC ném qua proxy → rollback; SCH:53-55 bắt RuntimeException, log KHÁC có chủ đích (D-10)

Dữ liệu và side effect

Bảng / topic / cache Cũ (đọc/ghi, điều kiện) Mới (đọc/ghi, điều kiện) Ghi chú
Redis job-lock-daily-update-status SETNX = IPv4, TTL 10 phút, không nhả; trong transaction acquireLock cùng khoá, TTL 10 phút, không nhả; ngoài transaction D-02, D-03, D-09; chung Redis với monolith → loại trừ nhau
batches (đọc) mọi cột, mọi công ty, exp_date IS NOT NULL như cũ (entity đầy đủ) D-05 giữ
batches (ghi) UPDATE dòng status đổi: status, updater (nếu khác 0), update_time (flush từng dòng), chuỗi làm sạch nếu khác UPDATE dòng status đổi: status, updater (nếu khác 0), update_time (= một now), chuỗi làm sạch nếu khác D-06, D-07

Transaction, khoá, idempotency, tenant

  • Ranh giới transaction: cũ @Transactional cấp class bao cả khoá và cập nhật → mới chỉ SVC.updateStatuses (SVC:40), gọi qua proxy từ SCH:52; khoá ngoài (D-09). Catch ở SCH ngoài transaction → rollback vẫn xảy ra (K-03).
  • Khoá / cập nhật nguyên tử: không khoá dòng, không @Version ở cả hai bên; ghi đè status theo dữ liệu nạp đầu transaction (F-logic-2, nghi-bug-cu, cũ = mới).
  • Idempotency, thời điểm phát event: không có event. Chạy lại (sau 10 phút hoặc node khác) → status đã đúng, không ghi thêm.
  • Lọc comId: không lọc ở cả hai (job hệ thống, mọi công ty — R-03, D-05). Không có cache.
  • Truy vấn: một SELECT; find trong WR:46 lấy từ persistence context, không N+1. Đọc DB chính (transaction không readOnly, K-24).

Lệch so với luồng mới đã duyệt (spec mục 6)

không

Dev cần chú ý khi review

Vị trí Vì sao
WR:46-52 Audit + làm sạch chuỗi tự mô phỏng thay listener của monolith; chỉ chạy cho dòng đổi status. Cột thực tế trong câu UPDATE phụ thuộc dirty-check Hibernate 6 + @DynamicUpdate — UNVERIFIED tới khi chạy MSSQL thật.
WR:49 + application-*.yml hibernate.jdbc.time_zone update_time/exp_date dựa vào jdbc.time_zone: Asia/Ho_Chi_Minh có ở profile dev/uat/prod; base application.yml không có.
SVC:43-44,56 today và now lấy từ Clock theo múi JVM (D-08); hai lần đọc clock.instant().
SVC:48 / EW:60-66 NPE có chủ đích cho lô thiếu cấu hình cảnh báo (D-04) → cả lượt rollback. Đừng "sửa" thành bỏ qua dòng.
SCH:47-55 Khoá ngoài transaction, không nhả; catch RuntimeException ngoài proxy (D-09, D-10); log chủ khoá khác cũ (F-logic-1).

Nguồn của trang này

  • Phân tích luồng migrations/batch/job-daily-update-status/verify/round-1/flow-analysis.md vòng 1 · sha256 fbfea2a1d931

v1 dựng 2026-11-09 14:46 · base commit đích 661efe9b8c