PR + Incident Write-up · Threads API v2

LikeJob Sync: Full Debug Trail, Kỹ Thuật Khóa Trùng, và Cách Ghi Issue

9 files touched +821 / −99 incident date 2026-05-14 commit d5371a5 scope like queue / worker / redis client
Mục tiêu tài liệu Ghi lại đầy đủ kỹ thuật đã dùng để xử lý lỗi likeJob chạy dày, lock bị đè, lỗi runtime Redis command, và chốt phương án ổn định cuối cùng. Tài liệu này phục vụ review PR, onboarding backend, và tái sử dụng khi mở issue sau này.
TL;DR

Vấn đề không phải một bug đơn lẻ, mà là chuỗi lệch pha giữa scheduler, worker concurrency, API Redis runtime, và chiến lược logging. Cách giải quyết hiệu quả là: cô lập triệu chứng theo từng lớp (scheduler → lock → pop batch → parse payload), chứng minh bằng dữ liệu runtime, rồi chốt lại bằng implementation tương thích client hiện tại + issue note để team không lặp lại.

1) Bối Cảnh & Triệu Chứng

Triệu chứng đầu tiên: worker log Processing add like jobs... liên tục, tần suất không đều, nhiều timestamp trùng nhau theo cặp. Cảm giác ban đầu giống "job chạy đúng nhưng không ăn dữ liệu".

Sau khi thêm lock log, tiếp tục thấy nhiều lần trigger và xuất hiện lỗi rỗng error: {}, khiến việc tìm nguyên nhân thật bị mù thông tin.

Before
  • Queue repeat có dấu hiệu chồng scheduler cũ
  • Job có lúc chạy dày hơn 5s (không đều)
  • Log lỗi thiếu message/stack nên khó truy nguyên
  • Dùng API Redis không tương thích runtime hiện tại
After
  • Scheduler được cleanup trước khi tạo lại
  • Có lock token theo tiến trình để tránh chạy chồng
  • Luồng pop batch dùng API tương thích chắc chắn
  • Tài liệu hóa đủ để tái hiện và mở issue chuẩn

2) Timeline Debug (Theo Thứ Tự Thật)

B1: Xác nhận worker thực sự chạy
Log xuất hiện đều, chứng tỏ consumer vẫn active.
B2: Nghi ngờ scheduler bị trùng
Do nhịp trigger không giống cố định 5 giây và có cặp log sát nhau.
B3: Thêm distributed lock
Dùng SET key value NX EX + token ownership khi release.
B4: Cải thiện log lock để thấy acquired/not acquired
Tách rõ "trigger" và "được quyền chạy".
B5: Lộ lỗi thật
TypeError: redisService.lMPop is not a function.
B6: Tra cứu package + typings cục bộ
Chốt được API redis@5.12.1 không dùng signature như giả định ban đầu.
B7: Chuyển sang pop batch tương thích chắc chắn
Dùng vòng rPop thay vì phụ thuộc method không ổn định.
B8: Giảm noise log theo yêu cầu
Bỏ helper log lỗi rườm rà, giữ luồng xử lý gọn.

3) Kỹ Thuật Tra Cứu: Vì Sao Kết Luận Như Vậy

Điểm quan trọng là phân biệt rõ 3 lớp: docs bên ngoài, type definitions trong local, và behavior runtime thực tế. Kết luận chỉ chốt khi cả 3 cùng khớp.

Dữ liệu runtime (log thật) evidence

Bằng chứng quyết định: lỗi runtime báo trực tiếp lMPop is not a function. Nghĩa là object client hiện tại không expose method đó theo cách đang gọi.

[2026-05-14 13:31:00 +0700] ERROR Like sync job failed
TypeError: import_redis.redisService.lMPop is not a function
  at LikeWorker.popElements (.../workers/like.worker.ts:129:41)
Tra cứu package cài thật trong project evidence

Đã kiểm tra dependency thực tế: redis@5.12.1@redis/client@5.12.1. Đây là nguồn sự thật của API đang chạy.

