service_skeleton 0.0.0.44.g75d07d7 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServiceSkeleton
4
+ module SignalsMethods
5
+ def registered_signal_handlers
6
+ @registered_signal_handlers || []
7
+ end
8
+
9
+ def hook_signal(sigspec, &blk)
10
+ @registered_signal_handlers ||= []
11
+
12
+ @registered_signal_handlers << [sigspec, blk]
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServiceSkeleton
4
+ module UltravisorChildren
5
+ def register_ultravisor_children(ultravisor, config:, metrics_registry:)
6
+ begin
7
+ ultravisor.add_child(
8
+ id: self.service_name.to_sym,
9
+ klass: self,
10
+ method: :run,
11
+ args: [config: config, metrics: metrics_registry],
12
+ access: :unsafe
13
+ )
14
+ rescue Ultravisor::InvalidKAMError
15
+ raise ServiceSkeleton::Error::InvalidServiceClassError,
16
+ "Class #{self.to_s} does not implement the `run' instance method"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServiceSkeleton
4
+ module UltravisorLoggerstash
5
+ def logstash_writer
6
+ #:nocov:
7
+ @ultravisor[:logstash_writer].unsafe_instance
8
+ #:nocov:
9
+ end
10
+ end
11
+ end
@@ -24,22 +24,20 @@ Gem::Specification.new do |s|
24
24
  common aspects of a system service.
25
25
  EOF
26
26
 
27
- s.authors = ["Matt Palmer"]
28
- s.email = ["matt.palmer@discourse.org"]
27
+ s.authors = ["Matt Palmer", "Sam Saffron"]
28
+ s.email = ["sam.saffron@discourse.org"]
29
29
  s.homepage = "https://github.com/discourse/service_skeleton"
30
30
 
31
31
  s.files = `git ls-files -z`.split("\0").reject { |f| f =~ /^(G|spec|Rakefile)/ }
32
32
 
33
33
  s.required_ruby_version = ">= 2.5.0"
34
34
 
35
- s.add_runtime_dependency "frankenstein", "~> 1.2"
36
- s.add_runtime_dependency "loggerstash", "~> 0.0"
37
- # prometheus-client provides no guaranteed backwards compatibility,
38
- # and in fact happily breaks things with no notice, so we're stuck
39
- # with hard-coding a specific version to avoid unexpected disaster.
40
- s.add_runtime_dependency "prometheus-client", "0.8.0"
35
+ s.add_runtime_dependency "frankenstein", "~> 2.0"
36
+ s.add_runtime_dependency "loggerstash", ">= 0.0.9", "< 1"
37
+ s.add_runtime_dependency "prometheus-client", "~> 2.0"
41
38
  s.add_runtime_dependency "sigdump", "~> 0.2"
42
39
  s.add_runtime_dependency "to_regexp", "~> 0.2"
40
+ s.add_runtime_dependency "ultravisor", "~> 0.a"
43
41
 
44
42
  s.add_development_dependency 'bundler'
45
43
  s.add_development_dependency 'github-release'
@@ -47,10 +45,11 @@ Gem::Specification.new do |s|
47
45
  s.add_development_dependency 'guard-rspec'
48
46
  s.add_development_dependency 'guard-rubocop'
49
47
  s.add_development_dependency 'rack-test'
50
- s.add_development_dependency 'rake', "~> 12.0"
48
+ s.add_development_dependency 'rake'
51
49
  s.add_development_dependency 'redcarpet'
52
50
  s.add_development_dependency 'rspec'
53
51
  s.add_development_dependency 'rubocop'
52
+ s.add_development_dependency 'rubocop-discourse'
54
53
  s.add_development_dependency 'simplecov'
55
54
  s.add_development_dependency 'yard'
56
55
  end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: service_skeleton
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0.44.g75d07d7
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Palmer
8
- autorequire:
8
+ - Sam Saffron
9
+ autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2019-07-26 00:00:00.000000000 Z
12
+ date: 2021-01-13 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: frankenstein
@@ -16,42 +17,48 @@ dependencies:
16
17
  requirements:
17
18
  - - "~>"
18
19
  - !ruby/object:Gem::Version
19
- version: '1.2'
20
+ version: '2.0'
20
21
  type: :runtime
21
22
  prerelease: false
22
23
  version_requirements: !ruby/object:Gem::Requirement
23
24
  requirements:
24
25
  - - "~>"
25
26
  - !ruby/object:Gem::Version
