resque-alive 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.org +10 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +91 -0
- data/LICENSE.txt +674 -0
- data/README.org +62 -0
- data/Rakefile +6 -0
- data/bin/bundle +114 -0
- data/bin/byebug +29 -0
- data/bin/console +14 -0
- data/bin/htmldiff +29 -0
- data/bin/ldiff +29 -0
- data/bin/rackup +29 -0
- data/bin/rake +29 -0
- data/bin/resque +29 -0
- data/bin/resque-scheduler +29 -0
- data/bin/resque-web +29 -0
- data/bin/rspec +29 -0
- data/bin/setup +8 -0
- data/bin/tilt +29 -0
- data/lib/resque/alive.rb +3 -0
- data/lib/resque/plugins/alive.rb +206 -0
- data/lib/resque/plugins/alive/config.rb +51 -0
- data/lib/resque/plugins/alive/heartbeat.rb +57 -0
- data/lib/resque/plugins/alive/server.rb +49 -0
- data/lib/resque/plugins/alive/version.rb +9 -0
- data/resque-alive.gemspec +52 -0
- metadata +214 -0
data/bin/setup
ADDED
data/bin/tilt
ADDED
@@ -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")
|
data/lib/resque/alive.rb
ADDED
@@ -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,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
|