service_skeleton 0.0.0.1.ENOTAG → 0.0.0.2.g46c1e0e

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +0 -2
  3. data/.rubocop.yml +114 -9
  4. data/.travis.yml +11 -0
  5. data/README.md +153 -279
  6. data/lib/service_skeleton/background_worker.rb +80 -0
  7. data/lib/service_skeleton/config.rb +18 -78
  8. data/lib/service_skeleton/config_variable.rb +8 -29
  9. data/lib/service_skeleton/config_variables.rb +68 -54
  10. data/lib/service_skeleton/error.rb +3 -5
  11. data/lib/service_skeleton/filtering_logger.rb +0 -2
  12. data/lib/service_skeleton/logging_helpers.rb +3 -10
  13. data/lib/service_skeleton/metrics_methods.rb +13 -28
  14. data/lib/service_skeleton/signal_handler.rb +183 -0
  15. data/lib/service_skeleton.rb +145 -22
  16. data/service_skeleton.gemspec +9 -10
  17. metadata +19 -102
  18. data/.editorconfig +0 -7
  19. data/.git-blame-ignore-revs +0 -2
  20. data/.github/workflows/ci.yml +0 -50
  21. data/lib/service_skeleton/config_class.rb +0 -16
  22. data/lib/service_skeleton/config_variable/boolean.rb +0 -21
  23. data/lib/service_skeleton/config_variable/enum.rb +0 -27
  24. data/lib/service_skeleton/config_variable/float.rb +0 -25
  25. data/lib/service_skeleton/config_variable/integer.rb +0 -25
  26. data/lib/service_skeleton/config_variable/kv_list.rb +0 -26
  27. data/lib/service_skeleton/config_variable/path_list.rb +0 -13
  28. data/lib/service_skeleton/config_variable/string.rb +0 -18
  29. data/lib/service_skeleton/config_variable/url.rb +0 -36
  30. data/lib/service_skeleton/config_variable/yaml_file.rb +0 -42
  31. data/lib/service_skeleton/generator.rb +0 -165
  32. data/lib/service_skeleton/metric_method_name.rb +0 -9
  33. data/lib/service_skeleton/runner.rb +0 -46
  34. data/lib/service_skeleton/service_name.rb +0 -20
  35. data/lib/service_skeleton/signal_manager.rb +0 -202
  36. data/lib/service_skeleton/signals_methods.rb +0 -15
  37. data/lib/service_skeleton/ultravisor_children.rb +0 -20
  38. data/lib/service_skeleton/ultravisor_loggerstash.rb +0 -11
  39. data/ultravisor/.yardopts +0 -1
  40. data/ultravisor/Guardfile +0 -9
  41. data/ultravisor/README.md +0 -404
  42. data/ultravisor/lib/ultravisor/child/call.rb +0 -21
  43. data/ultravisor/lib/ultravisor/child/call_receiver.rb +0 -14
  44. data/ultravisor/lib/ultravisor/child/cast.rb +0 -16
  45. data/ultravisor/lib/ultravisor/child/cast_receiver.rb +0 -11
  46. data/ultravisor/lib/ultravisor/child/process_cast_call.rb +0 -39
  47. data/ultravisor/lib/ultravisor/child.rb +0 -481
  48. data/ultravisor/lib/ultravisor/error.rb +0 -25
  49. data/ultravisor/lib/ultravisor/logging_helpers.rb +0 -32
  50. data/ultravisor/lib/ultravisor.rb +0 -216
  51. data/ultravisor/spec/example_group_methods.rb +0 -19
  52. data/ultravisor/spec/example_methods.rb +0 -8
  53. data/ultravisor/spec/spec_helper.rb +0 -52
  54. data/ultravisor/spec/ultravisor/add_child_spec.rb +0 -79
  55. data/ultravisor/spec/ultravisor/child/call_spec.rb +0 -121
  56. data/ultravisor/spec/ultravisor/child/cast_spec.rb +0 -111
  57. data/ultravisor/spec/ultravisor/child/id_spec.rb +0 -21
  58. data/ultravisor/spec/ultravisor/child/new_spec.rb +0 -152
  59. data/ultravisor/spec/ultravisor/child/restart_delay_spec.rb +0 -40
  60. data/ultravisor/spec/ultravisor/child/restart_spec.rb +0 -70
  61. data/ultravisor/spec/ultravisor/child/run_spec.rb +0 -95
  62. data/ultravisor/spec/ultravisor/child/shutdown_spec.rb +0 -124
  63. data/ultravisor/spec/ultravisor/child/spawn_spec.rb +0 -107
  64. data/ultravisor/spec/ultravisor/child/unsafe_instance_spec.rb +0 -55
  65. data/ultravisor/spec/ultravisor/child/wait_spec.rb +0 -32
  66. data/ultravisor/spec/ultravisor/new_spec.rb +0 -71
  67. data/ultravisor/spec/ultravisor/remove_child_spec.rb +0 -49
  68. data/ultravisor/spec/ultravisor/run_spec.rb +0 -334
  69. data/ultravisor/spec/ultravisor/shutdown_spec.rb +0 -106
