Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/controllers/profiles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def create
end

ProfileMailer.registered(@profile, @conference).deliver_later

if @profile.public_profile.present?
redirect_to("/#{event_name}/public_profiles/#{@profile.public_profile.id}/edit")
else
Expand Down
3 changes: 2 additions & 1 deletion app/helpers/env_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ def env_name
end

def review_app_number
m = ENV['DREAMKAST_NAMESPACE'].match(/dreamkast-dev-dk-(.*)-dk/) || ENV['DREAMKAST_NAMESPACE'].match(/dreamkast-dev-dk-(.*)-fifo-worker/)
namespace = ENV['DREAMKAST_NAMESPACE'].to_s
m = namespace.match(/dreamkast-dev-dk-(.*)-dk/) || namespace.match(/dreamkast-dev-dk-(.*)-fifo-worker/)
if m
m[1].to_i
else
Expand Down
22 changes: 14 additions & 8 deletions app/helpers/job_helper.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
module JobHelper
include EnvHelper

DEFAULT_FIFO_QUEUE_URL = 'http://localhost:4566/000000000000/fifo.fifo'.freeze

def sqs_queue_url
if ENV['SQS_FIFO_QUEUE_URL'] == ''
cli = Aws::SQS::Client.new(region: 'ap-northeast-1')
result = cli.get_queue_url({ queue_name: "review_app_#{review_app_number}" + '.fifo' })
raise(result.error) unless result.successful?
result.queue_url
else
ENV['SQS_FIFO_QUEUE_URL']
end
return ENV['SQS_FIFO_QUEUE_URL'] if ENV['SQS_FIFO_QUEUE_URL'].present?

namespace = ENV['DREAMKAST_NAMESPACE'].to_s
return DEFAULT_FIFO_QUEUE_URL if namespace.blank?

cli = Aws::SQS::Client.new(region: 'ap-northeast-1')
result = cli.get_queue_url({ queue_name: "review_app_#{review_app_number}.fifo" })
raise(result.error) unless result.successful?
result.queue_url
rescue StandardError => e
Rails.logger.warn("Failed to resolve SQS queue URL (#{e.class}: #{e.message}), fallback to #{DEFAULT_FIFO_QUEUE_URL}")
DEFAULT_FIFO_QUEUE_URL
end
end
23 changes: 23 additions & 0 deletions app/jobs/prepare_announcement_deliveries_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class PrepareAnnouncementDeliveriesJob < ApplicationJob
queue_as :fifo
self.queue_adapter = :sqs unless Rails.env.test?

def perform(announcement_id)
announcement = Announcement.find(announcement_id)
profiles = announcement.target_profiles.select(:id, :email)

Rails.logger.info("[PrepareAnnouncementDeliveriesJob] announcement=#{announcement_id} target_count=#{profiles.count}")

ActiveRecord::Base.transaction do
profiles.each do |profile|
AnnouncementDelivery.find_or_create_by!(announcement:, profile_id: profile.id) do |d|
d.email = profile.email
d.status = :queued
end
end
announcement.update!(send_status: :processing)
end

SendAnnouncementBatchJob.perform_later(announcement_id)
end
end
33 changes: 33 additions & 0 deletions app/jobs/send_announcement_batch_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
class SendAnnouncementBatchJob < ApplicationJob
BATCH_SIZE = (ENV['ANNOUNCEMENT_BATCH_SIZE'] || 10).to_i
BATCH_INTERVAL = (ENV['ANNOUNCEMENT_BATCH_INTERVAL_SECONDS'] || 1).to_i

queue_as :fifo
self.queue_adapter = :sqs unless Rails.env.test?

def perform(announcement_id)
announcement = Announcement.find(announcement_id)
batch = announcement.announcement_deliveries.queued.limit(BATCH_SIZE)

Rails.logger.info("[SendAnnouncementBatchJob] announcement=#{announcement_id} batch_size=#{batch.count}")

batch.each do |delivery|
result = AnnouncementMailer.notify(announcement, delivery).deliver_now
delivery.update!(status: :sent, provider_message_id: result.message_id)
rescue StandardError => e
Rails.logger.warn("[SendAnnouncementBatchJob] delivery=#{delivery.id} failed: #{e.class}: #{e.message}")
delivery.update!(status: :failed, last_error: e.message)
end

announcement.update!(
sent_count: announcement.announcement_deliveries.sent.count,
failed_count: announcement.announcement_deliveries.failed.count
)