npm ls redis @redis/client --depth=1
threads-api-v2
+-- redis@5.12.1
`-- @redis/client@5.12.1
Tra cứu d.ts để đối chiếu signature evidence

Trong declaration cục bộ, có module LMPOP, RPOP, RPOP_COUNT. Nhưng call-site thực tế của client không match assumption trước đó. Kết luận: phải gọi theo API đã kiểm chứng hoặc dùng phương án tương thích tối đa.

node_modules/@redis/client/dist/lib/commands/RPOP.d.ts
parseCommand(parser, key)  // 1 arg

node_modules/@redis/client/dist/lib/commands/RPOP_COUNT.d.ts
parseCommand(parser, key, count)

node_modules/@redis/client/dist/lib/commands/LMPOP.d.ts
exists in command module, but runtime method mapping may differ

4) Thay Đổi Kỹ Thuật Đã Áp Dụng

Phần này gom cả thay đổi cuối cùng và lý do kiến trúc đằng sau từng thay đổi.

workers/like.worker.ts mod +84 −49

Distributed lock + pop strategy tương thích. Bổ sung lock chống chạy chồng, nhóm theo post để giảm write, parse an toàn từng item, và pop tuần tự bằng rPop để không phụ thuộc API không ổn định trong runtime.

const lockToken = `${process.pid}:...`;
const isLocked = await acquireSyncLock(lockToken);
if (!isLocked) return;

for (let i = 0; i < count; i++) {
  const item = await redisService.rPop(key);
  if (!item) break;
  result.push(item);
}
src/modules/job/like-job/producer/like.producer.ts mod +43 −21

Scheduler hygiene. Trước khi add repeat job mới, cleanup các repeatable meta-job cũ cùng name để tránh tích lũy lịch chạy dư thừa qua nhiều lần restart/deploy.

const jobs = await likeQueue.getRepeatableJobs();
const duplicates = jobs.filter(j => j.name === INIT_SYNC_JOB);
await Promise.all(duplicates.map(j => likeQueue.removeRepeatableByKey(j.key)));
await likeQueue.add(INIT_SYNC_JOB, {}, { repeat: { every: 5000 }, attempts: 1 });
src/server.ts mod +45 −37

Boot sequence an toàn hơn. Dừng hành vi xóa queue khi start app; chuyển sang init scheduler có bắt lỗi riêng để không làm startup fail âm thầm.

src/modules/post/repository/like.repository.ts mod +22 −16

Sửa đúng nghiệp vụ unlike batch. Đổi điều kiện xóa sang pair-wise OR theo từng userId + postId, tránh xóa chéo ngoài ý muốn.

src/modules/post/repository/post.repository.ts mod +30 −35

Điểm chốt \"quá chuẩn\" của commit mới nhất. Hai query tăng/giảm like count đã được đơn giản hóa: bỏ JOIN likes, cập nhật trực tiếp trên bảng posts theo public_id. Cách này giảm coupling vào trạng thái bảng likes tại thời điểm đồng bộ và ổn định hơn khi xử lý batch.

UPDATE posts p JOIN likes l ON l.post_id = p.id
SET p.likes_count = GREATEST(p.likes_count ± ${count}, 0)
UPDATE posts
SET likes_count = GREATEST(likes_count ± ${count}, 0)
WHERE public_id = ${publicId}

5) Luồng Module Route (Flowchart)

Section này mô tả luồng đầy đủ từ API route đến worker và repository write path, theo phong cách flowchart tương tác giống tài liệu 13-flowchart-diagram.

isLiked=true isLiked=false POST /api/v1/posts/:id/like post.routes.ts router → middleware chain validate + auth middleware DTO + user context PostController.like() delegate to service isLiked? Service: like branch SADD + LPUSH likes_add Service: unlike branch SREM + LPUSH likes_remove HTTP response return likeCount fast path like_queue repeat scheduler INIT_SYNC_JOB every 5s LikeWorker lock SET NX EX token Pop Redis batch rPop loop + JSON parse Repository writes likeRepository + postRepository Sync done
process step decision terminal success

6) Chiến Lược Logging Đã Dùng

Phần này quan trọng vì chính logging strategy quyết định tốc độ tìm root cause.

1
Log theo pha xử lý, không log theo cảm giác
Pha lock, pha start, pha pop batch, pha group, pha write DB. Mỗi log tương ứng một "checkpoint" kỹ thuật.
2
Đầu tiên log để tìm nguyên nhân, sau đó giảm noise
Giai đoạn debug cần log chi tiết để mở khóa thông tin. Khi đã xác định root cause thì có thể dọn log cho clean runtime.
3
Nếu thấy error: {}, ưu tiên sửa cấu trúc log trước
Không có stack thì mọi giả thuyết đều yếu. Mục tiêu là luôn nhìn được vị trí fail cụ thể theo file/line.

7) Note Công Dụng Tạo Issue (Copy-Paste)

Issue kiểu này rất hữu dụng để team sau không debug lại từ đầu. Nên dùng issue như một hồ sơ kỹ thuật: mô tả triệu chứng, bằng chứng, giả thuyết, thử nghiệm, và quyết định cuối cùng.

## [Like Queue] Repeating sync job runs irregularly + Redis command mismatch

### 1) Symptoms
- Worker logs trigger irregularly (not strictly every 5s)
- Duplicate-like behavior suspected under repeat scheduling
- Runtime error observed: `redisService.lMPop is not a function`

### 2) Impact
- Like sync flow unstable
- Hard to trust batch processing correctness without root-cause fix

### 3) Root Causes
- Scheduler duplication risk from repeated bootstrap
- Runtime API mismatch between assumed Redis command method and installed client
- Limited observability in early logging phase

### 4) Evidence
- Runtime stacktrace from worker
- Local package version: redis@5.12.1
- Local d.ts verification under node_modules/@redis/client/dist/lib/commands

### 5) Fix Implemented
- Cleanup repeatable jobs before re-initialization
- Add lock token with NX/EX strategy
- Switch pop batch to compatible rPop loop
- Correct unlike delete condition to pair-wise OR

### 6) Verification
- Worker acquires lock as expected
- No `lMPop` TypeError after fix
- Batch logs show parsed item counts and grouped writes

### 7) Follow-up
- Add integration test for repeat scheduler duplication
- Add runbook note in docs/job-worker-flow.html

8) Test Plan / Verification Matrix

Scheduler cleanup works
Restart app nhiều lần vẫn chỉ còn một scheduler active theo name.
Lock prevents overlap
Khi 2 trigger gần nhau, chỉ 1 lần có quyền chạy đồng bộ.
Redis pop path stable
Không còn lỗi runtime về method không tồn tại.
Unlike batch correctness
Delete theo cặp user/post, tránh xóa chéo.
E2E regression cho like/unlike burst
Nên thêm test tạo burst 500 event để đo drain time và count consistency.

9) Rollout & Vận Hành

Với bài toán worker queue, rollout theo thứ tự tiến trình quan trọng hơn rollout theo endpoint API.

Step 1
Server
Deploy server để refresh scheduler logic và cleanup repeatable jobs cũ.
Step 2
Worker
Deploy worker logic lock + pop strategy mới.
Step 3
Observe
Quan sát lock-acquire pattern, batch size, và độ khớp dữ liệu DB.