service_skeleton 1.0.1 → 2.0.1

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.git-blame-ignore-revs +2 -0
  3. data/.github/workflows/ci.yml +50 -0
  4. data/.gitignore +0 -7
  5. data/.rubocop.yml +11 -1
  6. data/README.md +1 -53
  7. data/lib/service_skeleton/config.rb +20 -13
  8. data/lib/service_skeleton/generator.rb +4 -4
  9. data/lib/service_skeleton/runner.rb +3 -3
  10. data/lib/service_skeleton/ultravisor_children.rb +2 -1
  11. data/lib/service_skeleton/ultravisor_loggerstash.rb +9 -1
  12. data/service_skeleton.gemspec +4 -14
  13. data/ultravisor/.yardopts +1 -0
  14. data/ultravisor/Guardfile +9 -0
  15. data/ultravisor/README.md +404 -0
  16. data/ultravisor/lib/ultravisor.rb +216 -0
  17. data/ultravisor/lib/ultravisor/child.rb +485 -0
  18. data/ultravisor/lib/ultravisor/child/call.rb +21 -0
  19. data/ultravisor/lib/ultravisor/child/call_receiver.rb +14 -0
  20. data/ultravisor/lib/ultravisor/child/cast.rb +16 -0
  21. data/ultravisor/lib/ultravisor/child/cast_receiver.rb +11 -0
  22. data/ultravisor/lib/ultravisor/child/process_cast_call.rb +39 -0
  23. data/ultravisor/lib/ultravisor/error.rb +25 -0
  24. data/ultravisor/lib/ultravisor/logging_helpers.rb +32 -0
  25. data/ultravisor/spec/example_group_methods.rb +19 -0
  26. data/ultravisor/spec/example_methods.rb +8 -0
  27. data/ultravisor/spec/spec_helper.rb +56 -0
  28. data/ultravisor/spec/ultravisor/add_child_spec.rb +79 -0
  29. data/ultravisor/spec/ultravisor/child/call_spec.rb +121 -0
  30. data/ultravisor/spec/ultravisor/child/cast_spec.rb +111 -0
  31. data/ultravisor/spec/ultravisor/child/id_spec.rb +21 -0
  32. data/ultravisor/spec/ultravisor/child/new_spec.rb +152 -0
  33. data/ultravisor/spec/ultravisor/child/restart_delay_spec.rb +40 -0
  34. data/ultravisor/spec/ultravisor/child/restart_spec.rb +70 -0
  35. data/ultravisor/spec/ultravisor/child/run_spec.rb +95 -0
  36. data/ultravisor/spec/ultravisor/child/shutdown_spec.rb +124 -0
  37. data/ultravisor/spec/ultravisor/child/spawn_spec.rb +216 -0
  38. data/ultravisor/spec/ultravisor/child/unsafe_instance_spec.rb +55 -0
  39. data/ultravisor/spec/ultravisor/child/wait_spec.rb +32 -0
  40. data/ultravisor/spec/ultravisor/new_spec.rb +71 -0
  41. data/ultravisor/spec/ultravisor/remove_child_spec.rb +49 -0
  42. data/ultravisor/spec/ultravisor/run_spec.rb +334 -0
  43. data/ultravisor/spec/ultravisor/shutdown_spec.rb +106 -0
  44. metadata +48 -64
  45. data/.travis.yml +0 -11
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../spec_helper"
4
+
5
+ require_relative "../../../lib/ultravisor/child"
6
+ require_relative "../../../lib/ultravisor/error"
7
+
8
+ describe Ultravisor::Child do
9
+ uses_logger
10
+
11
+ let(:base_args) { { id: :bob, klass: mock_class, method: :run } }
12
+ let(:child) { Ultravisor::Child.new(**args) }
13
+ let(:mock_class) { Class.new.tap { |k| k.class_eval { def run; end; def sigterm; end } } }
14
+ let(:mock_instance) { instance_double(mock_class) }
15
+ let(:term_queue) { instance_double(Queue) }
16
+
17
+ describe "#shutdown" do
18
+ let(:args) { base_args }
19
+
20
+ context "when the child isn't running" do
21
+ it "returns immediately" do
22
+ child.shutdown
23
+ end
24
+ end
25
+
26
+ context "when the child is running" do
27
+ before(:each) do
28
+ orig_thread_new = Thread.method(:new)
29
+ allow(Thread).to receive(:new) do |&b|
30
+ orig_thread_new.call(&b).tap do |th|
31
+ @thread = th
32
+ allow(th).to receive(:kill).and_call_original
33
+ allow(th).to receive(:join).and_call_original
34
+ end
35
+ end
36
+
37
+ allow(mock_class).to receive(:new).and_return(mock_instance)
38
+ allow(mock_instance).to receive(:run)
39
+
40
+ allow(term_queue).to receive(:<<)
41
+ end
42
+
43
+ it "kills the thread" do
44
+ child.spawn(term_queue).shutdown
45
+
46
+ expect(@thread).to have_received(:kill)
47
+ end
48
+
49
+ it "waits for the thread to be done" do
50
+ child.spawn(term_queue).shutdown
51
+
52
+ expect(@thread).to have_received(:join).with(1)
53
+ end
54
+
55
+ it "doesn't put anything on the queue" do
56
+ expect(term_queue).to_not receive(:<<)
57
+
58
+ child.spawn(term_queue).shutdown
59
+ end
60
+
61
+ context "when there's a shutdown spec" do
62
+ let(:args) { base_args.merge(shutdown: { method: :sigterm, timeout: 0.05 }) }
63
+
64
+ before(:each) do
65
+ allow(mock_instance).to receive(:sigterm)
66
+ end
67
+
68
+ it "calls the specified shutdown method" do
69
+ expect(mock_instance).to receive(:sigterm)
70
+
71
+ child.spawn(term_queue).shutdown
72
+ end
73
+
74
+ it "waits for up to the timeout period" do
75
+ child.spawn(term_queue).shutdown
76
+
77
+ expect(@thread).to have_received(:join).with(0.05)
78
+ end
79
+
80
+ context "the worker doesn't finish quickly enough" do
81
+ before(:each) do
82
+ allow(mock_instance).to receive(:run) { sleep 15 }
83
+ end
84
+
85
+ it "kills the thread" do
86
+ child.spawn(term_queue).shutdown
87
+
88
+ expect(@thread).to have_received(:kill)
89
+ end
90
+ end
91
+ end
92
+
93
+ context "when the thread infinihangs" do
94
+ # No need for a big timeout, we know it's not going to succeed
95
+ let(:args) { base_args.merge(shutdown: { timeout: 0.000001 }) }
96
+ let(:m) { Mutex.new }
97
+ let(:cv) { ConditionVariable.new }
98
+
99
+ before(:each) do
100
+ allow(mock_instance).to receive(:run) do
101
+ Thread.handle_interrupt(Numeric => :never) do
102
+ m.synchronize do
103
+ @state = 1
104
+ cv.signal
105
+ cv.wait(m) until @state == 2
106
+ end
107
+ end
108
+ end
109
+
110
+ allow(logger).to receive(:error)
111
+ end
112
+
113
+ it "logs an error" do
114
+ expect(logger).to receive(:error)
115
+
116
+ child.spawn(term_queue)
117
+ m.synchronize { cv.wait(m) until @state == 1 }
118
+ child.shutdown
119
+ m.synchronize { @state = 2; cv.signal }
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../spec_helper"
4
+
5
+ require_relative "../../../lib/ultravisor/child"
6
+ require_relative "../../../lib/ultravisor/error"
7
+
8
+ describe Ultravisor::Child do
9
+ let(:child) { Ultravisor::Child.new(**args) }
10
+ let(:mock_class) { Class.new.tap { |k| k.class_eval { def run; end } } }
11
+ let(:mock_instance) { double(mock_class) } # rubocop:disable RSpec/VerifiedDoubles
12
+ let(:term_queue) { instance_double(Queue) }
13
+
14
+ describe "#spawn" do
15
+ before(:each) do
16
+ allow(term_queue).to receive(:<<)
17
+ end
18
+
19
+ context "with minimal arguments" do
20
+ let(:args) { { id: :bob, klass: mock_class, method: :run } }
21
+
22
+ before(:each) do
23
+ allow(mock_class).to receive(:new).and_return(mock_instance)
24
+ allow(mock_instance).to receive(:run)
25
+ end
26
+
27
+ it "instantiates the class" do
28
+ expect(mock_class).to receive(:new).with(no_args)
29
+
30
+ child.spawn(term_queue).wait
31
+ end
32
+
33
+ it "calls the run method on the class instance" do
34
+ expect(mock_instance).to receive(:run).with(no_args)
35
+
36
+ child.spawn(term_queue).wait
37
+ end
38
+
39
+ it "registers the thread it is running in" do
40
+ expect(mock_instance).to receive(:run) do
41
+ expect(child.instance_variable_get(:@thread)).to eq(Thread.current)
42
+ end
43
+
44
+ child.spawn(term_queue).wait
45
+ end
46
+
47
+ it "notes the start time" do
48
+ expect(mock_instance).to receive(:run) do
49
+ # Can only check @start_time while the child is running, as the
50
+ # variable gets nil'd after the run completes
51
+ expect(child.instance_variable_get(:@start_time).to_f).to be_within(0.01).of(Time.now.to_f)
52
+ end
53
+
54
+ child.spawn(term_queue).wait
55
+ end
56
+
57
+ it "notes the termination value" do
58
+ expect(mock_instance).to receive(:run).with(no_args).and_return(42)
59
+
60
+ child.spawn(term_queue)
61
+
62
+ expect(child.termination_value).to eq(42)
63
+ end
64
+
65
+ it "tells the ultravisor it terminated" do
66
+ expect(term_queue).to receive(:<<).with(child)
67
+
68
+ child.spawn(term_queue).wait
69
+ end
70
+
71
+ it "creates a new thread" do
72
+ expect(Thread).to receive(:new)
73
+
74
+ child.spawn(term_queue).wait
75
+ end
76
+
77
+ context "when the worker object's run method raises an exception" do
78
+ before(:each) do
79
+ allow(mock_instance).to receive(:run).and_raise(RuntimeError.new("FWACKOOM"))
80
+ end
81
+
82
+ it "makes a note of the exception" do
83
+ child.spawn(term_queue)
84
+
85
+ expect(child.termination_exception).to be_a(RuntimeError)
86
+ end
87
+
88
+ it "tells the ultravisor it terminated" do
89
+ expect(term_queue).to receive(:<<).with(child)
90
+
91
+ child.spawn(term_queue).wait
92
+ end
93
+ end
94
+ end
95
+
96
+ context "with a worker class that takes only non-keyword args" do
97
+ let(:args) { { id: :testy, klass: mock_class, args: ["foo", "bar"], method: :run } }
98
+ let(:mock_class) do
99
+ Class.new do
100
+ def initialize(foo, bar)
101
+ end
102
+
103
+ def run
104
+ end
105
+ end
106
+ end
107
+
108
+ it "creates the class instance with args" do
109
+ allow(mock_class).to receive(:new).and_call_original
110
+
111
+ child.spawn(term_queue).wait
112
+ expect(mock_class).to have_received(:new).with("foo", "bar")
113
+ end
114
+ end
115
+
116
+ context "with a worker class that takes non-keyword and optional args" do
117
+ let(:args) { { id: :testy, klass: mock_class, args: ["foo", "bar", baz: "wombat"], method: :run } }
118
+ let(:mock_class) do
119
+ Class.new do
120
+ def initialize(foo, bar, baz = {})
121
+ end
122
+
123
+ def run
124
+ end
125
+ end
126
+ end
127
+
128
+ it "creates the class instance with args" do
129
+ allow(mock_class).to receive(:new).and_call_original
130
+
131
+ child.spawn(term_queue).wait
132
+ expect(mock_class).to have_received(:new).with("foo", "bar", { baz: "wombat" })
133
+ end
134
+ end
135
+
136
+ context "with a worker class that takes mixed args" do
137
+ let(:args) { { id: :testy, klass: mock_class, args: ["foo", "bar", baz: "wombat"], method: :run } }
138
+ let(:mock_class) do
139
+ Class.new do
140
+ def initialize(foo, bar, baz:)
141
+ end
142
+
143
+ def run
144
+ end
145
+ end
146
+ end
147
+
148
+ it "creates the class instance with args" do
149
+ allow(mock_class).to receive(:new).and_call_original
150
+
151
+ child.spawn(term_queue).wait
152
+ expect(mock_class).to have_received(:new).with("foo", "bar", baz: "wombat")
153
+ end
154
+ end
155
+
156
+ context "with a worker class that takes keyword args" do
157
+ let(:args) { { id: :testy, klass: mock_class, args: [foo: "bar", baz: "wombat"], method: :run } }
158
+ let(:mock_class) do
159
+ Class.new do
160
+ def initialize(foo:, baz:)
161
+ end
162
+
163
+ def run
164
+ end
165
+ end
166
+ end
167
+
168
+ it "creates the class instance with args" do
169
+ allow(mock_class).to receive(:new).and_call_original
170
+
171
+ child.spawn(term_queue).wait
172
+ expect(mock_class).to have_received(:new).with(foo: "bar", baz: "wombat")
173
+ end
174
+ end
175
+
176
+ context "with a worker class that takes optional args" do
177
+ let(:args) { { id: :testy, klass: mock_class, args: [foo: "bar", baz: "wombat", fib: "wib", woop: "woob"], method: :run } }
178
+ let(:mock_class) do
179
+ Class.new do
180
+ def initialize(foo:, baz:, fib: "none", woop: nil)
181
+ end
182
+
183
+ def run
184
+ end
185
+ end
186
+ end
187
+
188
+ it "creates the class instance with args" do
189
+ allow(mock_class).to receive(:new).and_call_original
190
+ child.spawn(term_queue).wait
191
+
192
+ expect(mock_class).to have_received(:new).with(foo: "bar", baz: "wombat", fib: "wib", woop: "woob")
193
+ end
194
+ end
195
+
196
+ context "with a worker class that takes keyword args in form of a hash" do
197
+ let(:args) { { id: :testy, klass: mock_class, args: { foo: "bar", baz: "wombat" }, method: :run } }
198
+ let(:mock_class) do
199
+ Class.new do
200
+ def initialize(foo:, baz:)
201
+ end
202
+
203
+ def run
204
+ end
205
+ end
206
+ end
207
+
208
+ it "creates the class instance with args" do
209
+ allow(mock_class).to receive(:new).and_call_original
210
+ child.spawn(term_queue).wait
211
+
212
+ expect(mock_class).to have_received(:new).with(foo: "bar", baz: "wombat")
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../spec_helper"
4
+
5
+ require_relative "../../../lib/ultravisor/child"
6
+ require_relative "../../../lib/ultravisor/error"
7
+
8
+ describe Ultravisor::Child do
9
+ let(:args) { { id: :bob, klass: Object, method: :to_s } }
10
+ let(:child) { Ultravisor::Child.new(**args) }
11
+
12
+ describe "#unsafe_instance" do
13
+ context "by default" do
14
+ it "explodes" do
15
+ expect { child.unsafe_instance }.to raise_error(Ultravisor::ThreadSafetyError)
16
+ end
17
+ end
18
+
19
+ context "with access: :unsafe" do
20
+ let(:args) { { id: :bob, klass: Object, method: :to_s, access: :unsafe } }
21
+
22
+ context "when there's no instance object" do
23
+ it "waits for the instance object to appear" do
24
+ expect(child.instance_variable_get(:@spawn_cv)).to receive(:wait) do
25
+ child.instance_variable_set(:@instance, "gogogo")
26
+ end
27
+
28
+ child.unsafe_instance
29
+ end
30
+ end
31
+
32
+ context "when there's an instance object" do
33
+ before(:each) do
34
+ child.instance_variable_set(:@instance, "bob")
35
+ end
36
+
37
+ it "returns the instance object" do
38
+ expect(child.unsafe_instance).to eq("bob")
39
+ end
40
+ end
41
+ end
42
+
43
+ context "when the child is running" do
44
+ it "only exits once the child has finished" do
45
+ child.instance_variable_set(:@thread, Thread.new {})
46
+
47
+ expect(child.instance_variable_get(:@spawn_cv)).to receive(:wait).with(child.instance_variable_get(:@spawn_m)) do
48
+ child.instance_variable_set(:@thread, nil)
49
+ end
50
+
51
+ child.wait
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../spec_helper"
4
+
5
+ require_relative "../../../lib/ultravisor/child"
6
+ require_relative "../../../lib/ultravisor/error"
7
+
8
+ describe Ultravisor::Child do
9
+ let(:args) { { id: :bob, klass: mock_class, method: :run } }
10
+ let(:child) { Ultravisor::Child.new(**args) }
11
+ let(:mock_class) { Class.new.tap { |k| k.class_eval { def run; end } } }
12
+
13
+ describe "#wait" do
14
+ context "when the child isn't running" do
15
+ it "just returns straight away" do
16
+ child.wait
17
+ end
18
+ end
19
+
20
+ context "when the child is running" do
21
+ it "only exits once the child has finished" do
22
+ child.instance_variable_set(:@thread, Thread.new {})
23
+
24
+ expect(child.instance_variable_get(:@spawn_cv)).to receive(:wait).with(child.instance_variable_get(:@spawn_m)) do
25
+ child.instance_variable_set(:@thread, nil)
26
+ end
27
+
28
+ child.wait
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../spec_helper"
3
+
4
+ require_relative "../../lib/ultravisor"
5
+
6
+ describe Ultravisor do
7
+ describe ".new" do
8
+ context "without arguments" do
9
+ it "does not explode" do
10
+ expect { Ultravisor.new }.to_not raise_error
11
+ end
12
+
13
+ it "gives us an Ultravisor instance" do
14
+ expect(Ultravisor.new).to be_a(Ultravisor)
15
+ end
16
+ end
17
+
18
+ context "with empty children" do
19
+ it "does not explode" do
20
+ expect { Ultravisor.new children: [] }.to_not raise_error
21
+ end
22
+ end
23
+
24
+ context "with children that isn't an array" do
25
+ it "raises an error" do
26
+ [{}, "ohai!", nil, 42].each do |v|
27
+ expect { Ultravisor.new children: v }.to raise_error(ArgumentError)
28
+ end
29
+ end
30
+ end
31
+
32
+ context "with valid children" do
33
+ let(:ultravisor) { Ultravisor.new(children: [{ id: :testy, klass: Object, method: :to_s }]) }
34
+
35
+ it "registers the child by its ID" do
36
+ expect(ultravisor[:testy]).to be_a(Ultravisor::Child)
37
+ end
38
+ end
39
+
40
+ context "with two children with the same ID" do
41
+ it "explodes" do
42
+ expect do
43
+ Ultravisor.new(
44
+ children: [
45
+ { id: :testy, klass: Object, method: :to_s },
46
+ { id: :testy, klass: Class, method: :to_s },
47
+ ]
48
+ )
49
+ end.to raise_error(Ultravisor::DuplicateChildError)
50
+ end
51
+ end
52
+
53
+ context "with a valid strategy" do
54
+ it "does not explode" do
55
+ expect { Ultravisor.new strategy: :all_for_one }.to_not raise_error
56
+ end
57
+ end
58
+
59
+ [
60
+ { strategy: :bob },
61
+ { strategy: "all_for_one" },
62
+ { strategy: ["games"] },
63
+ ].each do |s|
64
+ context "with invalid strategy #{s.inspect}" do
65
+ it "explodes" do
66
+ expect { Ultravisor.new **s }.to raise_error(ArgumentError)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end