creeper 1.0.9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. data/.gitignore +1 -0
  2. data/.rvmrc +48 -0
  3. data/Gemfile +17 -1
  4. data/Guardfile +32 -0
  5. data/Rakefile +9 -1
  6. data/bin/creeper +10 -58
  7. data/bin/creeperctl +74 -0
  8. data/config.ru +18 -0
  9. data/creeper.gemspec +19 -9
  10. data/lib/creeper.rb +108 -413
  11. data/lib/creeper/beanstalk_connection.rb +35 -0
  12. data/lib/creeper/cli.rb +225 -0
  13. data/lib/creeper/client.rb +93 -0
  14. data/lib/creeper/core_ext.rb +54 -0
  15. data/lib/creeper/exception_handler.rb +30 -0
  16. data/lib/creeper/extensions/action_mailer.rb +33 -0
  17. data/lib/creeper/extensions/active_record.rb +30 -0
  18. data/lib/creeper/extensions/generic_proxy.rb +26 -0
  19. data/lib/creeper/fetch.rb +94 -0
  20. data/lib/creeper/legacy.rb +46 -0
  21. data/lib/creeper/logging.rb +46 -0
  22. data/lib/creeper/manager.rb +164 -0
  23. data/lib/creeper/middleware/chain.rb +100 -0
  24. data/lib/creeper/middleware/server/active_record.rb +13 -0
  25. data/lib/creeper/middleware/server/logging.rb +31 -0
  26. data/lib/creeper/middleware/server/retry_jobs.rb +79 -0
  27. data/lib/creeper/middleware/server/timeout.rb +21 -0
  28. data/lib/creeper/paginator.rb +31 -0
  29. data/lib/creeper/processor.rb +116 -0
  30. data/lib/creeper/rails.rb +21 -0
  31. data/lib/creeper/redis_connection.rb +28 -0
  32. data/lib/creeper/testing.rb +44 -0
  33. data/lib/creeper/util.rb +45 -0
  34. data/lib/creeper/version.rb +1 -1
  35. data/lib/creeper/web.rb +248 -0
  36. data/lib/creeper/worker.rb +62 -313
  37. data/spec/dummy/.gitignore +15 -0
  38. data/spec/dummy/Gemfile +51 -0
  39. data/spec/dummy/README.rdoc +261 -0
  40. data/spec/dummy/Rakefile +7 -0
  41. data/spec/dummy/app/assets/images/rails.png +0 -0
  42. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  43. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  44. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  45. data/spec/dummy/app/controllers/work_controller.rb +71 -0
  46. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  47. data/spec/dummy/app/mailers/.gitkeep +0 -0
  48. data/spec/dummy/app/mailers/user_mailer.rb +9 -0
  49. data/spec/dummy/app/models/.gitkeep +0 -0
  50. data/spec/dummy/app/models/post.rb +8 -0
  51. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  52. data/spec/dummy/app/views/user_mailer/greetings.html.erb +3 -0
  53. data/spec/dummy/app/views/work/index.html.erb +1 -0
  54. data/spec/dummy/app/workers/fast_worker.rb +10 -0
  55. data/spec/dummy/app/workers/hard_worker.rb +11 -0
  56. data/spec/dummy/app/workers/lazy_worker.rb +12 -0
  57. data/spec/dummy/app/workers/suicidal_worker.rb +33 -0
  58. data/spec/dummy/config.ru +4 -0
  59. data/spec/dummy/config/application.rb +68 -0
  60. data/spec/dummy/config/boot.rb +6 -0
  61. data/spec/dummy/config/creeper.yml +9 -0
  62. data/spec/dummy/config/database.yml +25 -0
  63. data/spec/dummy/config/environment.rb +5 -0
  64. data/spec/dummy/config/environments/development.rb +37 -0
  65. data/spec/dummy/config/environments/production.rb +67 -0
  66. data/spec/dummy/config/environments/test.rb +37 -0
  67. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  68. data/spec/dummy/config/initializers/creeper.rb +8 -0
  69. data/spec/dummy/config/initializers/inflections.rb +15 -0
  70. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  71. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  72. data/spec/dummy/config/initializers/session_store.rb +8 -0
  73. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  74. data/spec/dummy/config/locales/en.yml +5 -0
  75. data/spec/dummy/config/routes.rb +13 -0
  76. data/spec/dummy/db/migrate/20120123214055_create_posts.rb +10 -0
  77. data/spec/dummy/db/schema.rb +23 -0
  78. data/spec/dummy/db/seeds.rb +7 -0
  79. data/spec/dummy/lib/assets/.gitkeep +0 -0
  80. data/spec/dummy/lib/tasks/.gitkeep +0 -0
  81. data/spec/dummy/log/.gitkeep +0 -0
  82. data/spec/dummy/public/404.html +26 -0
  83. data/spec/dummy/public/422.html +26 -0
  84. data/spec/dummy/public/500.html +25 -0
  85. data/spec/dummy/public/favicon.ico +0 -0
  86. data/spec/dummy/public/index.html +241 -0
  87. data/spec/dummy/public/robots.txt +5 -0
  88. data/spec/dummy/script/rails +6 -0
  89. data/spec/dummy/vendor/assets/javascripts/.gitkeep +0 -0
  90. data/spec/dummy/vendor/assets/stylesheets/.gitkeep +0 -0
  91. data/spec/dummy/vendor/plugins/.gitkeep +0 -0
  92. data/spec/lib/creeper/cli_spec.rb +208 -0
  93. data/spec/lib/creeper/client_spec.rb +110 -0
  94. data/spec/lib/creeper/exception_handler_spec.rb +110 -0
  95. data/spec/lib/creeper/processor_spec.rb +92 -0
  96. data/spec/lib/creeper/testing_spec.rb +105 -0
  97. data/spec/lib/creeper_spec.rb +54 -120
  98. data/spec/spec_helper.rb +81 -7
  99. data/spec/support/config.yml +9 -0
  100. data/spec/support/fake_env.rb +0 -0
  101. data/spec/support/workers/base_worker.rb +11 -0
  102. data/spec/support/workers/my_worker.rb +4 -0
  103. data/spec/support/workers/queued_worker.rb +5 -0
  104. data/spec/support/workers/real_worker.rb +10 -0
  105. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  106. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  107. data/web/assets/javascripts/application.js +49 -0
  108. data/web/assets/javascripts/vendor/bootstrap.js +12 -0
  109. data/web/assets/javascripts/vendor/bootstrap/bootstrap-alert.js +91 -0
  110. data/web/assets/javascripts/vendor/bootstrap/bootstrap-button.js +98 -0
  111. data/web/assets/javascripts/vendor/bootstrap/bootstrap-carousel.js +154 -0
  112. data/web/assets/javascripts/vendor/bootstrap/bootstrap-collapse.js +136 -0
  113. data/web/assets/javascripts/vendor/bootstrap/bootstrap-dropdown.js +92 -0
  114. data/web/assets/javascripts/vendor/bootstrap/bootstrap-modal.js +210 -0
  115. data/web/assets/javascripts/vendor/bootstrap/bootstrap-popover.js +95 -0
  116. data/web/assets/javascripts/vendor/bootstrap/bootstrap-scrollspy.js +125 -0
  117. data/web/assets/javascripts/vendor/bootstrap/bootstrap-tab.js +130 -0
  118. data/web/assets/javascripts/vendor/bootstrap/bootstrap-tooltip.js +270 -0
  119. data/web/assets/javascripts/vendor/bootstrap/bootstrap-transition.js +51 -0
  120. data/web/assets/javascripts/vendor/bootstrap/bootstrap-typeahead.js +271 -0
  121. data/web/assets/javascripts/vendor/jquery.js +9266 -0
  122. data/web/assets/javascripts/vendor/jquery.timeago.js +148 -0
  123. data/web/assets/stylesheets/application.css +6 -0
  124. data/web/assets/stylesheets/layout.css +26 -0
  125. data/web/assets/stylesheets/vendor/bootstrap-responsive.css +567 -0
  126. data/web/assets/stylesheets/vendor/bootstrap.css +3365 -0
  127. data/web/views/_paging.slim +15 -0
  128. data/web/views/_summary.slim +9 -0
  129. data/web/views/_workers.slim +14 -0
  130. data/web/views/index.slim +10 -0
  131. data/web/views/layout.slim +37 -0
  132. data/web/views/poll.slim +3 -0
  133. data/web/views/queue.slim +15 -0
  134. data/web/views/queues.slim +19 -0
  135. data/web/views/retries.slim +31 -0
  136. data/web/views/retry.slim +52 -0
  137. data/web/views/scheduled.slim +27 -0
  138. metadata +341 -23
  139. data/lib/creeper/celluloid_ext.rb +0 -42
  140. data/lib/creeper/creep.rb +0 -25
  141. data/lib/creeper/err_logger.rb +0 -37
  142. data/lib/creeper/launcher.rb +0 -44
  143. data/lib/creeper/out_logger.rb +0 -39
  144. data/spec/lib/creeper/session_spec.rb +0 -15
  145. data/spec/lib/creeper/worker_spec.rb +0 -21