if announcement.announcement_deliveries.queued.exists?
SendAnnouncementBatchJob.set(wait: BATCH_INTERVAL.seconds).perform_later(announcement_id)
else
announcement.update!(send_status: :completed)
end
end
end
7 changes: 7 additions & 0 deletions app/mailers/announcement_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AnnouncementMailer < ApplicationMailer
def notify(announcement, delivery)
@announcement = announcement
@conference = announcement.conference
mail(to: delivery.email, subject: "#{announcement.conference.name}からのお知らせ")
end
end
22 changes: 22 additions & 0 deletions app/models/announcement.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class Announcement < ApplicationRecord
enum :receiver, { all_attendee: 0, only_online: 1, only_offline: 2, early_bird: 3 }
enum :send_status, { pending: 'pending', processing: 'processing', completed: 'completed' }
JA_RECEIVER = {
all_attendee: '全員',
only_online: 'オンライン参加者',
Expand All @@ -8,9 +9,12 @@ class Announcement < ApplicationRecord
}.freeze

belongs_to :conference
has_many :announcement_deliveries, dependent: :destroy

validates :receiver, presence: true

after_save :schedule_delivery, if: :saved_change_to_publish?

scope :published, -> {
where(publish: true)
}
Expand All @@ -19,6 +23,18 @@ def profile_names
JA_RECEIVER[receiver.to_sym]
end

def target_profiles
base = conference.profiles
case receiver
when 'all_attendee' then base
when 'only_online' then base.online
when 'only_offline' then base.offline
when 'early_bird' then base.where('profiles.created_at < ?', conference.early_bird_cutoff_at)
else
raise(ArgumentError, "Unknown receiver: #{receiver}")
end
end

def self.visible_to(profile)
base = where(publish: true, conference_id: profile.conference_id)
result = base.where(receiver: :all_attendee)
Expand All @@ -35,4 +51,10 @@ def self.visible_to(profile)

result
end

private

def schedule_delivery
PrepareAnnouncementDeliveriesJob.perform_later(id) if publish?
end
end
6 changes: 6 additions & 0 deletions app/models/announcement_delivery.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AnnouncementDelivery < ApplicationRecord
enum :status, { queued: 'queued', sent: 'sent', failed: 'failed' }

belongs_to :announcement
belongs_to :profile, optional: true
end
6 changes: 6 additions & 0 deletions app/views/admin/announcements/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
<th scope="col">To</th>
<th scope="col">Time</th>
<th scope="col">Announcement</th>
<th scope="col">送信状況</th>
<th scope="col">送信数</th>
<th scope="col">失敗数</th>
<th scope="col">Edit</th>
</tr>
</thead>
Expand All @@ -27,6 +30,9 @@
<td>
<%= simple_format announcement.body %>
</td>
<td><%= announcement.send_status %></td>
<td><%= announcement.sent_count %></td>
<td><%= announcement.failed_count %></td>
<td>
<%= link_to'Edit', edit_admin_announcement_path(id: announcement.id) %>
<%= link_to'Delete', admin_announcement_path(id: announcement.id), method: :delete, data: { confirm: 'Are you sure?'} %>
Expand Down
3 changes: 3 additions & 0 deletions app/views/announcement_mailer/notify.text.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%= @announcement.body %>

<%= render '/layouts/footer' %>
7 changes: 7 additions & 0 deletions db/migrate/20260222000000_add_send_status_to_announcements.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AddSendStatusToAnnouncements < ActiveRecord::Migration[8.0]
def change
add_column(:announcements, :send_status, :string, default: 'pending')
add_column(:announcements, :sent_count, :integer, default: 0, null: false)
add_column(:announcements, :failed_count, :integer, default: 0, null: false)
end
end
21 changes: 21 additions & 0 deletions db/migrate/20260222000001_create_announcement_deliveries.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class CreateAnnouncementDeliveries < ActiveRecord::Migration[8.0]
def change
create_table(:announcement_deliveries) do |t|
t.bigint(:announcement_id, null: false)
t.bigint(:profile_id)
t.string(:email, null: false)
t.string(:status, null: false, default: 'queued')
t.string(:provider_message_id)
t.text(:last_error)

t.timestamps
end

add_index(:announcement_deliveries, [:announcement_id, :status])
add_index(:announcement_deliveries, :announcement_id)
add_index(:announcement_deliveries, :profile_id)

