Skip to content

Commit 4db911a

Browse files
committed
Merge pull request #360 from ansonK/email-send-refactor
Refactor the jobs that send email.
2 parents d4905e3 + 1330692 commit 4db911a

23 files changed

Lines changed: 545 additions & 361 deletions

Gemfile.lock

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,3 @@ DEPENDENCIES
334334
webmock
335335
whenever
336336
will_paginate
337-
338-
BUNDLED WITH
339-
1.10.4
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
module EmailAllPetitionSignatories
2+
extend ActiveSupport::Concern
3+
4+
#
5+
# Concern to add shared functionality to ActiveJob classes that are responsible
6+
# for enqueuing send email jobs
7+
#
8+
9+
included do
10+
queue_as :default
11+
12+
def self.run_later_tonight(petition)
13+
requested_at = Time.current
14+
15+
petition.set_email_requested_at_for(new.timestamp_name, to: requested_at)
16+
17+
set(wait_until: later_tonight).
18+
perform_later(petition, requested_at.getutc.iso8601(6))
19+
end
20+
21+
def self.later_tonight
22+
1.day.from_now.at_midnight + rand(240).minutes + rand(60).seconds
23+
end
24+
private_class_method :later_tonight
25+
26+
end
27+
28+
29+
30+
def perform(petition, requested_at_string)
31+
@petition = petition
32+
@requested_at = requested_at_string.in_time_zone
33+
do_work!
34+
end
35+
36+
def timestamp_name
37+
raise NotImplementedError.new "Including classes must implement #timestamp_name method"
38+
end
39+
40+
private
41+
42+
attr_reader :petition, :requested_at
43+
44+
def do_work!
45+
return if petition_has_been_updated?
46+
47+
logger.info("Starting #{self.class.name} for petition '#{petition.action}' with email requested at: #{petition_timestamp}")
48+
enqueue_send_email_jobs
49+
logger.info("Finished #{self.class.name} for petition '#{petition.action}'")
50+
51+
end
52+
53+
#
54+
# Batches the signataries to send emails to in groups of 1000
55+
# and enqueues a job to do the actual sending
56+
#
57+
def enqueue_send_email_jobs
58+
signatures_to_email.find_each do |signature|
59+
email_delivery_job_class.perform_later(
60+
signature: signature,
61+
timestamp_name: timestamp_name,
62+
petition: petition,
63+
requested_at_as_string: requested_at.getutc.iso8601(6)
64+
)
65+
end
66+
end
67+
68+
# admins can ask to send the email multiple times and each time they
69+
# ask we enqueues a new job to send out emails with a new timestamp
70+
# we want to execute only the latest job enqueued
71+
def petition_has_been_updated?
72+
(petition_timestamp - requested_at).abs > 1
73+
end
74+
75+
def petition_timestamp
76+
petition.get_email_requested_at_for(timestamp_name)
77+
end
78+
79+
def signatures_to_email
80+
petition.signatures_to_email_for(timestamp_name)
81+
end
82+
83+
# The job class that handles the actual email sending for this job type
84+
def email_delivery_job_class
85+
raise NotImplementedError.new "Including classes must implement #email_delivery_job_class method"
86+
end
87+
end
88+
89+
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
module EmailDelivery
2+
extend ActiveSupport::Concern
3+
4+
#
5+
# Send a single email to a recipient informing them about a petition that they have signed
6+
# Implemented as a custom job rather than using action mailers #deliver_later so we can do
7+
# extra checking before sending the email
8+
#
9+
10+
included do
11+
queue_as :default
12+
end
13+
14+
def perform(signature:, timestamp_name:, petition:,
15+
requested_at_as_string:, mailer: PetitionMailer.name, logger: nil)
16+
17+
@mailer = mailer.constantize
18+
@signature = signature
19+
@petition = petition
20+
@requested_at = requested_at_as_string.in_time_zone
21+
@timestamp_name = timestamp_name
22+
@logger = logger
23+
24+
if can_send_email?
25+
send_email
26+
record_email_sent
27+
end
28+
end
29+
30+
private
31+
32+
attr_reader :mailer, :signature, :timestamp_name, :petition, :requested_at
33+
34+
def can_send_email?
35+
petition_has_not_been_updated? && email_not_previously_sent?
36+
end
37+
38+
def send_email
39+
create_email.deliver_now
40+
41+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, Net::SMTPError, Timeout::Error => e
42+
# log that the send failed
43+
logger.info("#{e.class.name} while sending email for #{self.class.name} to: #{signature.email} for #{signature.petition.action}")
44+
45+
#
46+
# TODO: check the error and if it is a n AWS SES rate error:
47+
# 454 Throttling failure: Maximum sending rate exceeded
48+
# 454 Throttling failure: Daily message quota exceeded
49+
#
50+
# Then reschedule the send for a day later rather than keep failing
51+
#
52+
53+
# reraise to rerun the job later via the job retry mechanism
54+
raise e
55+
end
56+
57+
def create_email
58+
raise NotImplementedError.new "Including classes must implement #create_email method"
59+
end
60+
61+
def record_email_sent
62+
signature.set_email_sent_at_for timestamp_name, to: petition_timestamp
63+
end
64+
65+
def petition_timestamp
66+
petition.get_email_requested_at_for(timestamp_name)
67+
end
68+
69+
# We do not want to send the email if the petition has been updated
70+
# As email sending is enqueued straight after a petition has been updated
71+
def petition_has_not_been_updated?
72+
(petition_timestamp - requested_at).abs < 1
73+
end
74+
75+
#
76+
# Have we already sent an email for this petition version?
77+
# If we have then the timestamp for the signature will match the timestamp for the petition
78+
#
79+
def email_not_previously_sent?
80+
# check that the signature is still in the list of signatures
81+
petition.signatures_to_email_for(timestamp_name).where(id: signature.id).exists?
82+
end
83+
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class DeliverDebateOutcomeEmailJob < ActiveJob::Base
2+
include EmailDelivery
3+
4+
def create_email
5+
mailer.notify_signer_of_debate_outcome signature.petition, signature
6+
end
7+
8+
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class DeliverDebateScheduledEmailJob < ActiveJob::Base
2+
include EmailDelivery
3+
4+
def create_email
5+
mailer.notify_signer_of_debate_scheduled signature.petition, signature
6+
end
7+
8+
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class DeliverThresholdResponseEmailJob < ActiveJob::Base
2+
include EmailDelivery
3+
4+
def create_email
5+
mailer.notify_signer_of_threshold_response signature.petition, signature
6+
end
7+
8+
end
Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
1-
class EmailDebateOutcomesJob < EmailPetitionSignatories::Job
2-
def self.run_later_tonight(petition)
3-
petition.set_email_requested_at_for('debate_outcome', to: Time.current)
4-
super(petition, petition.get_email_requested_at_for('debate_outcome'))
5-
end
1+
class EmailDebateOutcomesJob < ActiveJob::Base
2+
include EmailAllPetitionSignatories
63