26
- version: '1.2'
27
+ version: '2.0'
27
28
  - !ruby/object:Gem::Dependency
28
29
  name: loggerstash
29
30
  requirement: !ruby/object:Gem::Requirement
30
31
  requirements:
31
- - - "~>"
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 0.0.9
35
+ - - "<"
32
36
  - !ruby/object:Gem::Version
33
- version: '0.0'
37
+ version: '1'
34
38
  type: :runtime
35
39
  prerelease: false
36
40
  version_requirements: !ruby/object:Gem::Requirement
37
41
  requirements:
38
- - - "~>"
42
+ - - ">="
39
43
  - !ruby/object:Gem::Version
40
- version: '0.0'
44
+ version: 0.0.9
45
+ - - "<"
46
+ - !ruby/object:Gem::Version
47
+ version: '1'
41
48
  - !ruby/object:Gem::Dependency
42
49
  name: prometheus-client
43
50
  requirement: !ruby/object:Gem::Requirement
44
51
  requirements:
45
- - - '='
52
+ - - "~>"
46
53
  - !ruby/object:Gem::Version
47
- version: 0.8.0
54
+ version: '2.0'
48
55
  type: :runtime
49
56
  prerelease: false
50
57
  version_requirements: !ruby/object:Gem::Requirement
51
58
  requirements:
52
- - - '='
59
+ - - "~>"
53
60
  - !ruby/object:Gem::Version
54
- version: 0.8.0
61
+ version: '2.0'
55
62
  - !ruby/object:Gem::Dependency
56
63
  name: sigdump
57
64
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +87,20 @@ dependencies:
80
87
  - - "~>"
81
88
  - !ruby/object:Gem::Version
82
89
  version: '0.2'
90
+ - !ruby/object:Gem::Dependency
91
+ name: ultravisor
92
+ requirement: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.a
97
+ type: :runtime
98
+ prerelease: false
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.a
83
104
  - !ruby/object:Gem::Dependency
84
105
  name: bundler
85
106
  requirement: !ruby/object:Gem::Requirement
@@ -168,16 +189,16 @@ dependencies:
168
189
  name: rake
169
190
  requirement: !ruby/object:Gem::Requirement
170
191
  requirements:
171
- - - "~>"
192
+ - - ">="
172
193
  - !ruby/object:Gem::Version
173
- version: '12.0'
194
+ version: '0'
174
195
  type: :development
175
196
  prerelease: false
176
197
  version_requirements: !ruby/object:Gem::Requirement
177
198
  requirements:
178
- - - "~>"
199
+ - - ">="
179
200
  - !ruby/object:Gem::Version
180
- version: '12.0'
201
+ version: '0'
181
202
  - !ruby/object:Gem::Dependency
182
203
  name: redcarpet
183
204
  requirement: !ruby/object:Gem::Requirement
@@ -220,6 +241,20 @@ dependencies:
220
241
  - - ">="
221
242
  - !ruby/object:Gem::Version
222
243
  version: '0'
244
+ - !ruby/object:Gem::Dependency
245
+ name: rubocop-discourse
246
+ requirement: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - ">="
249
+ - !ruby/object:Gem::Version
250
+ version: '0'
251
+ type: :development
252
+ prerelease: false
253
+ version_requirements: !ruby/object:Gem::Requirement
254
+ requirements:
255
+ - - ">="
256
+ - !ruby/object:Gem::Version
257
+ version: '0'
223
258
  - !ruby/object:Gem::Dependency
224
259
  name: simplecov
225
260
  requirement: !ruby/object:Gem::Requirement
@@ -256,23 +291,23 @@ description: |
256
291
  for your services, as well as a collection of helper classes to manage
257
292
  common aspects of a system service.
258
293
  email:
259
- - matt.palmer@discourse.org
294
+ - sam.saffron@discourse.org
260
295
  executables: []
261
296
  extensions: []
262
297
  extra_rdoc_files: []
263
298
  files:
264
299
  - ".editorconfig"
300
+ - ".github/workflows/ci.yml"
265
301
  - ".gitignore"
266
302
  - ".rubocop.yml"
267
- - ".travis.yml"
268
303
  - ".yardopts"
269
304
  - CODE_OF_CONDUCT.md
270
305
  - CONTRIBUTING.md
271
306
  - LICENCE
272
307
  - README.md
273
308
  - lib/service_skeleton.rb
