schked 1.4.0 → 1.5.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 +4 -4
- data/README.md +48 -0
- data/lib/schked/cli.rb +16 -1
- data/lib/schked/config.rb +8 -0
- data/lib/schked/liveness_probe.rb +190 -0
- data/lib/schked/railtie.rb +10 -0
- data/lib/schked/version.rb +1 -1
- data/lib/schked/worker.rb +21 -2
- data/lib/schked.rb +1 -0
- metadata +18 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d392bfee50671c6bc87ab00b4cbdb29969e55544d35a6133ce23b24f1d3171e
|
|
4
|
+
data.tar.gz: 196a09231bd9abb1c0a83f7d4d6d4ecb140cb3b305e1710aa775f21690ae3eb7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '02810b0f6a3717db6e4f5aa4cee9eee4644c74c7a583fbc1c0e6acae97d63193366c1ecbd0ee087106acbe542a49ace4e5cdfe209aba0b11c3d31938ef088638'
|
|
7
|
+
data.tar.gz: b55de68f347b8be57cc32cbf13ef582cd4ac33c8cddb881bfcead7bc449454953147c20bd311a03bfaa38c444884e6bf45daa7b54d2bb4bcbad5e2207d50194e
|
data/README.md
CHANGED
|
@@ -124,6 +124,54 @@ config/initializers/schked.rb
|
|
|
124
124
|
Schked.config.logger = Logger.new(Rails.root.join("log", "schked.log"))
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
### Liveness probe
|
|
128
|
+
|
|
129
|
+
Schked can expose a small HTTP endpoint for Kubernetes liveness probes. It is **disabled by default** to keep the existing behavior unchanged.
|
|
130
|
+
|
|
131
|
+
Configure it in Ruby:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
Schked.config.liveness_probe = {
|
|
135
|
+
enabled: true,
|
|
136
|
+
bind: "0.0.0.0",
|
|
137
|
+
port: 8080,
|
|
138
|
+
path: "/healthz",
|
|
139
|
+
heartbeat_interval: 5,
|
|
140
|
+
heartbeat_threshold: 15
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Or via CLI flags:
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
bundle exec schked start --liveness-probe --liveness-bind 0.0.0.0 --liveness-port 8080 --liveness-path /healthz
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
In Rails, set it through the application config:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
# config/application.rb or config/environments/*.rb
|
|
154
|
+
config.schked.liveness_probe = {
|
|
155
|
+
enabled: true,
|
|
156
|
+
bind: "0.0.0.0",
|
|
157
|
+
port: 8080,
|
|
158
|
+
path: "/healthz",
|
|
159
|
+
heartbeat_interval: 5,
|
|
160
|
+
heartbeat_threshold: 15
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The endpoint returns `200 OK` while the scheduler is responsive and `503 Service Unavailable` when the heartbeat is stale or during shutdown. The scheduler updates the heartbeat every `heartbeat_interval` seconds (default `5`); if it is not updated within `heartbeat_threshold` seconds (default `15`), the endpoint reports unhealthy. Use it in Kubernetes like this:
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
livenessProbe:
|
|
168
|
+
httpGet:
|
|
169
|
+
path: /healthz
|
|
170
|
+
port: 8080
|
|
171
|
+
initialDelaySeconds: 10
|
|
172
|
+
periodSeconds: 10
|
|
173
|
+
```
|
|
174
|
+
|
|
127
175
|
### Monitoring
|
|
128
176
|
|
|
129
177
|
[Yabeda::Schked](https://github.com/yabeda-rb/yabeda-schked) - built-in metrics for monitoring Schked recurring jobs out of the box! Part of the [yabeda](https://github.com/yabeda-rb/yabeda) suite.
|
data/lib/schked/cli.rb
CHANGED
|
@@ -15,7 +15,7 @@ module Schked
|
|
|
15
15
|
.shellsplit
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
super
|
|
18
|
+
super
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def self.exit_on_failure?
|
|
@@ -26,8 +26,13 @@ module Schked
|
|
|
26
26
|
|
|
27
27
|
desc "start", "Start scheduler"
|
|
28
28
|
option :require, type: :array
|
|
29
|
+
option :liveness_probe, type: :boolean, default: nil, desc: "Enable or disable the liveness probe"
|
|
30
|
+
option :liveness_bind, type: :string, desc: "Address the liveness probe binds to"
|
|
31
|
+
option :liveness_port, type: :numeric, desc: "Port the liveness probe listens on"
|
|
32
|
+
option :liveness_path, type: :string, desc: "HTTP path for the liveness probe"
|
|
29
33
|
def start
|
|
30
34
|
load_requires
|
|
35
|
+
apply_liveness_probe_options
|
|
31
36
|
|
|
32
37
|
Schked.worker.wait
|
|
33
38
|
end
|
|
@@ -50,5 +55,15 @@ module Schked
|
|
|
50
55
|
# We have to load Schked at here, because of Rails and our railtie.
|
|
51
56
|
require "schked"
|
|
52
57
|
end
|
|
58
|
+
|
|
59
|
+
def apply_liveness_probe_options
|
|
60
|
+
overrides = {}
|
|
61
|
+
overrides[:enabled] = options[:liveness_probe] unless options[:liveness_probe].nil?
|
|
62
|
+
overrides[:bind] = options[:liveness_bind] if options[:liveness_bind]
|
|
63
|
+
overrides[:port] = options[:liveness_port] if options[:liveness_port]
|
|
64
|
+
overrides[:path] = options[:liveness_path] if options[:liveness_path]
|
|
65
|
+
|
|
66
|
+
Schked.config.liveness_probe = Schked.config.liveness_probe.to_h.merge(overrides) if overrides.any?
|
|
67
|
+
end
|
|
53
68
|
end
|
|
54
69
|
end
|
data/lib/schked/config.rb
CHANGED
|
@@ -9,6 +9,14 @@ module Schked
|
|
|
9
9
|
:redis,
|
|
10
10
|
:standalone
|
|
11
11
|
|
|
12
|
+
def liveness_probe
|
|
13
|
+
@liveness_probe ||= LivenessProbeConfig.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def liveness_probe=(value)
|
|
17
|
+
@liveness_probe = value.is_a?(LivenessProbeConfig) ? value : LivenessProbeConfig.new(value)
|
|
18
|
+
end
|
|
19
|
+
|
|
12
20
|
def paths
|
|
13
21
|
@paths ||= []
|
|
14
22
|
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "ipaddr"
|
|
5
|
+
|
|
6
|
+
module Schked
|
|
7
|
+
class LivenessProbeConfig
|
|
8
|
+
attr_reader :enabled, :bind, :port, :path, :heartbeat_interval, :heartbeat_threshold
|
|
9
|
+
|
|
10
|
+
DEFAULTS = {
|
|
11
|
+
enabled: false,
|
|
12
|
+
bind: "0.0.0.0",
|
|
13
|
+
port: 8080,
|
|
14
|
+
path: "/healthz",
|
|
15
|
+
heartbeat_interval: 5,
|
|
16
|
+
heartbeat_threshold: 15
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def initialize(attrs = {})
|
|
20
|
+
attrs = DEFAULTS.merge(attrs)
|
|
21
|
+
|
|
22
|
+
@enabled = !!attrs[:enabled]
|
|
23
|
+
@bind = validate_bind(attrs[:bind])
|
|
24
|
+
@port = validate_port(attrs[:port])
|
|
25
|
+
@path = validate_path(attrs[:path])
|
|
26
|
+
@heartbeat_interval = validate_positive_integer(attrs[:heartbeat_interval], :heartbeat_interval)
|
|
27
|
+
@heartbeat_threshold = validate_threshold(attrs[:heartbeat_threshold], attrs[:heartbeat_interval])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
{
|
|
32
|
+
enabled: enabled,
|
|
33
|
+
bind: bind,
|
|
34
|
+
port: port,
|
|
35
|
+
path: path,
|
|
36
|
+
heartbeat_interval: heartbeat_interval,
|
|
37
|
+
heartbeat_threshold: heartbeat_threshold
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def validate_bind(value)
|
|
44
|
+
value = value.to_s
|
|
45
|
+
raise ArgumentError, "Schked liveness_probe `bind` must be non-empty" if value.empty?
|
|
46
|
+
|
|
47
|
+
IPAddr.new(value)
|
|
48
|
+
value
|
|
49
|
+
rescue IPAddr::InvalidAddressError
|
|
50
|
+
raise ArgumentError, "Schked liveness_probe `bind` is invalid: #{value}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate_port(value)
|
|
54
|
+
port = Integer(value)
|
|
55
|
+
raise ArgumentError, "Schked liveness_probe `port` must be between 1 and 65535, got: #{port}" unless port.between?(1, 65_535)
|
|
56
|
+
|
|
57
|
+
port
|
|
58
|
+
rescue ArgumentError, TypeError
|
|
59
|
+
raise ArgumentError, "Schked liveness_probe `port` must be an integer between 1 and 65535, got: #{value.inspect}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def validate_path(value)
|
|
63
|
+
value = value.to_s
|
|
64
|
+
raise ArgumentError, "Schked liveness_probe `path` must start with /, got: #{value}" unless value.start_with?("/")
|
|
65
|
+
|
|
66
|
+
value
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def validate_positive_integer(value, name)
|
|
70
|
+
int = Integer(value)
|
|
71
|
+
raise ArgumentError, "Schked liveness_probe `#{name}` must be a positive integer, got: #{int}" unless int.positive?
|
|
72
|
+
|
|
73
|
+
int
|
|
74
|
+
rescue ArgumentError, TypeError
|
|
75
|
+
raise ArgumentError, "Schked liveness_probe `#{name}` must be a positive integer, got: #{value.inspect}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def validate_threshold(value, interval)
|
|
79
|
+
int = validate_positive_integer(value, :heartbeat_threshold)
|
|
80
|
+
interval = validate_positive_integer(interval, :heartbeat_interval)
|
|
81
|
+
raise ArgumentError, "Schked liveness_probe `heartbeat_threshold` must be >= heartbeat_interval" if int < interval
|
|
82
|
+
|
|
83
|
+
int
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class LivenessProbe
|
|
88
|
+
attr_reader :config, :logger
|
|
89
|
+
|
|
90
|
+
def initialize(config:, logger:)
|
|
91
|
+
@config = config
|
|
92
|
+
@logger = logger
|
|
93
|
+
@last_heartbeat_at = nil
|
|
94
|
+
@shutting_down = false
|
|
95
|
+
@server = nil
|
|
96
|
+
@thread = nil
|
|
97
|
+
@mutex = Mutex.new
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def start
|
|
101
|
+
return unless config.enabled
|
|
102
|
+
|
|
103
|
+
@server = create_server
|
|
104
|
+
logger.info("Schked liveness probe listening on #{config.bind}:#{config.port}#{config.path}")
|
|
105
|
+
|
|
106
|
+
@thread = Thread.new {
|
|
107
|
+
Thread.current.name = "schked-liveness-probe" if Thread.current.respond_to?(:name=)
|
|
108
|
+
accept_loop
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def heartbeat
|
|
113
|
+
@last_heartbeat_at = Time.now
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def stop
|
|
117
|
+
@mutex.synchronize do
|
|
118
|
+
return if @shutting_down
|
|
119
|
+
|
|
120
|
+
@shutting_down = true
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
sleep 0.1
|
|
124
|
+
@server&.close
|
|
125
|
+
@thread&.join(5)
|
|
126
|
+
logger.info("Schked liveness probe stopped")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def healthy?
|
|
130
|
+
return false if @shutting_down
|
|
131
|
+
return false if @last_heartbeat_at.nil?
|
|
132
|
+
|
|
133
|
+
Time.now - @last_heartbeat_at <= config.heartbeat_threshold
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def create_server
|
|
139
|
+
TCPServer.new(config.bind, config.port)
|
|
140
|
+
rescue Errno::EADDRINUSE => e
|
|
141
|
+
raise ArgumentError, "Schked liveness probe port #{config.port} is already in use on #{config.bind}: #{e.message}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def accept_loop
|
|
145
|
+
loop do
|
|
146
|
+
client = @server.accept
|
|
147
|
+
|
|
148
|
+
Thread.new(client) do |conn|
|
|
149
|
+
handle_client(conn)
|
|
150
|
+
end
|
|
151
|
+
rescue IOError, Errno::EBADF
|
|
152
|
+
break
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def handle_client(client)
|
|
157
|
+
request_line = read_request_line(client)
|
|
158
|
+
return if request_line.nil?
|
|
159
|
+
|
|
160
|
+
_method, path, _protocol = request_line.split(" ", 3)
|
|
161
|
+
|
|
162
|
+
response = response_for(path)
|
|
163
|
+
client.print(response)
|
|
164
|
+
ensure
|
|
165
|
+
begin
|
|
166
|
+
client.close
|
|
167
|
+
rescue IOError
|
|
168
|
+
# already closed
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def read_request_line(client)
|
|
173
|
+
return nil unless IO.select([client], nil, nil, 5)
|
|
174
|
+
|
|
175
|
+
client.gets
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def response_for(path)
|
|
179
|
+
if path == config.path
|
|
180
|
+
if healthy?
|
|
181
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\nConnection: close\r\n\r\nOK"
|
|
182
|
+
else
|
|
183
|
+
"HTTP/1.1 503 Service Unavailable\r\nContent-Type: text/plain\r\nContent-Length: 11\r\nConnection: close\r\n\r\nUnavailable"
|
|
184
|
+
end
|
|
185
|
+
else
|
|
186
|
+
"HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n"
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
data/lib/schked/railtie.rb
CHANGED
|
@@ -4,6 +4,8 @@ require "rails/railtie"
|
|
|
4
4
|
|
|
5
5
|
module Schked
|
|
6
6
|
class Railtie < Rails::Railtie
|
|
7
|
+
config.schked = ActiveSupport::OrderedOptions.new
|
|
8
|
+
|
|
7
9
|
class PathsConfig
|
|
8
10
|
def self.call(app)
|
|
9
11
|
return if Schked.config.do_not_load_root_schedule?
|
|
@@ -16,7 +18,15 @@ module Schked
|
|
|
16
18
|
end
|
|
17
19
|
end
|
|
18
20
|
|
|
21
|
+
class LivenessConfig
|
|
22
|
+
def self.call(app)
|
|
23
|
+
liveness_probe = app.config.schked.liveness_probe
|
|
24
|
+
Schked.config.liveness_probe = liveness_probe if liveness_probe
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
19
28
|
initializer("schked.paths", &PathsConfig.method(:call))
|
|
29
|
+
initializer("schked.liveness", after: :load_config_initializers) { |app| LivenessConfig.call(app) }
|
|
20
30
|
|
|
21
31
|
config.to_prepare do
|
|
22
32
|
Schked.config.logger = ::Rails.logger unless Schked.config.logger?
|
data/lib/schked/version.rb
CHANGED
data/lib/schked/worker.rb
CHANGED
|
@@ -6,6 +6,7 @@ module Schked
|
|
|
6
6
|
class Worker
|
|
7
7
|
def initialize(config:)
|
|
8
8
|
@config = config
|
|
9
|
+
@liveness_probe = nil
|
|
9
10
|
|
|
10
11
|
@locker = RedisLocker.new(config.redis, lock_ttl: 40_000, logger: config.logger) unless config.standalone?
|
|
11
12
|
|
|
@@ -15,6 +16,7 @@ module Schked
|
|
|
15
16
|
define_callbacks
|
|
16
17
|
define_extend_lock unless config.standalone?
|
|
17
18
|
load_schedule
|
|
19
|
+
start_liveness_probe
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def job(as)
|
|
@@ -30,6 +32,7 @@ module Schked
|
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
def stop
|
|
35
|
+
liveness_probe&.stop
|
|
33
36
|
scheduler.stop
|
|
34
37
|
end
|
|
35
38
|
|
|
@@ -44,7 +47,7 @@ module Schked
|
|
|
44
47
|
|
|
45
48
|
private
|
|
46
49
|
|
|
47
|
-
attr_reader :config, :scheduler, :locker
|
|
50
|
+
attr_reader :config, :scheduler, :locker, :liveness_probe
|
|
48
51
|
|
|
49
52
|
def define_callbacks
|
|
50
53
|
cfg = config
|
|
@@ -94,7 +97,10 @@ module Schked
|
|
|
94
97
|
|
|
95
98
|
Thread.new do
|
|
96
99
|
loop do
|
|
97
|
-
|
|
100
|
+
if @shutdown
|
|
101
|
+
liveness_probe&.stop
|
|
102
|
+
scheduler.shutdown(wait: 5)
|
|
103
|
+
end
|
|
98
104
|
sleep 1
|
|
99
105
|
end
|
|
100
106
|
end
|
|
@@ -109,5 +115,18 @@ module Schked
|
|
|
109
115
|
def load_schedule
|
|
110
116
|
scheduler.instance_eval(schedule)
|
|
111
117
|
end
|
|
118
|
+
|
|
119
|
+
def start_liveness_probe
|
|
120
|
+
return unless config.liveness_probe.enabled
|
|
121
|
+
|
|
122
|
+
@liveness_probe = LivenessProbe.new(config: config.liveness_probe, logger: config.logger)
|
|
123
|
+
@liveness_probe.start
|
|
124
|
+
|
|
125
|
+
scheduler.every("#{config.liveness_probe.heartbeat_interval}s", as: "Schked::Worker#liveness_heartbeat", overlap: false) do
|
|
126
|
+
@liveness_probe.heartbeat
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
@liveness_probe.heartbeat
|
|
130
|
+
end
|
|
112
131
|
end
|
|
113
132
|
end
|
data/lib/schked.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: schked
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Misha Merkushin
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: logger
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: connection_pool
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -175,14 +189,14 @@ dependencies:
|
|
|
175
189
|
requirements:
|
|
176
190
|
- - "~>"
|
|
177
191
|
- !ruby/object:Gem::Version
|
|
178
|
-
version: '
|
|
192
|
+
version: '1.37'
|
|
179
193
|
type: :development
|
|
180
194
|
prerelease: false
|
|
181
195
|
version_requirements: !ruby/object:Gem::Requirement
|
|
182
196
|
requirements:
|
|
183
197
|
- - "~>"
|
|
184
198
|
- !ruby/object:Gem::Version
|
|
185
|
-
version: '
|
|
199
|
+
version: '1.37'
|
|
186
200
|
description: Rufus-scheduler wrapper to run recurring jobs
|
|
187
201
|
email:
|
|
188
202
|
- merkushin.m.s@gmail.com
|
|
@@ -197,6 +211,7 @@ files:
|
|
|
197
211
|
- lib/schked.rb
|
|
198
212
|
- lib/schked/cli.rb
|
|
199
213
|
- lib/schked/config.rb
|
|
214
|
+
- lib/schked/liveness_probe.rb
|
|
200
215
|
- lib/schked/railtie.rb
|
|
201
216
|
- lib/schked/redis_client_factory.rb
|
|
202
217
|
- lib/schked/redis_locker.rb
|