4. Code
- Bỏ qua phần tạo bảng và CRUD
- Tạo một service để handle việc gửi nhận thông báo (sms / email). Mình sử dụng SNS của AWS
# Email # frozen_string_literal: true class SendReminderEmailService < BaseService def initialize(title, message) @message = message end def run credentials = Aws::Credentials.new(Rails.application.credentials.dig(:aws, :sns, :access_key_id), Rails.application.credentials.dig(:aws, :sns, :secret_access_key)) sns = Aws::SNS::Client.new(region: 'us-east-2', credentials: credentials) sns.publish({ topic_arn: ENV.fetch('SNS_REMINDER_ARN'), subject: title, message: message }) end private attr_reader :title, :message end
# SMS # frozen_string_literal: true class SendSmsService < BaseService def initialize(phone_number, message) @phone_number = phone_number @message = message end def run credentials = Aws::Credentials.new(Rails.application.credentials.dig(:aws, :sns, :access_key_id), Rails.application.credentials.dig(:aws, :sns, :secret_access_key)) sns = Aws::SNS::Client.new(region: 'us-east-2', credentials: credentials) sns.publish({ phone_number: phone_number, message: message }) end private attr_reader :phone_number, :message end
Ý định ban đầu của mình là sẽ gửi thông báo thông qua SMS tuy nhiên để gửi SMS thì phải đăng ký sender id với nhà mạng. Tham khảo bài này
Có nhiều dịch vụ bán sender id đã đăng ký sẵn ở Việt Nam tuy nhiên không muốn tốn thêm tiền nên mình quyết định không dùng 😁. Tuy nhiên nếu bạn nào muốn thử nghiệm có thể mua dùng thử
Tiếp theo là tạo backgroundjob để thực thi việc gửi thông báo.
Có nhiều dịch vụ bán sender id đã đăng ký sẵn ở Việt Nam tuy nhiên không muốn tốn thêm tiền nên mình quyết định không dùng 😁. Tuy nhiên nếu bạn nào muốn thử nghiệm có thể mua dùng thử
Tiếp theo là tạo backgroundjob để thực thi việc gửi thông báo.
class ExecuteReminderJob include Sidekiq::Job def perform(reminder_id, user_id) reminder = Reminder.find(reminder_id) user = User.find(user_id) # SendSmsService.run(user.phone_number, reminder.content) SendReminderEmailService.run(reminder.title, reminder.content) end end
- Cuối cùng là cronjob chạy vào 00:00 hàng ngày để tạo các ExecuteReminderJob:
class GenerateReminderJob include Sidekiq::Job def perform user_reminders = UserReminder.all.includes(:user, :reminder) current_time = Time.now user_reminders.each do |user_reminder| reminder = user_reminder.reminder next if reminder.day.exclude?(current_time.strftime('%a')) user = user_reminder.user execute_time = current_time.beginning_of_day.since(reminder.hour.to_i.hour).since(reminder.minute.to_i.minute) ExecuteReminderJob.perform_at(execute_time, user_reminder.reminder_id, user_reminder.user_id) end end end
ENV.each_key do |key| env key.to_sym, ENV[key] end set :enviroment, ENV["RAILS_ENV"] every 1.day, at: '00:00 am' do runner 'GenerateReminderJob.perform_async' end
Ơ vậy đối với các bản ghi reminder được tạo ngày hôm nay mà thời gian > thời gian hiện tại thì hôm nay sẽ không gửi thông báo à? 🤔
Vậy là phải thêm 1 after_create để handle trường hợp này. Mình đặt nó vào trong user_reminder
Thật ra trong trường hợp gửi email thì đặt nó vào reminders cũng được nhưng mình vẫn muốn để khoảng trống biết đâu sau này muốn thử nghiệm gửi sms với số điện thoại trong bảng user thì sao =)))
Vậy là phải thêm 1 after_create để handle trường hợp này. Mình đặt nó vào trong user_reminder
Thật ra trong trường hợp gửi email thì đặt nó vào reminders cũng được nhưng mình vẫn muốn để khoảng trống biết đâu sau này muốn thử nghiệm gửi sms với số điện thoại trong bảng user thì sao =)))
after_create :schedule_reminder_init private def schedule_reminder_init current_time = Time.now current_hour = current_time.hour current_minute = current_time.strftime('%M').to_i return if reminder.day.exclude?(current_time.strftime('%a')) || (reminder.hour.to_i <= current_hour && reminder.minute.to_i < current_minute) execute_time = current_time.beginning_of_day.since(reminder.hour.to_i.hour).since(reminder.minute.to_i.minute) ExecuteReminderJob.perform_at(execute_time, reminder_id, user_id) end
5. Docker
- Để chạy được cronjob + sidekiq ta cần một container redis và một container với môi trường giống hệt container chạy app rails tuy nhiên con này chỉ có chức năng duy nhất là thực hiện cronjob và sidekiq.
- Đối với server thật thì 2 container này sẽ chạy trên 2 cái server riêng tuy nhiên mình chưa có cơ hội động vào bao giờ. Lý do cho việc này là vì những tác vụ chạy background job thường rất nặng đặc biết là những tác vụ liên quan tới file sẽ ngốn ram nhiều. Chạy riêng 2 server sẽ tăng đáng kể tốc độ.
- Nhưng làm thế nào mà job truyền từ con server Rails sang con server chạy Sidekiq được? 🙄
- 🗨️Job sau khi được tạo ra sẽ không truyền thẳng vào sidekiq mà sẽ đi vào trong queue của Redis, sau đó sidekiq sẽ trỏ vào trong Redis để lấy job ra thực thi.
- Như trong chức năng reminders này của mình job có 2 luồng đi vào redis một là từ rails app container (before_create trong user_reminders) và một là từ con sidekiq (chính xác là từ cron sẽ gọi vào rails app và tạo job).
- Container app và sidekiq sẽ mount chung vào cùng một volume trong trường hợp này của mình và sidekiq sẽ dùng lại image của rails app đã build nên không có tình huống 2 con container này bị lệch code nhau.
Thêm những dòng sau vào docker-compose.prod.yml
redis: image: redis:7.0.8 env_file: - .env.docker ports: - 6379:6379 command: ["bash", "-c", "docker-entrypoint.sh --requirepass $${REDIS_PASSWORD}"] networks: - internal sidekiq: container_name: blog_sidekiq image: blog_image:latest command: "bundle exec sidekiq" volumes: - .:/blog depends_on: - postgres - redis - app networks: - internal environment: - RAILS_ENV=production env_file: - .env
Nhớ là thêm REDIS_URL vào trong file .env nhé
Giờ thì docker-compose up --build thôi 🙌
Xem lại Part 1
Giờ thì docker-compose up --build thôi 🙌
Xem lại Part 1