Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add every: Proc support + until: val + until: Proc support #25

Closed
wants to merge 2 commits into from
Closed
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
4 changes: 4 additions & 0 deletions lib/caffeinate/drip.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ def send_at(mailing = nil)
::Caffeinate::ScheduleEvaluator.call(self, mailing)
end

def past_until?(mailing)
::Caffeinate::UntilEvaluator.call(self, mailing)
end

# Checks if the drip is enabled
#
# This is kind of messy and could use some love.
Expand Down
2 changes: 1 addition & 1 deletion lib/caffeinate/dripper/drip_collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Caffeinate
module Dripper
# A collection of Drip objects for a `Caffeinate::Dripper`
class DripCollection
VALID_DRIP_OPTIONS = [:mailer_class, :step, :delay, :every, :start, :using, :mailer, :at, :on].freeze
VALID_DRIP_OPTIONS = [:mailer_class, :step, :delay, :every, :start, :using, :mailer, :at, :on, :until].freeze

include Enumerable

Expand Down
5 changes: 4 additions & 1 deletion lib/caffeinate/dripper/periodical.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ module ClassMethods
def periodical(action_name, every:, start: -> { ::Caffeinate.config.time_now }, **options, &block)
options[:start] = start
options[:every] = every
options[:until] ||= 5_000.years.from_now # can't call this out as a param above because `until` is a Ruby keyword

drip(action_name, options, &block)

after_send do |mailing, _message|
if mailing.drip.action == action_name
next_mailing = mailing.dup
next_mailing.send_at = mailing.drip.send_at(mailing)
next_mailing.save!
next_mailing.save! unless mailing.drip.past_until?(next_mailing)
end
end
end
Expand Down
38 changes: 33 additions & 5 deletions lib/caffeinate/schedule_evaluator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,15 @@ def initialize(drip, mailing)
# todo: test this decision tree.
def call
if periodical?
start = mailing.instance_exec(&options[:start])
start += options[:every] if mailing.caffeinate_campaign_subscription.caffeinate_mailings.count.positive?
date = start.from_now
date = if mailing.caffeinate_campaign_subscription.caffeinate_mailings.count.positive?
if options[:every].respond_to? :call
mailing.instance_exec(&options[:every])
else
options[:every]
end
else
mailing.instance_exec(&options[:start])
end.from_now
elsif options[:on]
date = OptionEvaluator.new(options[:on], self, mailing).call
else
Expand All @@ -52,19 +58,41 @@ def call

date
end

def respond_to_missing?(name, include_private = false)
@drip.respond_to?(name, include_private)
end

def method_missing(method, *args, &block)
@drip.send(method, *args, &block)
end

private

def periodical?
options[:every].present?
end
end

class UntilEvaluator
def self.call(drip, mailing)
new(drip, mailing).call
end

attr_reader :mailing
def initialize(drip, mailing)
@drip = drip
@mailing = mailing
end

def call
# Procs for `until:` should return truthy if drip should stop;
# falsey if drip should continue
if @drip.options[:until].respond_to? :call
!!mailing.instance_exec(&@drip.options[:until])
else
@mailing.send_at > @drip.options[:until]
end
end
end
end
3 changes: 3 additions & 0 deletions spec/caffeinate/deliver_async_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ class DeliverAsyncTest
describe '#perform' do
it 'delivers a pending mail' do
campaign.to_dripper.drip :hello, mailer_class: 'ArgumentMailer', delay: 0.hours
Timecop.travel(1.minute.from_now)

expect(subscription.caffeinate_mailings.count).to eq(1)
mailing = subscription.next_caffeinate_mailing
expect(mailing).to be_pending

DeliverAsyncTest.new.perform(mailing.id)
mailing.reload
expect(mailing).not_to be_pending
Expand Down
1 change: 1 addition & 0 deletions spec/caffeinate/dripper/cases/callbacks_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class CallbacksTestTwoDripper < ::Caffeinate::Dripper::Base
describe '.on_process' do
before do
dripper.drip :hello, mailer_class: 'ArgumentMailer', delay: 0.hours
Timecop.travel(1.minute.from_now)
company = create(:company)
campaign.subscribe(company)
dripper.cattr_accessor :on_performing
Expand Down
121 changes: 113 additions & 8 deletions spec/caffeinate/dripper/cases/periodical_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
require 'rails_helper'

describe ::Caffeinate::Dripper::Periodical do
let!(:campaign) { create(:caffeinate_campaign, slug: 'periodical_dripper') }

class PeriodicalMailer < ApplicationMailer
def welcome(_)
mail(to: '[email protected]', from: '[email protected]', subject: 'hello') do |format|
Expand All @@ -17,27 +15,35 @@ class PeriodicalDripper < ::Caffeinate::Dripper::Base
self.campaign = :periodical_dripper
default mailer_class: 'PeriodicalMailer'

periodical :welcome, every: 1.hour, start: proc { |_thing| 0.hours }
periodical :welcome, every: 1.hour, start: -> { 30.minutes }
end

describe '.periodical' do
describe '.periodical_static' do
let!(:campaign) { create(:caffeinate_campaign, slug: 'periodical_dripper') }
let!(:campaign_subscription) { create(:caffeinate_campaign_subscription, caffeinate_campaign: campaign) }

it 'has a single mailing' do
expect(campaign_subscription.caffeinate_mailings.count).to eq(1)
end

