[Part 2] Chức năng reminders

Đăng bởi Lưu Đại vào ngày 02-02-2023

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. 

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 =))) 
  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