@@ -0,0 +1,13 @@
1
+ module Creeper
2
+ module Middleware
3
+ module Server
4
+ class ActiveRecord
5
+ def call(*args)
6
+ yield
7
+ ensure
8
+ ::ActiveRecord::Base.clear_active_connections! if defined?(::ActiveRecord)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ module Creeper
2
+ module Middleware
3
+ module Server
4
+ class Logging
5
+
6
+ def call(worker, msg, queue, job, conn)
7
+ Creeper::Logging.with_context("#{worker.class.to_s} JOB-#{job.id rescue nil} MSG-#{worker.object_id.to_s(36)}") do
8
+ begin
9
+ start = Time.now
10
+ logger.info { "start" }
11
+ yield
12
+ logger.info { "done: #{elapsed(start)} sec" }
13
+ rescue
14
+ logger.info { "fail: #{elapsed(start)} sec" }
15
+ raise
16
+ end
17
+ end
18
+ end
19
+
20
+ def elapsed(start)
21
+ (Time.now - start).to_f.round(3)
22
+ end
23
+
24
+ def logger
25
+ Creeper.logger
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
@@ -0,0 +1,79 @@
1
+ module Creeper
2
+ module Middleware
3
+ module Server
4
+ ##
5
+ # Automatically retry jobs that fail in Creeper.
6
+ # Creeper's retry support assumes a typical development lifecycle:
7
+ # 0. push some code changes with a bug in it
8
+ # 1. bug causes message processing to fail, creeper's middleware captures
9
+ # the message and pushes it onto a retry queue
10
+ # 2. creeper retries messages in the retry queue multiple times with
11
+ # an exponential delay, the message continues to fail
12
+ # 3. after a few days, a developer deploys a fix. the message is
13
+ # reprocessed successfully.
14
+ # 4. if 3 never happens, creeper will eventually give up and throw the
15
+ # message away.
16
+ #
17
+ # A message looks like:
18
+ #
19
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'] }
20
+ #
21
+ # We'll add a bit more data to the message to support retries:
22
+ #
23
+ # * 'queue' - the queue to use
24
+ # * 'retry_count' - number of times we've retried so far.
25
+ # * 'error_message' - the message from the exception
26
+ # * 'error_class' - the exception class
27
+ # * 'failed_at' - the first time it failed
28
+ # * 'retried_at' - the last time it was retried
29
+ #
30
+ # We don't store the backtrace as that can add a lot of overhead
31
+ # to the message and everyone is using Airbrake, right?
32
+ class RetryJobs
33
+ include Creeper::Util
34
+
35
+ # delayed_job uses the same basic formula
36
+ MAX_COUNT = 25
37
+ DELAY = proc { |count| (count ** 4) + 15 }
38
+
39
+ def call(worker, msg, queue, job, conn)
40
+ yield
41
+ rescue => e
42
+ raise unless msg['retry']
43
+
44
+ msg['queue'] = queue
45
+ msg['error_message'] = e.message
46
+ msg['error_class'] = e.class.name
47
+ count = if msg['retry_count']
48
+ msg['retried_at'] = Time.now.utc
49
+ msg['retry_count'] += 1
50
+ else
51
+ msg['failed_at'] = Time.now.utc
52
+ msg['retry_count'] = 0
53
+ end
54
+
55
+ if msg['backtrace'] == true
56
+ msg['error_backtrace'] = e.backtrace
57
+ elsif msg['backtrace'].to_i != 0
58
+ msg['error_backtrace'] = e.backtrace[0..msg['backtrace'].to_i]
59
+ end
60
+
61
+ if count <= MAX_COUNT
62
+ delay = DELAY.call(count)
63
+ logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
64
+ retry_at = Time.now.to_f + delay
65
+ payload = Creeper.dump_json(msg)
66
+ Creeper.redis do |conn|
67
+ conn.zadd('retry', retry_at.to_s, payload)
68
+ end
69
+ else
70
+ # Goodbye dear message, you (re)tried your best I'm sure.
71
+ logger.debug { "Dropping message after hitting the retry maximum: #{msg}" }
72
+ end
73
+ raise
74
+ end
75
+
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,21 @@
1
+ require 'timeout'
2
+
3
+ module Creeper
4
+ module Middleware
5
+ module Server
6
+ class Timeout
7
+
8
+ def call(worker, msg, queue, job, conn)
9
+ if msg['timeout'] && msg['timeout'].to_i != 0
10
+ ::Timeout.timeout(msg['timeout'].to_i) do
11
+ yield
12
+ end
13
+ else
14
+ yield
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ module Creeper
2
+ module Paginator
3
+ def page(key, pageidx=1, page_size=25)
4
+ current_page = pageidx.to_i < 1 ? 1 : pageidx.to_i
5
+ pageidx = current_page - 1
6
+ total_size = 0
7
+ items = []
8
+ starting = pageidx * page_size
9
+ ending = starting + page_size - 1
10
+
11
+ Creeper.redis do |conn|
12
+ type = conn.type(key)
13
+
14
+ case type
15
+ when 'zset'
16
+ total_size = conn.zcard(key)
17
+ items = conn.zrange(key, starting, ending, :with_scores => true)
18
+ when 'list'
19
+ total_size = conn.llen(key)
20
+ items = conn.lrange(key, starting, ending)
21
+ when 'none'
22
+ return [1, 0, []]
23
+ else
24
+ raise "can't page a #{type}"
25
+ end
26
+ end
27
+
28
+ [current_page, total_size, items]
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,116 @@
1
+ require 'celluloid'
2
+ require 'creeper/util'
3
+
4
+ require 'creeper/middleware/server/active_record'
5
+ require 'creeper/middleware/server/retry_jobs'
6
+ require 'creeper/middleware/server/logging'
7
+ require 'creeper/middleware/server/timeout'
8
+
9
+ module Creeper
10
+ ##
11
+ # The Processor receives a message from the Manager and actually
12
+ # processes it. It instantiates the worker, runs the middleware
13
+ # chain and then calls Creeper::Worker#perform.
14
+ class Processor
15
+ include Util
16
+ include Celluloid
17
+
18
+ exclusive :process if ENV['CREEPER_EXCLUSIVE']
19
+
20
+ def self.default_middleware
21
+ Middleware::Chain.new do |m|
22
+ m.add Middleware::Server::Logging
23
+ m.add Middleware::Server::RetryJobs
24
+ m.add Middleware::Server::ActiveRecord
25
+ m.add Middleware::Server::Timeout
26
+ end
27
+ end
28
+
29
+ def initialize(boss)
30
+ @boss = boss
31
+ end
32
+
33
+ def process(msgstr, queue, job, conn)
34
+ msg = Creeper.load_json(msgstr) rescue msgstr
35
+ klass = Creeper.job_descriptions[queue]
36
+ klass ||= constantize(msg['class'])
37
+ worker = klass.new
38
+
39
+ stats(worker, msg, queue) do
40
+ Creeper.server_middleware.invoke(worker, msg, queue, job, conn) do
41
+ args = msg['args']
42
+ args ||= [msg]
43
+ worker.perform(*cloned(args))
44
+ end
45
+ end
46
+ job.delete rescue nil
47
+ @boss.processor_done!(current_actor)
48
+ rescue => ex
49
+ job.bury rescue nil
50
+ handle_exception(ex, msg || { :message => msgstr })
51
+ raise
52
+ ensure
53
+ conn.close rescue nil
54
+ end
55
+
56
+ # See http://github.com/tarcieri/celluloid/issues/22
57
+ def inspect
58
+ "#<Processor #{to_s}>"
59
+ end
60
+
61
+ def to_s
62
+ @str ||= "#{hostname}:#{process_id}-#{Thread.current.object_id}:default"
63
+ end
64
+
65
+ private
66
+
67
+ def stats(worker, msg, queue)
68
+ redis do |conn|
69
+ conn.multi do
70
+ conn.sadd('workers', self)
71
+ conn.setex("worker:#{self}:started", EXPIRY, Time.now.to_s)
72
+ hash = {:queue => queue, :payload => msg, :run_at => Time.now.strftime("%Y/%m/%d %H:%M:%S %Z")}
73
+ conn.setex("worker:#{self}", EXPIRY, Creeper.dump_json(hash))
74
+ end
75
+ end
76
+
77
+ dying = false
78
+ begin
79
+ yield
80
+ rescue Exception
81
+ dying = true
82
+ redis do |conn|
83
+ conn.multi do
84
+ conn.incrby("stat:failed", 1)
85
+ end
86
+ end
87
+ raise
88
+ ensure
89
+ redis do |conn|
90
+ conn.multi do
91
+ conn.srem("workers", self)
92
+ conn.del("worker:#{self}")
93
+ conn.del("worker:#{self}:started")
94
+ conn.incrby("stat:processed", 1)
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ # Singleton classes are not clonable.
101
+ SINGLETON_CLASSES = [ NilClass, TrueClass, FalseClass, Symbol, Fixnum, Float ].freeze
102
+
103
+ # Clone the arguments passed to the worker so that if
104
+ # the message fails, what is pushed back onto Redis hasn't
105
+ # been mutated by the worker.
106
+ def cloned(ary)
107
+ ary.map do |val|
108
+ SINGLETON_CLASSES.include?(val.class) ? val : val.clone
109
+ end
110
+ end
111
+
112
+ def hostname
113
+ @h ||= `hostname`.strip
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,21 @@
1
+ module Creeper
2
+ def self.hook_rails!
3
+ return unless Creeper.options[:enable_rails_extensions]
4
+ if defined?(ActiveRecord)
5
+ ActiveRecord::Base.extend(Creeper::Extensions::ActiveRecord)
6
+ ActiveRecord::Base.send(:include, Creeper::Extensions::ActiveRecord)
7
+ end
8
+
9
+ if defined?(ActionMailer)
10
+ ActionMailer::Base.extend(Creeper::Extensions::ActionMailer)
11
+ end
12
+ end
13
+
14
+ class Rails < ::Rails::Engine
15
+ config.autoload_paths << File.expand_path("#{config.root}/app/workers") if File.exist?("#{config.root}/app/workers")
16
+
17
+ initializer 'creeper' do
18
+ Creeper.hook_rails!
19
+ end
20
+ end if defined?(::Rails)
21
+ end
@@ -0,0 +1,28 @@
1
+ require 'connection_pool'
2
+ require 'redis'
3
+ require 'redis/namespace'
4
+
5
+ module Creeper
6
+ class RedisConnection
7
+ def self.create(options={})
8
+ url = options[:url] || ENV['REDISTOGO_URL'] || 'redis://localhost:6379/0'
9
+ driver = options[:driver] || 'ruby'
10
+ # need a connection for Fetcher and Retry
11
+ size = options[:size] || (Creeper.server? ? (Creeper.options[:concurrency] + 2) : 5)
12
+
13
+ ConnectionPool.new(:timeout => 1, :size => size) do
14
+ build_client(url, options[:namespace], driver)
15
+ end
16
+ end
17
+
18
+ def self.build_client(url, namespace, driver)
19
+ client = Redis.connect(:url => url, :driver => driver)
20
+ if namespace
21
+ Redis::Namespace.new(namespace, :redis => client)
22
+ else
23
+ client
24
+ end
25
+ end
26
+ private_class_method :build_client
27
+ end
28
+ end
@@ -0,0 +1,44 @@
1
+ module Creeper
2
+ module Worker
3
+
4
+ ##
5
+ # The Creeper testing infrastructure overrides perform_async
6
+ # so that it does not actually touch the network. Instead it
7
+ # stores the asynchronous jobs in a per-class array so that
8
+ # their presence/absence can be asserted by your tests.
9
+ #
10
+ # This is similar to ActionMailer's :test delivery_method and its
11
+ # ActionMailer::Base.deliveries array.
12
+ #
13
+ # Example:
14
+ #
15
+ # require 'creeper/testing'
16
+ #
17
+ # assert_equal 0, HardWorker.jobs.size
18
+ # HardWorker.perform_async(:something)
19
+ # assert_equal 1, HardWorker.jobs.size
20
+ # assert_equal :something, HardWorker.jobs[0]['args'][0]
21
+ #
22
+ # assert_equal 0, Creeper::Extensions::DelayedMailer.jobs.size
23
+ # MyMailer.delayed.send_welcome_email('foo@example.com')
24
+ # assert_equal 1, Creeper::Extensions::DelayedMailer.jobs.size
25
+ #
26
+ module ClassMethods
27
+ alias_method :client_push_old, :client_push
28
+ def client_push(opts)
29
+ jobs << opts
30
+ true
31
+ end
32
+
33
+ def jobs
34
+ @pushed ||= []
35
+ end
36
+
37
+ def drain
38
+ while job = jobs.shift do
39
+ new.perform(*job['args'])
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,45 @@
1
+ require 'creeper/exception_handler'
2
+
3
+ module Creeper
4
+ ##
5
+ # This module is part of Creeper core and not intended for extensions.
6
+ #
7
+ module Util
8
+ include ExceptionHandler
9
+
10
+ EXPIRY = 60 * 60
11
+
12
+ def constantize(camel_cased_word)
13
+ names = camel_cased_word.split('::')
14
+ names.shift if names.empty? || names.first.empty?
15
+
16
+ constant = Object
17
+ names.each do |name|
18
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
19
+ end
20
+ constant
21
+ end
22
+
23
+ def watchdog(last_words)
24
+ yield
25
+ rescue => ex
26
+ handle_exception(ex, { :context => last_words })
27
+ end
28
+
29
+ def logger
30
+ Creeper.logger
31
+ end
32
+
33
+ def beanstalk(&block)
34
+ Creeper.beanstalk(&block)
35
+ end
36
+
37
+ def redis(&block)
38
+ Creeper.redis(&block)
39
+ end
40
+
41
+ def process_id
42
+ Process.pid
43
+ end
44
+ end
45
+ end
@@ -1,3 +1,3 @@
1
1
  module Creeper
2
- VERSION = "1.0.9"
2
+ VERSION = "2.0.0"
3
3
  end