creeper 1.0.9 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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