devise emails to background job with devise-async gem and async resque email testing

jamesperet 9 years ago
parent
commit
bf9725dae7

+ 1 - 0
Gemfile

@@ -44,6 +44,7 @@ gem 'simple_form'
44 44
 gem 'bootstrap_form'
45 45
 gem 'friendly_id', '~> 5.0.0'
46 46
 gem 'devise'
47
+gem "devise-async"
47 48
 gem "letter_opener", :group => :development
48 49
 gem 'redcarpet'
49 50
 gem 'summernote-rails'

+ 3 - 0
Gemfile.lock

@@ -71,6 +71,8 @@ GEM
71 71
       railties (>= 3.2.6, < 5)
72 72
       thread_safe (~> 0.1)
73 73
       warden (~> 1.2.3)
74
+    devise-async (0.9.0)
75
+      devise (~> 3.2)
74 76
     diff-lcs (1.2.5)
75 77
     email_spec (1.6.0)
76 78
       launchy (~> 2.1)
@@ -291,6 +293,7 @@ DEPENDENCIES
291 293
   cucumber-rails
292 294
   database_cleaner
293 295
   devise
296
+  devise-async
294 297
   email_spec
295 298
   factory_girl_rails (~> 4.0)
296 299
   figaro

+ 1 - 1
app/models/user.rb

@@ -1,7 +1,7 @@
1 1
 class User < ActiveRecord::Base
2 2
   # Include default devise modules. Others available are:
3 3
   # :confirmable, :lockable, :timeoutable and :omniauthable
4
-  devise :database_authenticatable, :registerable,
4
+  devise :database_authenticatable, :async, :registerable,
5 5
          :recoverable, :rememberable, :trackable, :validatable
6 6
          
7 7
   validates :password, presence: true, length: {minimum: 5, maximum: 120}, on: :create

+ 1 - 1
app/workers/subscribe_to_mailchimp.rb

@@ -6,7 +6,7 @@ class SubscribeToMailchimp
6 6
     # Get User
7 7
     subscription = Subscription.find_by_id(id)
8 8
 
9
-    return true if (Rails.env.test? && !testing)
9
+    return true if (Rails.env.test?)
10 10
     list_id = ENV['MAILCHIMP_LIST_ID']
