resque-alive 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'tilt' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("tilt", "tilt")
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "resque/plugins/alive"
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "resque"
4
+ require "resque/plugins/alive/config"
5
+ require "resque/plugins/alive/heartbeat"
6
+ require "resque/plugins/alive/server"
7
+ require "resque/plugins/alive/version"
8
+
9
+ module Resque
10
+ module Plugins
11
+ module Alive
12
+ module Redis
13
+ TTL_EXPIRED = -2
14
+ end
15
+
16
+ def self.procline(string)
17
+ $0 = "resque-alive-#{Resque::Plugins::Alive::VERSION}: #{string}"
18
+ logger.send(:debug, $0)
19
+ end
20
+
21
+ def self.start
22
+ append_resque_alive_heartbeat_queue
23
+ Resque.before_first_fork do
24
+ Resque::Plugins::Alive.tap do |resque_alive|
25
+ procline("starting")
26
+ logger.info(banner)
27
+ register_current_instance
28
+ store_alive_key
29
+
30
+ procline("registering heartbeat")
31
+ Resque.enqueue(
32
+ Heartbeat,
33
+ hostname
34
+ )
35
+
36
+ procline("initializing webserver")
37
+ @server_pid = fork do
38
+ procline("listening on port: #{config.port} at path: #{config.path}")
39
+ resque_alive::Server.run!
40
+ end
41
+
42
+ logger.info(successful_startup_text)
43
+
44
+ # TODO: worker_exit is, an as yet unreleased feature of
45
+ # resque. If the version of resque used by the host
46
+ # application doesn't yet support worker_exit, register an
47
+ # Kernel#at_exit hook to gracefully shutdown resque-alive.
48
+ unless Resque.respond_to?(:worker_exit)
49
+ at_exit do
50
+ Resque::Plugins::Alive.shutdown
51
+ end
52
+ end
53
+ end
54
+
55
+ # TODO: worker_exit is, an as yet unreleased feature of
56
+ # resque, but should be the preferred method of triggering a
57
+ # graceful shutdown of resque-alive.
58
+ #
59
+ # https://github.com/resque/resque/blob/master/HISTORY.md#unreleased
60
+ if Resque.respond_to?(:worker_exit)
61
+ Resque.worker_exit do
62
+ Resque::Plugins::Alive.shutdown
63
+ end
64
+ end
65
+ end
66
+
67
+ Resque.before_pause do
68
+ Resque::Plugins::Alive.unregister_current_instance
69
+ end
70
+ end
71
+
72
+ def self.shutdown
73
+ procline("shutting down webserver #{@server_pid}")
74
+ Process.kill('TERM', @server_pid) unless @server_pid.nil?
75
+ Process.wait(@server_pid) unless @server_pid.nil?
76
+
77
+ procline("unregistering resque_alive")
78
+ Resque::Plugins::Alive.unregister_current_instance
79
+
80
+ procline("shutting down...")
81
+ end
82
+
83
+ QUEUE_ENV_VARS = %w(QUEUE QUEUES)
84
+ def self.append_resque_alive_heartbeat_queue
85
+ QUEUE_ENV_VARS.each do |env_var|
86
+ if ENV[env_var]
87
+ ENV[env_var] = [ENV[env_var], current_queue].join(",")
88
+ end
89
+ end
90
+ end
91
+
92
+ def self.config
93
+ @config ||= Config.instance
94
+ end
95
+
96
+ def self.setup
97
+ yield(config)
98
+ end
99
+
100
+ def self.redis
101
+ Resque.redis { |r| r }
102
+ end
103
+
104
+ def self.current_liveness_key
105
+ "#{config.liveness_key}::#{hostname}"
106
+ end
107
+
108
+ def self.hostname
109
+ config.hostname
110
+ end
111
+
112
+ def self.store_alive_key
113
+ redis.set(
114
+ current_liveness_key,
115
+ Time.now.to_i,
116
+ ex: config.time_to_live.to_i
117
+ )
118
+ end
119
+
120
+ def self.alive?
121
+ redis.ttl(current_liveness_key) != Redis::TTL_EXPIRED
122
+ end
123
+
124
+ def self.registered_instances
125
+ redis.keys("#{config.registered_instance_key}::*")
126
+ end
127
+
128
+ def self.register_current_instance
129
+ register_instance(current_instance_register_key)
130
+ end
131
+
132
+ def self.current_instance_register_key
133
+ "#{config.registered_instance_key}::#{hostname}"
134
+ end
135
+
136
+
137
+ def self.register_instance(instance_name)
138
+ redis.set(
139
+ instance_name,
140
+ Time.now.to_i,
141
+ ex: config.registration_ttl.to_i
142
+ )
143
+ end
144
+
145
+ def self.unregister_current_instance
146
+ # Delete any pending jobs for this instance
147
+ logger.info(shutdown_info)
148
+ purge_pending_jobs
149
+ redis.del(current_instance_register_key)
150
+ end
151
+
152
+ def self.purge_pending_jobs
153
+ logger.info("[Resque::Plugins::Alive] Begin purging pending jobs for queue #{current_queue}")
154
+ pending_heartbeat_jobs_count = Resque::Job.destroy(current_queue, Heartbeat)
155
+ logger.info("[Resque::Plugins::Alive] Purged #{pending_heartbeat_jobs_count} pending for #{hostname}")
156
+ logger.info("[Resque::Plugins::Alive] Removing queue #{current_queue}")
157
+ Resque.remove_queue(current_queue)
158
+ logger.info("[Resque::Plugins::Alive] Finished purging pending jobs for queue #{current_queue}")
159
+ end
160
+
161
+ def self.logger
162
+ Resque.logger
163
+ end
164
+
165
+ def self.banner
166
+ <<~BANNER
167
+ =================== Resque::Plugins::Alive =================
168
+ Hostname: #{hostname}
169
+ Liveness key: #{current_liveness_key}
170
+ Port: #{config.port}
171
+ Time to live: #{config.time_to_live}s
172
+ Current instance register key: #{current_instance_register_key}
173
+ Worker running on queue: #{@queue}
174
+ starting ...
175
+ BANNER
176
+ end
177
+
178
+ def self.shutdown_info
179
+ <<~BANNER
180
+ =================== Shutting down Resque::Plugins::Alive =================
181
+ Hostname: #{hostname}
182
+ Liveness key: #{current_liveness_key}
183
+ Current instance register key: #{current_instance_register_key}
184
+ BANNER
185
+ end
186
+
187
+ def self.current_queue
188
+ Heartbeat.current_queue
189
+ end
190
+
191
+ def self.successful_startup_text
192
+ <<~BANNER
193
+ Registered instances:
194
+ - #{registered_instances.join("\n\s\s- ")}
195
+ =================== Resque::Plugins::Alive Ready! =================
196
+ BANNER
197
+ end
198
+
199
+ def self.enabled?
200
+ config.enabled
201
+ end
202
+ end
203
+ end
204
+ end
205
+
206
+ Resque::Plugins::Alive.start if Resque::Plugins::Alive.enabled?
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ module Resque
6
+ module Plugins
7
+ module Alive
8
+ class Config
9
+ include Singleton
10
+
11
+ def initialize
12
+ set_defaults
13
+ end
14
+
15
+ def set_defaults
16
+ self.port = ENV["RESQUE_ALIVE_PORT"] || 7433
17
+ self.path = ENV["RESQUE_ALIVE_PATH"] || "/"
18
+ self.liveness_key = "RESQUE::LIVENESS_PROBE_TIMESTAMP"
19
+ self.time_to_live = 10 * 60
20
+ self.callback = proc {}
21
+ self.registered_instance_key = "RESQUE_REGISTERED_INSTANCE"
22
+ self.queue_prefix = :resque_alive
23
+ self.server = ENV["RESQUE_ALIVE_SERVER"] || "webrick"
24
+ self.hostname = ENV["HOSTNAME"] || "HOSTNAME_NOT_SET"
25
+ self.enabled = !ENV["RESQUE_ALIVE_DISABLED"]
26
+ end
27
+
28
+ def enabled?
29
+ enabled
30
+ end
31
+
32
+ def registration_ttl
33
+ @registration_ttl || time_to_live + 60
34
+ end
35
+
36
+ attr_accessor(
37
+ :callback,
38
+ :enabled,
39
+ :hostname,
40
+ :liveness_key,
41
+ :path,
42
+ :port,
43
+ :queue_prefix,
44
+ :registered_instance_key,
45
+ :server,
46
+ :time_to_live,
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "resque-scheduler"
4
+
5
+ module Resque
6
+ module Plugins
7
+ module Alive
8
+ class Heartbeat
9
+ # TODO: For the case where multiple workers exist on the same
10
+ # host, maybe this needs to know the PID of the worker process
11
+ # and namespace the queue to that pid?
12
+ def self.perform(_hostname = config.hostname)
13
+ ping
14
+ schedule_next_heartbeat
15
+ end
16
+
17
+ def self.ping
18
+ Alive.store_alive_key
19
+ Alive.register_current_instance
20
+
21
+ begin
22
+ config.callback.call
23
+ rescue StandardError
24
+ nil
25
+ end
26
+ end
27
+
28
+ def self.schedule_next_heartbeat
29
+ Resque.enqueue_in_with_queue(
30
+ current_queue,
31
+ inside_ttl_window,
32
+ self.name,
33
+ current_hostname
34
+ )
35
+ end
36
+
37
+ def self.inside_ttl_window
38
+ config.time_to_live / 2
39
+ end
40
+
41
+ def self.current_hostname
42
+ config.hostname
43
+ end
44
+
45
+ def self.config
46
+ Config.instance
47
+ end
48
+
49
+ def self.current_queue
50
+ "#{config.queue_prefix}-#{current_hostname}"
51
+ end
52
+
53
+ @queue = current_queue
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack"
4
+
5
+ module Resque
6
+ module Plugins
7
+ module Alive
8
+ class Server
9
+ class << self
10
+ def run!
11
+ handler = Rack::Handler.get(server)
12
+
13
+ Signal.trap("TERM") { handler.shutdown }
14
+
15
+ handler.run(self, Port: port, Host: '0.0.0.0')
16
+ end
17
+
18
+ def port
19
+ config.port
20
+ end
21
+
22
+ def path
23
+ config.path
24
+ end
25
+
26
+ def server
27
+ config.server
28
+ end
29
+
30
+ def config
31
+ Alive.config
32
+ end
33
+
34
+ def call(env)
35
+ if Rack::Request.new(env).path != path
36
+ [404, {}, ["Received unknown path"]]
37
+ elsif Alive.alive?
38
+ [200, {}, ["Alive key is present"]]
39
+ else
40
+ response = "Alive key is absent"
41
+ Alive.logger.error(response)
42
+ [404, {}, [response]]
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resque
4
+ module Plugins
5
+ module Alive
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,52 @@
1
+ require_relative "lib/resque/plugins/alive/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "resque-alive"
5
+ spec.version = Resque::Plugins::Alive::VERSION
6
+ spec.authors = ["Aaron Kuehler"]
7
+ spec.email = ["aaron.kuehler@gmail.com"]
8
+
9
+ spec.summary = %q{Adds a Kubernetes Liveness probe to Resque}
10
+ spec.description = <<~EOD
11
+
12
+ resque-alive adds a Kubernetes Liveness probe to a Resque instance.
13
+
14
+ How?
15
+
16
+ resque-alive provides a small rack application which
17
+ exposes HTTP endpoint to return the "Aliveness" of the Resque
18
+ instance. Aliveness is determined by the presence of an
19
+ auto-expiring key. resque-alive schedules a "heartbeat"
20
+ job to periodically refresh the expiring key - in the event the
21
+ Resque instance can"t process the job, the key expires and the
22
+ instance is marked as unhealthy.
23
+ EOD
24
+
25
+ spec.homepage = "https://github.com/indiebrain/resque-alive"
26
+ spec.license = "GPL-3.0"
27
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
28
+
29
+ spec.metadata["homepage_uri"] = spec.homepage
30
+ spec.metadata["source_code_uri"] = "https://github.com/indiebrain/resque-alive"
31
+ spec.metadata["changelog_uri"] = "https://github.com/indiebrain/resque-alive/blob/master/changelog.txt"
32
+
33
+ # Specify which files should be added to the gem when it is released.
34
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
35
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
36
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
37
+ end
38
+ spec.bindir = "exe"
39
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
40
+ spec.require_paths = ["lib"]
41
+
42
+ spec.add_development_dependency "bundler", ">= 1.16"
43
+ spec.add_development_dependency "byebug"
44
+ spec.add_development_dependency "mock_redis"
45
+ spec.add_development_dependency "rack-test"
46
+ spec.add_development_dependency "rake", "~> 12.0"
47
+ spec.add_development_dependency "resque_spec", "~> 0.18.1"
48
+ spec.add_development_dependency "rspec", "~> 3.0"
49
+
50
+ spec.add_dependency "resque"
51
+ spec.add_dependency "resque-scheduler"
52
+ end