274
- - lib/service_skeleton/background_worker.rb
275
309
  - lib/service_skeleton/config.rb
310
+ - lib/service_skeleton/config_class.rb
276
311
  - lib/service_skeleton/config_variable.rb
277
312
  - lib/service_skeleton/config_variable/boolean.rb
278
313
  - lib/service_skeleton/config_variable/enum.rb
@@ -282,17 +317,25 @@ files:
282
317
  - lib/service_skeleton/config_variable/path_list.rb
283
318
  - lib/service_skeleton/config_variable/string.rb
284
319
  - lib/service_skeleton/config_variable/url.rb
320
+ - lib/service_skeleton/config_variable/yaml_file.rb
285
321
  - lib/service_skeleton/config_variables.rb
286
322
  - lib/service_skeleton/error.rb
287
323
  - lib/service_skeleton/filtering_logger.rb
324
+ - lib/service_skeleton/generator.rb
288
325
  - lib/service_skeleton/logging_helpers.rb
326
+ - lib/service_skeleton/metric_method_name.rb
289
327
  - lib/service_skeleton/metrics_methods.rb
290
- - lib/service_skeleton/signal_handler.rb
328
+ - lib/service_skeleton/runner.rb
329
+ - lib/service_skeleton/service_name.rb
330
+ - lib/service_skeleton/signal_manager.rb
331
+ - lib/service_skeleton/signals_methods.rb
332
+ - lib/service_skeleton/ultravisor_children.rb
333
+ - lib/service_skeleton/ultravisor_loggerstash.rb
291
334
  - service_skeleton.gemspec
292
335
  homepage: https://github.com/discourse/service_skeleton
293
336
  licenses: []
294
337
  metadata: {}
295
- post_install_message:
338
+ post_install_message:
296
339
  rdoc_options: []
297
340
  require_paths:
298
341
  - lib
@@ -303,12 +346,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
303
346
  version: 2.5.0
304
347
  required_rubygems_version: !ruby/object:Gem::Requirement
305
348
  requirements:
306
- - - ">"
349
+ - - ">="
307
350
  - !ruby/object:Gem::Version
308
- version: 1.3.1
351
+ version: '0'
309
352
  requirements: []
310
- rubygems_version: 3.0.1
311
- signing_key:
353
+ rubygems_version: 3.1.4
354
+ signing_key:
312
355
  specification_version: 4
313
356
  summary: The bare bones of a service
314
357
  test_files: []