7-
def perform(petition, requested_at_string, mailer = PetitionMailer.name, logger = nil)
8-
@mailer = mailer.constantize
9-
worker(petition, requested_at_string, logger).do_work!
4+
def email_delivery_job_class
5+
DeliverDebateOutcomeEmailJob
106
end
117

128
def timestamp_name
139
'debate_outcome'
1410
end
15-
16-
def create_email(petition, signature)
17-
@mailer.notify_signer_of_debate_outcome(petition, signature)
18-
end
1911
end
Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
1-
class EmailDebateScheduledJob < EmailPetitionSignatories::Job
2-
def self.run_later_tonight(petition)
3-
petition.set_email_requested_at_for('debate_scheduled', to: Time.current)
4-
super(petition, petition.get_email_requested_at_for('debate_scheduled'))
5-
end
1+
class EmailDebateScheduledJob < ActiveJob::Base
2+
include EmailAllPetitionSignatories
63

7-
def perform(petition, requested_at_string, mailer = PetitionMailer.name, threshold_logger = nil)
8-
@mailer = mailer.constantize
9-
worker(petition, requested_at_string, threshold_logger).do_work!
4+
def email_delivery_job_class
5+
DeliverDebateScheduledEmailJob
106
end
117

128
def timestamp_name
139
'debate_scheduled'
1410
end
15-
16-
def create_email(petition, signature)
17-
@mailer.notify_signer_of_debate_scheduled(petition, signature)
18-
end
1911
end

app/jobs/email_petition_signatories.rb

Lines changed: 0 additions & 92 deletions
This file was deleted.
Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
1-
class EmailThresholdResponseJob < EmailPetitionSignatories::Job
2-
def self.run_later_tonight(petition, requested_at = Time.current)
3-
petition.set_email_requested_at_for('government_response', to: requested_at)
4-
super(petition, petition.get_email_requested_at_for('government_response'))
5-
end
1+
class EmailThresholdResponseJob < ActiveJob::Base
2+
include EmailAllPetitionSignatories
63

7-
def perform(petition, requested_at_string, mailer = PetitionMailer.name, logger = nil)
8-
@mailer = mailer.constantize
9-
worker(petition, requested_at_string, logger).do_work!
4+
def email_delivery_job_class
5+
DeliverThresholdResponseEmailJob
106
end
117

128
def timestamp_name
139
'government_response'
1410
end
15-
16-
def create_email(petition, signature)
17-
@mailer.notify_signer_of_threshold_response(petition, signature)
18-
end
1911
end

0 commit comments

Comments
 (0)