it "correctly sets the first mailing to the `start` offset" do
expect(campaign_subscription.caffeinate_mailings.first.send_at).to be_within(1.second).of(Time.current + 30.minutes)
end

context 'with performed dripper' do
let(:perform) { PeriodicalDripper.perform! }
let(:perform) { Timecop.travel(1.hour.from_now); PeriodicalDripper.perform! }

it 'changes deliveries count' do
expect do
perform
end.to change(ActionMailer::Base.deliveries, :size).by(1)
end

it 'creates another mailing' do
expect { perform }.to change(campaign_subscription.caffeinate_mailings, :count).by(1)
it "creates another mailing and sets the send_at to exactly the interval (`start` no longer matters)" do
perform

expect(campaign_subscription.caffeinate_mailings.count).to eq 2
expect(campaign_subscription.caffeinate_mailings.last.send_at).to be_within(1.second).of(1.hour.from_now)
end

it 'creates an unsent mailing' do
Expand All @@ -47,7 +53,106 @@ class PeriodicalDripper < ::Caffeinate::Dripper::Base

it 'sends a mail' do
perform
expect(campaign_subscription.caffeinate_mailings.unsent.first.send_at).to be_within(10.seconds).of(1.hour.from_now)
expect(campaign_subscription.caffeinate_mailings.unsent.first.send_at).to be_within(1.seconds).of(1.hour.from_now)
end
end
end

class DynamicPeriodicalDripper < ::Caffeinate::Dripper::Base
self.campaign = :dynamic_periodical_dripper
default mailer_class: 'PeriodicalMailer'

periodical :welcome, every: -> { 2.weeks + rand(0..60).minutes }, start: -> { 0.hours }, until: Time.parse("01/01/2020") + 3.weeks
end

describe '.periodical_dynamic' do
let!(:campaign) { create(:caffeinate_campaign, slug: 'dynamic_periodical_dripper') }
let!(:campaign_subscription) { create(:caffeinate_campaign_subscription, caffeinate_campaign: campaign) }

it 'has a single mailing' do
expect(campaign_subscription.caffeinate_mailings.count).to eq(1)
end

it "correctly sets the first mailing to the `start` offset" do
expect(campaign_subscription.caffeinate_mailings.first.send_at).to be_within(1.second).of(Time.current)
end

context 'with performed dripper' do
let(:perform) { Timecop.travel(1.second.from_now); DynamicPeriodicalDripper.perform! }

it 'changes deliveries count' do
expect do
perform
end.to change(ActionMailer::Base.deliveries, :size).by(1)
end

it "creates another mailing and sets the send_at to exactly the interval (`start` no longer matters)" do
perform

expect(campaign_subscription.caffeinate_mailings.count).to eq 2
expect(campaign_subscription.caffeinate_mailings.last.send_at).to be_within(60.minutes).of(2.weeks.from_now)
end

it 'creates an unsent mailing' do
perform
expect(campaign_subscription.caffeinate_mailings.unsent.count).to eq(1)
end

it 'sends a mail' do
perform
expect(campaign_subscription.caffeinate_mailings.unsent.first.send_at).to be_within(60.minutes).of(2.weeks.from_now)
end

it "stops after the first mailing because the until: is short" do

perform # first run

m = campaign_subscription.caffeinate_mailings.unsent.last

Timecop.travel(3.weeks.from_now)
DynamicPeriodicalDripper.perform! # second run

expect(m.reload.sent_at).to_not be_nil
expect(campaign_subscription.caffeinate_mailings.count).to eq 2 # no third
end
end
end

class ProcUntilDripper < ::Caffeinate::Dripper::Base
self.campaign = :proc_until
default mailer_class: 'PeriodicalMailer'

periodical :welcome, every: -> { 2.weeks + rand(0..60).minutes }, start: -> { 0.hours }, until: -> { send_at.month >= 2 }
end

describe '.periodical_dynamic' do
let!(:campaign) { create(:caffeinate_campaign, slug: 'proc_until') }
let!(:campaign_subscription) { create(:caffeinate_campaign_subscription, caffeinate_campaign: campaign) }

context 'with performed dripper' do
it "stops after the first mailing because the until: is short" do
expect(campaign_subscription.caffeinate_mailings.count).to eq 1
m = campaign_subscription.caffeinate_mailings.unsent.last

Timecop.travel(1.second.from_now)
ProcUntilDripper.perform! # first run

expect(m.reload.sent_at).to_not be_nil # first message was sent
expect(campaign_subscription.caffeinate_mailings.count).to eq 2 # second created
m = campaign_subscription.caffeinate_mailings.unsent.last

Timecop.travel(m.send_at + 1.second)
ProcUntilDripper.perform! # second run

expect(m.reload.sent_at).to_not be_nil # second message was sent
expect(campaign_subscription.caffeinate_mailings.count).to eq 3 # third created
m = campaign_subscription.caffeinate_mailings.unsent.last

Timecop.travel(m.send_at + 1.second)
ProcUntilDripper.perform! # third run — now would be into February

expect(m.reload.sent_at).to_not be_nil # third message was sent
expect(campaign_subscription.caffeinate_mailings.count).to eq 3 # no fourth message created
end
end
end
Expand Down
4 changes: 4 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@
end

config.shared_context_metadata_behavior = :apply_to_host_groups

config.around(:example) do |example|
Timecop.freeze("01/01/2020") { example.call }
end
end