@@ -1,11 +0,0 @@
1
- language: ruby
2
- sudo: false
3
- cache: bundler
4
-
5
- rvm:
6
- - 2.5.3
7
- - 2.4.5
8
- - 2.3.8
9
-
10
- gemfile:
11
- - Gemfile
@@ -1,96 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class ServiceSkeleton
4
- module BackgroundWorker
5
- include ServiceSkeleton::LoggingHelpers
6
-
7
- # async code is a shit to test, and rarely finds bugs anyway, so let's
8
- # just
9
- #:nocov:
10
- # this whole thing.
11
-
12
- # This signal is raised internally if needed to shut down a worker thread.
13
- class TerminateBackgroundThread < Exception; end
14
- private_constant :TerminateBackgroundThread
15
-
16
- def initialize(*_)
17
- @bg_worker_op_mutex = Mutex.new
18
- @bg_worker_op_cv = ConditionVariable.new
19
-
20
- begin
21
- super
22
- rescue ArgumentError => ex
23
- if ex.message =~ /wrong number of arguments.*expected 0/
24
- super()
25
- else
26
- raise
27
- end
28
- end
29
- end
30
-
31
- def start!
32
- @bg_worker_op_mutex.synchronize do
33
- return if @bg_worker_thread
34
-
35
- @bg_worker_thread = Thread.new do
36
- Thread.current.name = self.class.to_s
37
-
38
- Thread.handle_interrupt(Exception => :never) do
39
- logger.debug("BackgroundWorker(#{self.class})#start!") { "Background worker thread #{Thread.current.object_id} starting" }
40
- begin
41
- Thread.handle_interrupt(Exception => :immediate) do
42
- @bg_worker_op_mutex.synchronize { @bg_worker_op_cv.signal }
43
- self.start
44
- end
45
- rescue TerminateBackgroundThread
46
- logger.debug("BackgroundWorker(#{self.class})#start!") { "Background worker thread #{Thread.current.object_id} received magical termination exception" }
47
- rescue Exception => ex
48
- log_exception(ex) { "Background worker thread #{Thread.current.object_id} received fatal exception" }
49
- else
50
- logger.debug("BackgroundWorker(#{self.class})#start!") { "Background worker thread #{Thread.current.object_id} terminating" }
51
- end
52
- end
53
- logger.debug("BackgroundWorker(#{self.class})#start!") { "Background worker thread #{Thread.current.object_id} is now done" }
54
- end
55
-
56
- @bg_worker_op_cv.wait(@bg_worker_op_mutex) until @bg_worker_thread
57
- end
58
- end
59
-
60
- def stop!(force = nil)
61
- @bg_worker_op_mutex.synchronize do
62
- return if @bg_worker_thread.nil?
63
-
64
- logger.debug("BackgroundWorker(#{self.class})#stop!") { "Terminating worker thread #{@bg_worker_thread.object_id} as requested" }
65
-
66
- if force == :force
67
- logger.debug(logloc) { "Forcing termination" }
68
- @bg_worker_thread.raise(TerminateBackgroundThread)
69
- else
70
- logger.debug(logloc) { "Gracefully terminating worker thread" }
71
- shutdown
72
- end
73
-
74
- begin
75
- logger.debug(logloc) { "Waiting for worker thread #{@bg_worker_thread.object_id} to finish itself off" }
76
- @bg_worker_thread.join unless @bg_worker_thread == Thread.current
77
- rescue TerminateBackgroundThread
78
- nil
79
- end
80
-
81
- @bg_worker_thread = nil
82
-
83
- logger.debug("BackgroundWorker(#{self.class})#stop!") { "Worker thread #{@bg_worker_thread.object_id} terminated" }
84
- end
85
- end
86
-
87
- private
88
-
89
- attr_reader :logger
90
-
91
- def shutdown
92
- logger.debug("BackgroundWorker(#{self.class})#stop!") { "Using default shutdown method" }
93
- @bg_worker_thread.raise(TerminateBackgroundThread) if @bg_worker_thread
94
- end
95
- end
96
- end
@@ -1,197 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "./background_worker"
4
- require_relative "./logging_helpers"
5
-
6
- class ServiceSkeleton
7
- # Manage signals in a sane and safe manner.
8
- #
9
- # Signal handling is a shit of a thing. The code that runs when a signal is
10
- # triggered can't use mutexes (which are used in all sorts of places you
11
- # might not expect, like Logger!) or anything else that might block. This
12
- # greatly constrains what you can do inside a signal handler, so the standard
13
- # approach is to stuff a character down a pipe, and then have the *real*
14
- # signal handling run later.
15
- #
16
- # Also, there's always the (slim) possibility that something else might have
17
- # hooked into a signal we want to receive. Because only a single signal
18
- # handler can be active for a given signal at a time, we need to "chain" the
19
- # existing handler, by calling the previous signal handler from our signal
20
- # handler after we've done what we need to do. This class takes care of
21
- # that, too, because it's a legend.
22
- #
23
- # So that's what this class does: it allows you to specify signals and
24
- # associated blocks of code to run, it sets up signal handlers which send
25
- # notifications to a background thread and chain correctly, and it manages
26
- # the background thread to receive the notifications and execute the
27
- # associated blocks of code outside of the context of the signal handler.
28
- #
29
- class SignalHandler
30
- include ServiceSkeleton::LoggingHelpers
31
- include ServiceSkeleton::BackgroundWorker
32
-
33
- # Setup a signal handler instance.
34
- #
35
- # A single signal handler instance can handle up to 256 hooks, potentially
36
- # hooking the same signal more than once. Use #hook_signal to register
37
- # signal handling callbacks.
38
- #
39
- # @param logger [Logger] the logger to use for all the interesting information
40
- # about what we're up to.
41
- #
42
- def initialize(logger:, service:, signal_counter:)
43
- @logger, @service, @signal_counter = logger, service, signal_counter
44
-
45
- @signal_registry = []
46
-
47
- @handler_install_mutex = Mutex.new
48
-
49
- super
50
- end
51
-
52
- #:nocov:
53
-
54
- # Register a callback to be executed on the receipt of a specified signal.
55
- #
56
- # @param sig [String, Symbol, Integer] the signal to hook into. Anything that
57
- # `Signal.trap` will accept is OK by us, too.
58
- #
59
- # @param blk [Proc] the code to run when the signal is received.
60
- #
61
- # @return [void]
62
- #
63
- # @raise [RuntimeError] if you try to create more than 256 signal hooks.
64
- #
65
- # @raise [ArgumentError] if `sig` isn't recognised as a valid signal
66
- # specifier by `Signal.trap`.
67
- #
68
- def hook_signal(sig, &blk)
69
- logger.debug(logloc) { "Hooking signal #{sig}" }
70
-
71
- handler_num = @signal_registry.length
72
-
73
- if handler_num > 255
74
- raise RuntimeError,
75
- "Signal hook limit reached. Slow down there, pardner"
76
- end
77
-
78
- sigspec = { signal: sig, callback: blk }
79
-
80
- @handler_install_mutex.synchronize do
81
- if @bg_worker_thread
82
- install_handler(sigspec, handler_num)
83
- else
84
- # If the background thread isn't running yet, the signal handler will
85
- # be installed when that is started.
86
- logger.debug(logloc) { "Deferring installation of handler for #{sig} (#{handler_num})" }
87
- end
88
-
89
- @signal_registry << sigspec
90
- end
91
- end
92
-
93
- def start
94
- @handler_install_mutex.synchronize do
95
- logger.info(logloc) { "Starting signal handler with #{@signal_registry.length} hooks" }
96
-
97
- @r, @w = IO.pipe
98
-
99
- install_signal_handlers
100
- end
101
-
102
- loop do
103
- begin
104
- if ios = IO.select([@r])
105
- if ios.first.include?(@r)
106
- if ios.first.first.eof?
107
- logger.info(logloc) { "Signal pipe closed; shutting down" }
108
- break
109
- else
110
- c = ios.first.first.read_nonblock(1)
111
- logger.debug(logloc) { "Received character #{c.inspect} from signal pipe" }
112
- handle_signal(c)
113
- end
114
- else
115
- logger.error(logloc) { "Mysterious return from select: #{ios.inspect}" }
116
- end
117
- end
118
- rescue IOError
119
- # Something has gone terribly wrong here... bail
120
- break
121
- rescue StandardError => ex
122
- log_exception(ex) { "Exception in select loop" }
123
- end
124
- end
125
- end
126
-
127
- private
128
-
129
- attr_reader :logger
130
-
131
- # Given a character (presumably) received via the signal pipe, execute the
132
- # associated handler.
133
- #
134
- # @param char [String] a single character, corresponding to an entry in the
135
- # signal registry.
136
- #
137
- # @return [void]
138
- #
139
- def handle_signal(char)
140
- handler = @signal_registry[char.ord]
141
-
142
- if handler
143
- logger.debug(logloc) { "#{handler[:signal]} received" }
144
- @signal_counter.increment(signal: handler[:signal].to_s)
145
- begin
146
- handler[:callback].call
147
- rescue => ex
148
- log_exception(ex) { "Exception in signal handler" }
149
- end
150
- else
151
- logger.error(logloc) { "Unrecognised signal character: #{char.inspect}" }
152
- end
153
- end
154
-
155
- def install_signal_handlers
156
- @signal_registry.each_with_index do |sigspec, i|
157
- install_handler(sigspec, i)
158
- end
159
- end
160
-
161
- def install_handler(sigspec, i)
162
- logger.debug(logloc) { "Installing signal handler for #{sigspec[:signal]}" }
163
- chain = nil
164
-
165
- p = ->(*args) do
166
- @w.write_nonblock(i.chr) rescue nil
167
- chain.call(*args) if chain.respond_to?(:call)
168
- end
169
- chain = Signal.trap(sigspec[:signal], &p)
170
-
171
- sigspec[:chain] = chain
172
- sigspec[:handler] = p
173
- end
174
-
175
- def shutdown
176
- uninstall_signal_handlers
177
-
178
- @r.close
179
- end
180
-
181
- def uninstall_signal_handlers
182
- @signal_registry.reverse.each do |sigspec|
183
- tmp_sig = Signal.trap(sigspec[:signal], "IGNORE")
184
- if tmp_sig == sigspec[:handler]
185
- # The current handler is ours, so we can replace
186
- # it with the chained handler
187
- Signal.trap(sigspec[:signal], sigspec[:chain])
188
- else
189
- # The current handler *isn't* this one, so we better
190
- # put it back, because whoever owns it might get
191
- # angry.
192
- Signal.trap(sigspec[:signal], tmp_sig)
193
- end
194
- end
195
- end
196
- end
197
- end