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.
@@ -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