add_foreign_key(:announcement_deliveries, :announcements, on_delete: :cascade)
add_foreign_key(:announcement_deliveries, :profiles, on_delete: :nullify)
end
end
21 changes: 20 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.0].define(version: 2026_02_15_133000) do
ActiveRecord::Schema[8.0].define(version: 2026_02_22_000001) do
create_table "admin_profiles", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.bigint "conference_id", null: false
t.string "name"
Expand All @@ -25,12 +25,29 @@
t.index ["user_id"], name: "fk_rails_bdfe0f01ea"
end

create_table "announcement_deliveries", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "announcement_id", null: false
t.bigint "profile_id"
t.string "email", null: false
t.string "status", default: "queued", null: false
t.string "provider_message_id"
t.text "last_error"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["announcement_id", "status"], name: "index_announcement_deliveries_on_announcement_id_and_status"
t.index ["announcement_id"], name: "index_announcement_deliveries_on_announcement_id"
t.index ["profile_id"], name: "index_announcement_deliveries_on_profile_id"
end

create_table "announcements", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.bigint "conference_id", null: false
t.datetime "publish_time", precision: nil
t.text "body", size: :medium, collation: "utf8mb4_0900_ai_ci"
t.boolean "publish"
t.integer "receiver", null: false
t.string "send_status", default: "pending"
t.integer "sent_count", default: 0, null: false
t.integer "failed_count", default: 0, null: false
t.index ["conference_id"], name: "index_announcements_on_conference_id"
end

Expand Down Expand Up @@ -787,6 +804,8 @@

add_foreign_key "admin_profiles", "conferences"
add_foreign_key "admin_profiles", "users"
add_foreign_key "announcement_deliveries", "announcements", on_delete: :cascade
add_foreign_key "announcement_deliveries", "profiles", on_delete: :nullify
add_foreign_key "announcements", "conferences"
add_foreign_key "attendee_announcement_middles", "attendee_announcements"
add_foreign_key "attendee_announcement_middles", "profiles"
Expand Down
4 changes: 2 additions & 2 deletions devbox.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
"REDIS_URL": "redis://127.0.0.1:6379",
"S3_BUCKET": "dreamkast-test-bucket",
"S3_REGION": "ap-northeast-1",
"SQS_FIFO_QUEUE_URL": "http://localhost:4566/000000000000/default",
"SQS_MAIL_QUEUE_URL": "http://localhost:4566/000000000000/default",
"SQS_FIFO_QUEUE_URL": "http://localhost:4566/000000000000/fifo.fifo",
"SQS_MAIL_QUEUE_URL": "http://localhost:4566/000000000000/fifo.fifo",
"DREAMKAST_API_ADDR": "http://localhost:8080",
"AWS_REGION": "ap-northeast-1",
"LIBRARY_PATH": "$DEVBOX_PROJECT_ROOT/.devbox/nix/profile/default/lib",
Expand Down
2 changes: 1 addition & 1 deletion docs/SETUP_DOCKER_COMPOSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ MYSQL_PASSWORD=root
MYSQL_DATABASE=dreamkast
REDIS_URL=redis://redis:6379
RAILS_MASTER_KEY=
SQS_FIFO_QUEUE_URL=http://localstack:4566/000000000000/default
SQS_FIFO_QUEUE_URL=http://localstack:4566/000000000000/fifo.fifo
EOF
```

Expand Down
2 changes: 1 addition & 1 deletion docs/SETUP_LOCAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export MYSQL_PASSWORD=password
export MYSQL_DATABASE=dreamkast
export REDIS_URL=redis://redis:6379
export RAILS_MASTER_KEY=
export SQS_FIFO_QUEUE_URL=http://localhost:9324/queue/default
export SQS_FIFO_QUEUE_URL=http://localhost:4566/000000000000/fifo.fifo
```

### 4. AWS CLIの設定とECRログイン
Expand Down
2 changes: 1 addition & 1 deletion init-localstack.sh
Original file line number Diff line number Diff line change
@@ -1 +1 @@
awslocal sqs create-queue --queue-name fifo
awslocal sqs create-queue --queue-name fifo.fifo --region ap-northeast-1 --attributes "FifoQueue=true"
2 changes: 1 addition & 1 deletion localstack/init/ready.d/create_queue.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env bash

awslocal sqs create-queue --queue-name fifo.fifo --region ap-northeast-1 --attributes "FifoQueue=true"
awslocal sqs create-queue --queue-name fifo.fifo --region ap-northeast-1 --attributes "FifoQueue=true"
8 changes: 8 additions & 0 deletions spec/factories/announcement_deliveries.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FactoryBot.define do
factory :announcement_delivery do
association :announcement
association :profile, factory: :alice
email { profile.email }
status { 'queued' }
end
end
Loading
Loading