@@ -1,202 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "./logging_helpers"
4
-
5
- module ServiceSkeleton
6
- # Manage signals in a sane and safe manner.
7
- #
8
- # Signal handling is a shit of a thing. The code that runs when a signal is
9
- # triggered can't use mutexes (which are used in all sorts of places you
10
- # might not expect, like Logger!) or anything else that might block. This
11
- # greatly constrains what you can do inside a signal handler, so the standard
12
- # approach is to stuff a character down a pipe, and then have the *real*
13
- # signal handling run later.
14
- #
15
- # Also, there's always the (slim) possibility that something else might have
16
- # hooked into a signal we want to receive. Because only a single signal
17
- # handler can be active for a given signal at a time, we need to "chain" the
18
- # existing handler, by calling the previous signal handler from our signal
19
- # handler after we've done what we need to do. This class takes care of
20
- # that, too, because it's a legend.
21
- #
22
- # So that's what this class does: it allows you to specify signals and
23
- # associated blocks of code to run, it sets up signal handlers which send
24
- # notifications to a background thread and chain correctly, and it manages
25
- # the background thread to receive the notifications and execute the
26
- # associated blocks of code outside of the context of the signal handler.
27
- #
28
- class SignalManager
29
- include ServiceSkeleton::LoggingHelpers
30
-
31
- # Setup a signal handler instance.
32
- #
33
- # @param logger [Logger] the logger to use for all the interesting information
34
- # about what we're up to.
35
- #
36
- def initialize(logger:, counter:, signals:)
37
- @logger, @signal_counter, @signal_list = logger, counter, signals
38
-
39
- @registry = Hash.new { |h, k| h[k] = SignalHandler.new(k) }
40
-
41
- @signal_list.each do |sig, proc|
42
- @registry[signum(sig)] << proc
43
- end
44
- end
45
-
46
- def run
47
- logger.info(logloc) { "Starting signal manager for #{@signal_list.length} signals" }
48
-
49
- @r, @w = IO.pipe
50
-
51
- install_signal_handlers
52
-
53
- signals_loop
54
- ensure
55
- remove_signal_handlers
56
- end
57
-
58
- def shutdown
59
- @r.close
60
- end
61
-
62
- private
63
-
64
- attr_reader :logger
65
-
66
- def signals_loop
67
- #:nocov:
68
- loop do
69
- begin
70
- if ios = IO.select([@r])
71
- if ios.first.include?(@r)
72
- if ios.first.first.eof?
73
- logger.info(logloc) { "Signal pipe closed; shutting down" }
74
- break
75
- else
76
- c = ios.first.first.read_nonblock(1)
77
- logger.debug(logloc) { "Received character #{c.inspect} from signal pipe" }
78
- handle_signal(c)
79
- end
80
- else
81
- logger.error(logloc) { "Mysterious return from select: #{ios.inspect}" }
82
- end
83
- end
84
- rescue IOError
85
- # Something has gone terribly wrong here... bail
86
- break
87
- rescue StandardError => ex
88
- log_exception(ex) { "Exception in select loop" }
89
- end
90
- end
91
- #:nocov:
92
- end
93
-
94
- # Given a character (presumably) received via the signal pipe, execute the
95
- # associated handler.
96
- #
97
- # @param char [String] a single character, corresponding to an entry in the
98
- # signal registry.
99
- #
100
- # @return [void]
101
- #
102
- def handle_signal(char)
103
- if @registry.has_key?(char.ord)
104
- handler = @registry[char.ord]
105
- logger.debug(logloc) { "#{handler.signame} received" }
106
- @signal_counter.increment(labels: { signal: handler.signame.to_s })
107
-
108
- begin
109
- handler.call
110
- rescue StandardError => ex
111
- log_exception(ex) { "Exception while calling signal handler" }
112
- end
113
- else
114
- logger.error(logloc) { "Unrecognised signal character: #{char.inspect}" }
115
- end
116
- end
117
-
118
- def install_signal_handlers
119
- @registry.values.each do |h|
120
- h.write_pipe = @w
121
- h.hook
122
- end
123
- end
124
-
125
- def signum(spec)
126
- if spec.is_a?(Integer)
127
- return spec
128
- end
129
-
130
- if spec.is_a?(Symbol)
131
- str = spec.to_s
132
- elsif spec.is_a?(String)
133
- str = spec.dup
134
- else
135
- raise ArgumentError,
136
- "Unsupported class (#{spec.class}) of signal specifier #{spec.inspect}"
137
- end
138
-
139
- str.sub!(/\ASIG/i, '')
140
-
141
- if Signal.list[str.upcase]
142
- Signal.list[str.upcase]
143
- else
144
- raise ArgumentError,
145
- "Unrecognised signal specifier #{spec.inspect}"
146
- end
147
- end
148
-
149
- def remove_signal_handlers
150
- @registry.values.each { |h| h.unhook }
151
- end
152
-
153
- class SignalHandler
154
- attr_reader :signame
155
- attr_writer :write_pipe
156
-
157
- def initialize(signum)
158
- @signum = signum
159
- @callbacks = []
160
-
161
- @signame = Signal.list.invert[@signum]
162
- end
163
-
164
- def <<(proc)
165
- @callbacks << proc
166
- end
167
-
168
- def call
169
- @callbacks.each { |cb| cb.call }
170
- end
171
-
172
- def hook
173
- @handler = ->(_) do
174
- #:nocov:
175
- @write_pipe.write_nonblock(@signum.chr) rescue nil
176
- @chain.call if @chain.respond_to?(:call)
177
- #:nocov:
178
- end
179
-
180
- @chain = Signal.trap(@signum, &@handler)
181
- end
182
-
183
- def unhook
184
- #:nocov:
185
- tmp_handler = Signal.trap(@signum, "IGNORE")
186
- if tmp_handler == @handler
187
- # The current handler is ours, so we can replace it
188
- # with the chained handler
189
- Signal.trap(@signum, @chain)
190
- else
191
- # The current handler *isn't* ours, so we better
192
- # put it back, because whoever owns it might get
193
- # angry.
194
- Signal.trap(@signum, tmp_handler)
195
- end
196
- #:nocov:
197
- end
198
- end
199
-
200
- private_constant :SignalHandler
201
- end
202
- end
@@ -1,15 +0,0 @@
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
@@ -1,20 +0,0 @@
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
@@ -1,11 +0,0 @@
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
data/ultravisor/.yardopts DELETED
@@ -1 +0,0 @@
1
- --markup markdown
data/ultravisor/Guardfile DELETED
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
- guard 'rspec',
3
- cmd: "bundle exec rspec",
4
- all_on_start: true,
5
- all_after_pass: true do
6
- watch(%r{^spec/.+_spec\.rb$})
7
- watch(%r{^spec/.+_methods\.rb$})
8
- watch(%r{^lib/}) { "spec" }
9
- end
data/ultravisor/README.md DELETED
@@ -1,404 +0,0 @@
1
- > # WARNING WARNING WARNING
2
- >
3
- > This README is, at least in part, speculative fiction. I practice
4
- > README-driven development, and as such, not everything described in here
5
- > actually exists yet, and what does exist may not work right.
6
-
7
- Ultravisor is like a supervisor, but... *ULTRA*. The idea is that you specify
8
- objects to instantiate and run in threads, and then the Ultravisor makes that
9
- happen behind the scenes, including logging failures, restarting if necessary,
10
- and so on. If you're familiar with Erlang supervision trees, then Ultravisor
11
- will feel familiar to you, because I stole pretty much every good idea that
12
- is in Ultravisor from Erlang. You will get a lot of very excellent insight
13
- from reading [the Erlang/OTP Supervision Principles](http://erlang.org/doc/design_principles/sup_princ.html).
14
-
15
- # Usage
16
-
17
- This section gives you a basic overview of the high points of how Ultravisor
18
- can be used. It is not intended to be an exhaustive reference of all possible
19
- options; the {Ultravisor} class API documentation provides every possible option
20
- and its meaning.
21
-
22
-
23
- ## The Basics
24
-
25
- Start by loading the code:
26
-
27
- require "ultravisor"
28
-
29
- Creating a new Ultravisor is a matter of instantiating a new object:
30
-
31
- u = Ultravisor.new
32
-
33
- In order for it to be useful, though, you'll need to add one or more children
34
- to the Ultravisor instance, which can either be done as part of the call to
35
- `.new`, or afterwards, as you see fit:
36
-
37
- # Defining a child in the constructor
38
- u = Ultravisor.new(children: [{id: :child, klass: Child, method: :run}])
39
-
40
- # OR define it afterwards
41
- u = Ultravisor.new
42
- u.add_child(id: :my_child, klass: Child, method: :run)
43
-
44
- Once you have an Ultravisor with children configured, you can set it running:
45
-
46
- u.run
47
-
48
- This will block until the Ultravisor terminates, one way or another.
49
-
50
- We'll learn about other available initialization arguments, and all the other
51
- features of Ultravisor, in the following sections.
52
-
53
-
54
- ## Defining Children
55
-
56
- As children are the primary reason Ultravisor exists, it is worth getting a handle
57
- on them first.
58
-
59
- Defining children, as we saw in the introduction, can be done by calling
60
- {Ultravisor#add_child} for each child you want to add, or else you can provide
61
- a list of children to start as part of the {Ultravisor.new} call, using the
62
- `children` named argument. You can also combine the two approaches, if some
63
- children are defined statically, while others only get added conditionally.
64
-
65
- Let's take another look at that {Ultravisor#add_child} method from earlier:
66
-
67
- u.add_child(id: :my_child, klass: Child, method: :run)
68
-
69
- First up, every child has an ID. This is fairly straightforward -- it's a
70
- unique ID (within a given Ultravisor) that refers to the child. Attempting to
71
- add two children with the same ID will raise an exception.
72
-
73
- The `class` and `method` arguments require a little more explanation. One
74
- of the foundational principles of "fail fast" is "clean restart" -- that is, if you
75
- do need to restart something, it's important to start with as clean a state as possible.
76
- Thus, if a child needs to be restarted, we don't want to reuse an existing object, which
77
- may be in a messy and unuseable state. Instead, we want a clean, fresh object to work on.
78
- That's why you specify a `class` when you define a child -- it is a new instance of that
79
- class that will be used every time the child is started (or restarted).
80
-
81
- The `method` argument might now be obvious. Once the new instance of the
82
- specified `class` exists, the Ultravisor will call the specified `method` to start
83
- work happening. It is expected that this method will ***not return***, in most cases.
84
- So you probably want some sort of infinite loop.
85
-
86
- You might think that this is extremely inflexible, only being able to specify a class
87
- and a method to call. What if you want to pass in some parameters? Don't worry, we've
88
- got you covered:
89
-
90
- u.add_child(
91
- id: :my_child,
92
- klass: Child,
93
- args: ['foo', 42, x: 1, y: 2],
94
- method: :run,
95
- )
96
-
97
- The call to `Child.new` can take arbitrary arguments, just by defining an array
98
- for the `args` named parameter. Did you know you can define a hash inside an
99
- array like `['foo', 'bar', x: 1, y: 2] => ['foo', 'bar', {:x => 1, :y => 2}]`?
100
- I didn't, either, until I started working on Ultravisor, but you can, and it
101
- works *exactly* like named parameters in method calls.
102
-
103
- You can also add children after the Ultravisor has been set running:
104
-
105
- u = Ultravisor.new
106
-
107
- u.add_child(id: :c1, klass: SomeWorker, method: :run)
108
-
109
- u.run # => starts running an instance of SomeWorker, doesn't return
110
-
111
- # In another thread...
112
- u.add_child(id: :c2, klass: OtherWorker, method: go!)
113
-
114
- # An instance of OtherWorker will be created and set running
115
-
116
- If you add a child to an already-running Ultravisor, that child will immediately be
117
- started running, almost like magic.
118
-
119
-
120
- ### Ordering of Children
121
-
122
- The order in which children are defined is important. When children are (re)started,
123
- they are always started in the order they were defined. When children are stopped,
124
- either because the Ultravisor is shutting down, or because of a [supervision
125
- strategy](#supervision-strategies), they are always stopped in the *reverse* order
126
- of their definition.
127
-
128
- All child specifications passed to {Ultravisor.new} always come first, in the
129
- order they were in the array. Any children defined via calls to
130
- {Ultravisor#add_child} will go next, in the order the `add_child` calls were
131
- made.
132
-
133
-
134
- ## Restarting Children
135
-
136
- One of the fundamental purposes of a supervisor like Ultravisor is that it restarts
137
- children if they crash, on the principle of "fail fast". There's no point failing fast
138
- if things don't get automatically fixed. This is the default behaviour of all
139
- Ultravisor children.
140
-
141
- Controlling how children are restarted is the purpose of the "restart policy",
142
- which is controlled by the `restart` and `restart_policy` named arguments in
143
- the child specification. For example, if you want to create a child that will
144
- only ever be run once, regardless of what happens to it, then use `restart:
145
- :never`:
146
-
147
- u.add_child(
148
- id: :my_one_shot_child,
149
- klass: Child,
150
- method: :run_maybe,
151
- restart: :never
152
- )
153
-
154
- If you want a child which gets restarted if its `method` raises an exception,
155
- but *not* if it runs to completion without error, then use `restart: :on_failure`:
156
-
157
- u.add_child(
158
- id: :my_run_once_child,
159
- klass: Child,
160
- method: :run_once,
161
- restart: :on_failure
162
- )
163
-
164
- ### The Limits of Failure
165
-
166
- While restarting is great in general, you don't particularly want to fill your
167
- logs with an endlessly restarting child -- say, because it doesn't have
168
- permission to access a database. To solve that problem, an Ultravisor will
169
- only attempt to restart a child a certain number of times before giving up and
170
- exiting itself. The parameters of how this works are controlled by the
171
- `restart_policy`, which is itself a hash:
172
-
173
- u.add_child(
174
- id: :my_restartable_child,
175
- klass: Child,
176
- method: :run,
177
- restart_policy: {
178
- period: 5,
179
- retries: 2,
180
- delay: 1,
181
- }
182
- )
183
-
184
- The meaning of each of the `restart_policy` keys is best explained as part
185
- of how Ultravisor restarts children.
186
-
187
- When a child needs to be restarted, Ultravisor first waits a little while
188
- before attempting the restart. The amount of time to wait is specified
189
- by the `delay` value in the `restart_policy`. Then a new instance of the
190
- `class` is instantiated, and the `method` is called on that instance.
191
-
192
- The `period` and `retries` values of the `restart_policy` come into play
193
- when the child exits repeatedly. If a single child needs to be restarted
194
- more than `retries` times in `period` seconds, then instead of trying to
195
- restart again, Ultravisor gives up. It doesn't try to start the child
196
- again, it terminates all the *other* children of the Ultravisor, and
197
- then it exits. Note that the `delay` between restarts is *not* part
198
- of the `period`; only time spent actually running the child is
199
- accounted for.
200
-
201
-
202
- ## Managed Child Termination
203
-
204
- If children need to be terminated, by default, child threads are simply
205
- forcibly terminated by calling {Thread#kill} on them. However, for workers
206
- which hold resources, this can cause problems.
207
-
208
- Thus, it is possible to control both how a child is terminated, and how long
209
- to wait for that termination to occur, by using the `shutdown` named argument
210
- when you add a child (either via {Ultravisor#add_child}, or as part of the
211
- `children` named argument to {Ultravisor.new}), like this:
212
-
213
- u.add_child(
214
- id: :fancy_worker,
215
- shutdown: {
216
- method: :gentle_landing,
217
- timeout: 30
218
- }
219
- )
220
-
221
- When a child with a custom shutdown policy needs to be terminated, the
222
- method named in the `method` key is called on the instance of `class` that
223
- represents that child. Once the shutdown has been signalled to the
224
- worker, up to `timeout` seconds is allowed to elapse. If the child thread has
225
- not terminated by this time, the thread is forcibly terminated by calling
226
- {Thread#kill}. This timeout prevents shutdown or group restart from hanging
227
- indefinitely.
228
-
229
- Note that the `method` specified in the `shutdown` specification should
230
- signal the worker to terminate, and then return immediately. It should
231
- *not* wait for termination itself.
232
-
233
-
234
- ## Supervision Strategies
235
-
236
- When a child needs to be restarted, by default only the child that exited
237
- will be restarted. However, it is possible to cause other
238
- children to be restarted as well, if that is necessary. To do that, you
239
- use the `strategy` named parameter when creating the Ultravisor:
240
-
241
- u = Ultravisor.new(strategy: :one_for_all)
242
-
243
- The possible values for the strategy are:
244
-
245
- * `:one_for_one` -- the default restart strategy, this simply causes the
246
- child which exited to be started again, in line with its restart policy.
247
-
248
- * `:all_for_one` -- if any child needs to be restarted, all children of the
249
- Ultravisor get terminated in reverse of their start order, and then all
250
- children are started again, except those which are `restart: :never`, or
251
- `restart: :on_failure` which had not already exited without error.
252
-
253
- * `:rest_for_one` -- if any child needs to be restarted, all children of
254
- the Ultravisor which are *after* the restarted child get terminated
255
- in reverse of their start order, and then all children are started again,
256
- except those which are `restart: :never`, or `restart: :on_failure` which
257
- had not already exited without error.
258
-
259
-
260
- ## Interacting With Child Objects
261
-
262
- Since the Ultravisor is creating the object instances that run in the worker
263
- threads, you don't automatically have access to the object instance itself.
264
- This is somewhat by design -- concurrency bugs are hell. However, there *are*
265
- ways around this, if you need to.
266
-
267
-
268
- ### The power of cast / call
269
-
270
- A common approach for interacting with an object in an otherwise concurrent
271
- environment is the `cast` / `call` pattern. From the outside, the interface
272
- is quite straightforward:
273
-
274
- ```
275
- u = Ultravisor.new(children: [
276
- { id: :castcall, klass: CastCall, method: :run, enable_castcall: true }
277
- ])
278
-
279
- # This will return `nil` immediately
280
- u[:castcall].cast.some_method
281
-
282
- # This will, at some point in the future, return whatever `CastCall#to_s` could
283
- u[:castcall].call.some_method
284
- ```
285
-
286
- To enable `cast` / `call` support for a child, you must set the `enable_castcall`
287
- keyword argument on the child. This is because failing to process `cast`s and
288
- `call`s can cause all sorts of unpleasant backlogs, so children who intend to
289
- receive (and process) `cast`s and `call`s must explicitly opt-in.
290
-
291
- The interface to the object from outside is straightforward. You get a
292
- reference to the instance of {Ultravisor::Child} for the child you want to talk
293
- to (which is returned by {Ultravisor#add_child}, or {Ultravisor#[]}), and then
294
- call `child.cast.<method>` or `child.call.<method>`, passing in arguments as
295
- per normal. Any public method can be the target of the `cast` or `call`, and you
296
- can pass in any arguments you like, *including blocks* (although bear in mind that
297
- any blocks passed will be run in the child instance's thread, and many
298
- concurrency dragons await the unwary).
299
-
300
- The difference between the `cast` and `call` methods is in whether or not a
301
- return value is expected, and hence when the method call chained through
302
- `cast` or `call` returns.
303
-
304
- When you call `cast`, the real method call gets queued for later execution,
305
- and since no return value is expected, the `child.cast.<method>` returns
306
- `nil` immediately and your code gets on with its day. This is useful
307
- when you want to tell the worker something, or instruct it to do something,
308
- but there's no value coming back.
309
-
310
- In comparison, when you call `call`, the real method call still gets queued,
311
- but the calling code blocks, waiting for the return value from the queued
312
- method call. This may seem pointless -- why have concurrency that blocks? --
313
- but the value comes from the synchronisation. The method call only happens
314
- when the worker loop calls `process_castcall`, which it can do at a time that
315
- suits it, and when it knows that nothing else is going on that could cause
316
- problems.
317
-
318
- One thing to be aware of when interacting with a worker instance is that it may
319
- crash, and be restarted by the Ultravisor, before it gets around to processing
320
- a queued message. If you used `child.cast`, then the method call is just...
321
- lost, forever. On the other hand, if you used `child.call`, then an
322
- {Ultravisor::ChildRestartedError} exception will be raised, which you can deal
323
- with as you see fit.
324
-
325
- The really interesting part is what happens *inside* the child instance. The
326
- actual execution of code in response to the method calls passed through `cast`
327
- and `call` will only happen when the running instance of the child's class
328
- calls `process_castcall`. When that happens, all pending casts and calls will
329
- be executed. Since this happens within the same thread as the rest of the
330
- child instance's code, it's a lot safer than trying to synchronise everything
331
- with locks.
332
-
333
- You can, of course, just call `process_castcall` repeatedly, however that's a
334
- somewhat herp-a-derp way of doing it. The `castcall_fd` method in the running
335
- instance will return an IO object which will become readable whenever there is
336
- a pending `cast` or `call` to process. Thus, if you're using `IO.select` or
337
- similar to wait for work to do, you can add `castcall_fd` to the readable set
338
- and only call `process_castcall` when the relevant IO object comes back. Don't
339
- actually try *reading* from it yourself; `process_castcall` takes care of all that.
340
-
341
- If you happen to have a child class whose *only* purpose is to process `cast`s
342
- and `call`s, you should configure the Ultravisor to use `process_castcall_loop`
343
- as its entry method. This is a wrapper method which blocks on `castcall_fd`
344
- becoming readable, and loops infinitely.
345
-
346
- It is important to remember that not all concurrency bugs can be prevented by
347
- using `cast` / `call`. For example, read-modify-write operations will still
348
- cause all the same problems they always do, so if you find yourself calling
349
- `child.call`, modifying the value returned, and then calling `child.cast`
350
- with that modified value, you're in for a bad time.
351
-
352
-
353
- ### Direct (Unsafe) Instance Access
354
-
355
- If you have a worker class which you're *really* sure is safe against concurrent
356
- access, you can eschew the convenience and safety of `cast` / `call`, and instead
357
- allow direct access to the worker instance object.
358
-
359
- To do this, specify `access: :unsafe` in the child specification, and then
360
- call `child.unsafe_instance` to get the instance object currently in play.
361
-
362
- Yes, the multiple mentions of `unsafe` are there deliberately, and no, I won't
363
- be removing them. They're there to remind you, always, that what you're doing
364
- is unsafe.
365
-
366
- If the child is restarting at the time `child.unsafe_instance` is called,
367
- the call will block until the child worker is started again, after which
368
- you'll get the newly created worker instance object. The worker could crash
369
- again at any time, of course, leaving you with a now out-of-date object
370
- that is no longer being actively run. It's up to you to figure out how to
371
- deal with that. If the Ultravisor associated with the child
372
- has terminated, your call to `child.unsafe_instance` will raise an
373
- {Ultravisor::ChildRestartedError}.
374
-
375
- Why yes, Gracie, there *are* a lot of things that can go wrong when using
376
- direct instance object access. Still wondering why those `unsafe`s are in
377
- the name?
378
-
379
-
380
- ## Supervision Trees
381
-
382
- Whilst a collection of workers is a neat thing to have, more powerful systems
383
- can be constructed if supervisors can, themselves, be supervised. Primarily
384
- this is useful when recovering from persistent errors, because you can use
385
- a higher-level supervisor to restart an entire tree of workers which has one
386
- which is having problems.
387
-
388
- Creating a supervision tree is straightforward. Because Ultravisor works by
389
- instantiating plain old ruby objects, and Ultravisor is, itself, a plain old
390
- ruby class, you use it more-or-less like you would any other object:
391
-
392
- u = Ultravisor.new
393
- u.add_child(id: :sub_sup, klass: Ultravisor, method: :run, args: [children: [...]])
394
-
395
- That's all there is to it. Whenever the parent Ultravisor wants to work on the
396
- child Ultravisor, it treats it like any other child, asking it to terminate,
397
- start, etc, and the child Ultravisor's work consists of terminating, starting,
398
- etc all of its children.
399
-
400
- The only difference in default behaviour between a regular worker child and an
401
- Ultravisor child is that an Ultravisor's `shutdown` policy is automatically set
402
- to `method: :stop!, timeout: :infinity`. This is because it is *very* bad news
403
- to forcibly terminate an Ultravisor before its children have stopped -- all
404
- those children just get cast into the VM, never to be heard from again.
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
- class Ultravisor::Child::Call
3
- attr_reader :method_name
4
-
5
- def initialize(method_name, args, blk, rv_q, rv_fail)
6
- @method_name, @args, @blk, @rv_q, @rv_fail = method_name, args, blk, rv_q, rv_fail
7
- end
8
-
9
- def go!(receiver)
10
- @rv_q << receiver.__send__(@method_name, *@args, &@blk)
11
- rescue Exception => ex
12
- @rv_q << @rv_fail
13
- raise
14
- ensure
15
- @rv_q.close
16
- end
17
-
18
- def child_restarted!
19
- @rv_q << @rv_fail
20
- end
21
- end