Tuần vừa qua, mình có cơ hội xây dựng một tính năng mới có sử dụng Bull Queue (docs.nestjs.com/techniques/queues) để giải quyết bài toán push notifications. Trước đây, do đã từng làm việc với Bull nên mình tự tin lắm, “ăn mày quá khứ” về độ hiểu biết và … thôi vô vấn đề chính nè.
Mình có 2 service A và B giao tiếp thông qua các Job events được đẩy vào queue.
⇒ Note: Mình làm màu chia ra 2 services để dễ scale thôi, mọi người gom lại trong một service cũng được nha
Service A: chịu trách nhiệm consume event xyz rồi add job vào queue
@Injectable()
export class ServiceA {
constructor(
@InjectQueue('noti-queue')
private notiQueue: Queue,
) {
}
async listenEvent(event: Event) {
await this.notiQueue.add({
{ name: `${event.payload.name}` },
{
jobId: `${event.id}`, // Override jobId - default: unique integer
timeout: 60 * 1000, // 1 minute
removeOnComplete: {
age: 300 // 5 minutes
}
} as JobOptions
})
}
}
Mình muốn add một job vào noti-queue và sẽ được remove khi đã hoàn thành xử lý sau 5 phút (vì có nhu cầu khác nên team mình chưa muốn remove liền), chỗ này có cái lưu lý nhỏ thôi:
timeout thì đang tính trên đơn vị milliseconds
age của removeOnComplete tính trên seconds
Service B: đóng vai trò là 1 processor sẽ lấy job vừa được add vào noti-queue ra để xử lý các nghiệp vụ cần thiết.
@Processor('noti-queue')
export class ServiceB {
@Process()
async process() {
...
}
}
Sau khi process completely, mình có set thuộc tính removeOnComplete với age = 5 phút, để clean up bớt cho Redis, nếu bạn có nhu cầu trace log thì có thể tăng age lên cũng được (Quá khứ mình tệ nên muốn xóa nhanh, không muốn nhìn lại 😭)
Testing
Dựng xong hết rồi, mình bắt đầu testing ✍️:
consume event và add new job vào lúc 10:00 am
processor xử lý và mark job completed ngay luôn (nếu bạn có set delay thì sẽ dựa vào thời gian đó để được process nha)
10:06, 10:07, v.v trôi qua, hmmm, mình đang mong muốn job phải được xóa khỏi queue, do mình set age = 5min thôi, kỳ ta, mình bắt đầu nghi ngờ nhân sinh, mình đi check lại doc của Bull, mình assume là sau khi process thành công 5 phút, job phải bị xóa khỏi queue
10:15, mình đi add thêm một job mới để test lại, khi job thứ 2 này vừa được process completely, job 1 đã được xóa. 🤯 Trước đây, mình thường set
removeOnComplete: true
luôn nên khi process done sẽ được xóa liền, giờ sử dụng một thuộc tính khác vô tình nhìn ra lỗ hỏng kiến thức của mình 🙏.
Root-cause
Mình đi clone source code của package Bull https://github.com/OptimalBits/bull?tab=readme-ov-file#documentation về để tìm hiểu vấn đề kỹ hơn. Chỗ unpack ARGV[6] này sẽ được lấy ra từ removeOnComplete
của mình truyền vào nha. Case của mình sẽ không set count nên lúc này maxCount sẽ được hiểu là nil
, mà nil ~= 0
sẽ đi vào tới block này và check tiếp
maxAge ~= nil
, phần còn lại là tinh hoa hội tụ của Redis.
Mình xin giải thích ngắn gọn logic của khúc này như vầy nè:
Ở đây, khi có một job mới được process completely, nó sẽ mark 1 score (timestamp) và add vào 1 set trong Redis (giống như stack nha mấy ông)
Sau đó sẽ đi compare latest score của job mới được add vào với age mình đã define trước đó, nếu những job nào có score nhỏ hơn sẽ được đánh giá là old (outdated) và bị xóa khỏi queue
Trước đó mình lầm tưởng, 10:00 xử lý done thì 10:05 sẽ được xóa nhưng lý do không phải vậy, nó phải chờ khi có 1 job mới được latest completed mới đi compare để quyết định xóa nè.
Đến đây hy vọng phần kiến thức nhỏ của mình về BullQueue có thể giúp ích được cho các bạn để các bạn sẽ tránh được những sai lầm mình đã trải qua với removeOnComplete của Bull Queue.
Bài viết chắc chắn còn nhiều thiếu sót, mình hi vọng được mọi người đóng góp và chia sẻ để mình có thể học hỏi và cải thiện. Xin chân thành cảm ơn mọi người đã dành thời gian đọc bài viết của mình.
Nếu mọi người thấy bài viết thú vị thì cho mình xin 1 like hoặc subscribe nha. Mến chúc mọi người tháng mới những điều tốt đẹp nhất.