FEATURE: send max 200 emails every minute for bulk invites (#7875)

DEV: deprecate `invite.via_email` in favor of `invite.emailed_status`

This commit adds a new column `emailed_status` in `invites` table for
 tracking email sending status.
 0 - not required
 1 - pending
 2 - bulk pending
 3 - sending
 4 - sent

For normal email invites, invite record is created with emailed_status
 set to 'pending'.

When bulk invites are sent invite record is created with emailed_status
 set to 'bulk pending'.

For invites that generates link, invite record is created with
 emailed_status set to 'not required'.

When invite email is in queue emailed_status is updated to 'sending'

Once the email is sent via `InviteEmail` job the invite emailed_status
 is updated to 'sent'.
This commit is contained in:
Arpit Jalan
2019-07-19 11:29:12 +05:30
committed by GitHub
parent d26aa6e71e
commit eb9155f3fe
15 changed files with 185 additions and 26 deletions

View File

@@ -23,8 +23,13 @@ module Jobs
@current_user = User.find_by(id: args[:current_user_id])
raise Discourse::InvalidParameters.new(:current_user_id) unless @current_user
@guardian = Guardian.new(@current_user)
@total_invites = invites.length
process_invites(invites)
if @total_invites > Invite::BULK_INVITE_EMAIL_LIMIT
Jobs.enqueue(:process_bulk_invite_emails)
end
ensure
notify_user
end
@@ -104,7 +109,15 @@ module Jobs
end
end
else
Invite.invite_by_email(email, @current_user, topic, groups.map(&:id))
if @total_invites > Invite::BULK_INVITE_EMAIL_LIMIT
invite = Invite.create_invite_by_email(email, @current_user,
topic: topic,
group_ids: groups.map(&:id),
emailed_status: Invite.emailed_status_types[:bulk_pending]
)
else
Invite.invite_by_email(email, @current_user, topic, groups.map(&:id))
end
end
rescue => e
save_log "Error inviting '#{email}' -- #{Rails::Html::FullSanitizer.new.sanitize(e.message)}"

View File

@@ -15,8 +15,10 @@ module Jobs
message = InviteMailer.send_invite(invite)
Email::Sender.new(message, :invite).send
if invite.emailed_status != Invite.emailed_status_types[:not_required]
invite.update_column(:emailed_status, Invite.emailed_status_types[:sent])
end
end
end
end

View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true
require_dependency 'email/sender'
module Jobs
class ProcessBulkInviteEmails < Jobs::Base
def execute(args)
pending_invite_ids = Invite.where(emailed_status: Invite.emailed_status_types[:bulk_pending]).limit(Invite::BULK_INVITE_EMAIL_LIMIT).pluck(:id)
if pending_invite_ids.length > 0
Invite.where(id: pending_invite_ids).update_all(emailed_status: Invite.emailed_status_types[:sending])
pending_invite_ids.each do |invite_id|
Jobs.enqueue(:invite_email, invite_id: invite_id)
end
Jobs.enqueue_in(1.minute, :process_bulk_invite_emails)
end
end
end
end

View File

@@ -3,10 +3,16 @@
require_dependency 'rate_limiter'
class Invite < ActiveRecord::Base
self.ignored_columns = %w{
via_email
}
class UserExists < StandardError; end
include RateLimiter::OnCreateRecord
include Trashable
BULK_INVITE_EMAIL_LIMIT = 200
rate_limit :limit_invites_per_day
belongs_to :user
@@ -31,6 +37,10 @@ class Invite < ActiveRecord::Base
validate :user_doesnt_already_exist
attr_accessor :email_already_exists
def self.emailed_status_types
@emailed_status_types ||= Enum.new(not_required: 0, pending: 1, bulk_pending: 2, sending: 3, sent: 4)
end
def user_doesnt_already_exist
@email_already_exists = false
return if email.blank?
@@ -66,7 +76,7 @@ class Invite < ActiveRecord::Base
topic: topic,
group_ids: group_ids,
custom_message: custom_message,
send_email: true
emailed_status: emailed_status_types[:pending]
)
end
@@ -75,7 +85,7 @@ class Invite < ActiveRecord::Base
invite = create_invite_by_email(email, invited_by,
topic: topic,
group_ids: group_ids,
send_email: false
emailed_status: emailed_status_types[:not_required]
)
"#{Discourse.base_url}/invites/#{invite.invite_key}" if invite
@@ -89,8 +99,8 @@ class Invite < ActiveRecord::Base
topic = opts[:topic]
group_ids = opts[:group_ids]
send_email = opts[:send_email].nil? ? true : opts[:send_email]
custom_message = opts[:custom_message]
emailed_status = opts[:emailed_status] || emailed_status_types[:pending]
lower_email = Email.downcase(email)
if user = find_user_by_email(lower_email)
@@ -112,16 +122,20 @@ class Invite < ActiveRecord::Base
end
if invite
if invite.emailed_status == Invite.emailed_status_types[:not_required]
emailed_status = invite.emailed_status
end
invite.update_columns(
created_at: Time.zone.now,
updated_at: Time.zone.now,
via_email: invite.via_email && send_email
emailed_status: emailed_status
)
else
create_args = {
invited_by: invited_by,
email: lower_email,
via_email: send_email
emailed_status: emailed_status
}
create_args[:moderator] = true if opts[:moderator]
@@ -143,7 +157,10 @@ class Invite < ActiveRecord::Base
end
end
Jobs.enqueue(:invite_email, invite_id: invite.id) if send_email
if emailed_status == emailed_status_types[:pending]
invite.update_column(:emailed_status, Invite.emailed_status_types[:sending])
Jobs.enqueue(:invite_email, invite_id: invite.id)
end
invite.reload
invite
@@ -261,10 +278,11 @@ end
# invalidated_at :datetime
# moderator :boolean default(FALSE), not null
# custom_message :text
# via_email :boolean default(FALSE), not null
# emailed_status :integer
#
# Indexes
#
# index_invites_on_email_and_invited_by_id (email,invited_by_id)
# index_invites_on_emailed_status (emailed_status)
# index_invites_on_invite_key (invite_key) UNIQUE
#

View File

@@ -60,7 +60,7 @@ InviteRedeemer = Struct.new(:invite, :username, :name, :password, :user_custom_f
user.save!
if invite.via_email
if invite.emailed_status != Invite.emailed_status_types[:not_required]
user.email_tokens.create!(email: user.email)
user.activate
end

View File

@@ -44,6 +44,7 @@ end
# last_used :datetime
# created_at :datetime not null
# updated_at :datetime not null
# name :string
#
# Indexes
#