service_skeleton 0.0.0.44.g75d07d7 → 1.0.3

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,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