service_skeleton 1.0.1 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
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