11 11
     response = Rails.configuration.mailchimp.lists.subscribe({
12 12
       id: list_id,

+ 2 - 0
config/initializers/cucumber_external_resque.rb

@@ -0,0 +1,2 @@
1
+# require "#{Rails.root}/features/support/cucumber_external_resque_worker"
2
+# CucumberExternalResqueWorker.install_hooks_on_startup

+ 1 - 1
config/initializers/devise.rb

@@ -13,7 +13,7 @@ Devise.setup do |config|
13 13
   config.mailer_sender = ENV['SERVER_EMAIL']
14 14
 
15 15
   # Configure the class responsible to send e-mails.
16
-  # config.mailer = 'Devise::Mailer'
16
+  #config.mailer = 'Devise::Mailer'
17 17
 
18 18
   # ==> ORM configuration
19 19
   # Load and configure the ORM. Supports :active_record (default) and

+ 6 - 0
config/initializers/devise_async.rb

@@ -0,0 +1,6 @@
1
+# config/initializers/devise_async.rb
2
+Devise::Async.setup do |config|
3
+  config.enabled = true
4
+  config.backend = :resque
5
+  config.queue   = :send_reset_password
6
+end

+ 1 - 0
config/initializers/resque.rb

@@ -0,0 +1 @@
1
+Resque.inline = Rails.env.test?

+ 3 - 0
features/step_definitions/email_steps.rb

@@ -60,6 +60,9 @@ Then /^(?:I|they|"([^"]*?)") should receive (an|no|\d+) emails? with subject "([
60 60
   # @email = ActionMailer::Base.deliveries.last
61 61
   # @email.to.should include address
62 62
   # @email.subject.should subject
63
+  # CucumberExternalResqueWorker.reset_counter
64
+  # Resque.remove_queue(:send_contact_message_queue)
65
+  # CucumberExternalResqueWorker.process_all
63 66
   unread_emails_for(address).select { |m| m.subject =~ Regexp.new(Regexp.escape(subject)) }.size.should == parse_email_count(amount)
64 67
 end
65 68
 

+ 125 - 0
features/support/cucumber_external_resque_worker.rb

@@ -0,0 +1,125 @@
1
+# This is adapted from this gist: https://gist.github.com/532100 by Square
2
+# The main difference is that it doesn't require Bluth for WorkerBase
3
+# It also calls prune_dead_workers on start so it doesn't hang on every other run
4
+# It does not do anything special to avoid connecting to your main redis instance; you should be
5
+# doing that elsewhere
6
+
7
+class CucumberExternalResqueWorker
8
+  DEFAULT_STARTUP_TIMEOUT = 1.minute
9
+  COUNTER_KEY = "cucumber:counter"
10
+
11
+  class << self
12
+    attr_accessor :pid, :startup_timeout
13
+
14
+    def start
15
+      # Call from a Cucumber support file so it is run on startup
16
+      return unless Rails.env.test?
17
+      if self.pid = fork
18
+        start_parent
19
+        wait_for_worker_to_start
20
+      else
21
+        start_child
22
+      end
23
+    end
24
+
25
+    def install_hooks_on_startup
26
+      # Call from a Rails initializer
27
+      return unless Rails.env.test?
28
+      # Because otherwise crashed workers cause a fork and we pause the actual worker forever
29
+      Resque::Worker.all.each { |worker| worker.prune_dead_workers }
30
+      install_pause_on_start_hook
31
+      install_worker_base_counter_patch
32
+    end
33
+
34
+    def process_all
35
+      # Call from a Cucumber step
36
+      unpause
37
+      sleep 1 until done?
38
+      pause
39
+    end
40
+
41
+    def incr
42
+      Resque.redis.incr(COUNTER_KEY)
43
+    end
44
+
45
+    def decr
46
+      Resque.redis.decr(COUNTER_KEY)
47
+    end
48
+
49
+    def reset_counter
50
+      Resque.redis.set(COUNTER_KEY, 0)
51
+    end
52
+
53
+    private
54
+
55
+    def done?
56
+      Resque.redis.get(CucumberExternalResqueWorker::COUNTER_KEY).to_i.zero?
57
+    end
58
+
59
+    def pause(pid = self.pid)
60
+      return unless Rails.env.test?
61
+      Process.kill("USR2", pid)
62
+    end
63
+
64
+    def unpause
65
+      return unless Rails.env.test?
66
+      Process.kill("CONT", pid)
67
+    end
68
+
69
+    def start_parent
70
+      at_exit do
71
+        #reset_counter
72
+        Process.kill("KILL", pid) if pid
73
+      end
74
+    end
75
+
76
+    def start_child
77
+      # Array form of exec() is required here, otherwise the worker is not a direct child process of cucumber.
78
+      # If it's not the direct child process then the PID returned from fork() is wrong, which means we can't
79
+      # communicate with the worker.
80
+      exec('rake', 'resque:work', "QUEUE=*", "RAILS_ENV=test", "VVERBOSE=1")
81
+    end
82
+
83
+    def wait_for_worker_to_start
84
+      self.startup_timeout ||= DEFAULT_STARTUP_TIMEOUT
85
+      start = Time.now.to_i
86
+      while (Time.now.to_i - start) < startup_timeout
87
+        return if worker_started?
88
+        sleep 1
89
+      end
90
+
91
+      raise "Timeout while waiting for the worker to start. Waited #{startup_timeout} seconds."
92
+    end
93
+
94
+    def worker_started?
95
+      Resque.info[:workers].to_i > 0
96
+    end
97
+
98
+    def install_pause_on_start_hook
99
+      Resque.before_first_fork do
100
+        #reset_counter
101
+        pause(Process.pid)
102
+      end
103
+    end
104
+
105
+    def install_worker_base_counter_patch
106
+      Resque.class_eval do
107
+        class << self
108
+          def enqueue_with_counters(*args, &block)
109
+            CucumberExternalResqueWorker.incr
110
+            enqueue_without_counters(*args, &block)
111
+          end
112
+          alias_method_chain :enqueue, :counters
113
+        end
114
+      end
115
+      Resque::Job.class_eval do
116
+        def perform_with_counters(*args, &block)
117
+          perform_without_counters(*args, &block)
118
+        ensure
119
+          CucumberExternalResqueWorker.decr
120
+        end
121
+        alias_method_chain :perform, :counters
122
+      end
123
+    end
124
+  end
125
+end

+ 6 - 1
features/support/env.rb

@@ -6,6 +6,9 @@
6 6
 
7 7
 require 'cucumber/rails'
8 8
 
9
+# require "#{Rails.root}/features/support/cucumber_external_resque_worker"
10
+# CucumberExternalResqueWorker.start
11
+
9 12
 # Capybara defaults to CSS3 selectors rather than XPath.
10 13
 # If you'd prefer to use XPath, just uncomment this line and adjust any
11 14
 # selectors in your step definitions to use the XPath syntax.
@@ -31,7 +34,7 @@ ActionController::Base.allow_rescue = false
31 34
 # Remove/comment out the lines below if your app doesn't have a database.
32 35
 # For some databases (like MongoDB and CouchDB) you may need to use :truncation instead.
33 36
 begin
34
-  DatabaseCleaner.strategy = :transaction
37
+  DatabaseCleaner.strategy = :truncation
35 38
 rescue NameError
36 39
   raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it."
37 40
 end
@@ -66,3 +69,5 @@ require "#{Rails.root}/spec/spec_helper"
66 69
 ActionMailer::Base.delivery_method = :test
67 70
 ActionMailer::Base.perform_deliveries = true
68 71
 ActionMailer::Base.deliveries.clear
72
+
73
+