service_skeleton 1.0.3 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.git-blame-ignore-revs +2 -0
  3. data/.github/workflows/ci.yml +16 -0
  4. data/.rubocop.yml +2 -0
  5. data/lib/service_skeleton/generator.rb +1 -1
  6. data/lib/service_skeleton/runner.rb +1 -1
  7. data/service_skeleton.gemspec +0 -1
  8. data/ultravisor/.yardopts +1 -0
  9. data/ultravisor/Guardfile +9 -0
  10. data/ultravisor/README.md +404 -0
  11. data/ultravisor/lib/ultravisor.rb +216 -0
  12. data/ultravisor/lib/ultravisor/child.rb +481 -0
  13. data/ultravisor/lib/ultravisor/child/call.rb +21 -0
  14. data/ultravisor/lib/ultravisor/child/call_receiver.rb +14 -0
  15. data/ultravisor/lib/ultravisor/child/cast.rb +16 -0
  16. data/ultravisor/lib/ultravisor/child/cast_receiver.rb +11 -0
  17. data/ultravisor/lib/ultravisor/child/process_cast_call.rb +39 -0
  18. data/ultravisor/lib/ultravisor/error.rb +25 -0
  19. data/ultravisor/lib/ultravisor/logging_helpers.rb +32 -0
  20. data/ultravisor/spec/example_group_methods.rb +19 -0
  21. data/ultravisor/spec/example_methods.rb +8 -0
  22. data/ultravisor/spec/spec_helper.rb +52 -0
  23. data/ultravisor/spec/ultravisor/add_child_spec.rb +79 -0
  24. data/ultravisor/spec/ultravisor/child/call_spec.rb +121 -0
  25. data/ultravisor/spec/ultravisor/child/cast_spec.rb +111 -0
  26. data/ultravisor/spec/ultravisor/child/id_spec.rb +21 -0
  27. data/ultravisor/spec/ultravisor/child/new_spec.rb +152 -0
  28. data/ultravisor/spec/ultravisor/child/restart_delay_spec.rb +40 -0
  29. data/ultravisor/spec/ultravisor/child/restart_spec.rb +70 -0
  30. data/ultravisor/spec/ultravisor/child/run_spec.rb +95 -0
  31. data/ultravisor/spec/ultravisor/child/shutdown_spec.rb +124 -0
  32. data/ultravisor/spec/ultravisor/child/spawn_spec.rb +107 -0
  33. data/ultravisor/spec/ultravisor/child/unsafe_instance_spec.rb +55 -0
  34. data/ultravisor/spec/ultravisor/child/wait_spec.rb +32 -0
  35. data/ultravisor/spec/ultravisor/new_spec.rb +71 -0
  36. data/ultravisor/spec/ultravisor/remove_child_spec.rb +49 -0
  37. data/ultravisor/spec/ultravisor/run_spec.rb +334 -0
  38. data/ultravisor/spec/ultravisor/shutdown_spec.rb +106 -0
  39. metadata +34 -16
@@ -0,0 +1,111 @@
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
+ class CastCallTest
9
+ def run
10
+ end
11
+
12
+ def poke_processor
13
+ process_castcall
14
+ end
15
+ end
16
+
17
+ describe Ultravisor::Child do
18
+ let(:child) { Ultravisor::Child.new(**args) }
19
+ let(:instance) { child.__send__(:new_instance) }
20
+
21
+ describe "#cast" do
22
+ context "without enable_castcall" do
23
+ let(:args) do
24
+ {
25
+ id: :cast_child,
26
+ klass: CastCallTest,
27
+ method: :run,
28
+ }
29
+ end
30
+
31
+ it "does not accept calls to #cast" do
32
+ expect { child.cast }.to raise_error(NoMethodError)
33
+ end
34
+ end
35
+
36
+ context "with enable_castcall" do
37
+ let(:args) do
38
+ {
39
+ id: :cast_child,
40
+ klass: CastCallTest,
41
+ method: :run,
42
+ enable_castcall: true,
43
+ }
44
+ end
45
+
46
+ before(:each) do
47
+ # Got to have an instance otherwise all hell breaks loose
48
+ child.instance_variable_set(:@instance, instance)
49
+
50
+ # So we can check if and when it's been called
51
+ allow(instance).to receive(:to_s).and_call_original
52
+ end
53
+
54
+ it "accepts calls to #cast" do
55
+ expect { child.cast }.to_not raise_error
56
+ end
57
+
58
+ it "does not accept cast calls to methods that do not exist on the worker object" do
59
+ expect { child.cast.flibbetygibbets }.to raise_error(NoMethodError)
60
+ end
61
+
62
+ it "does not accept cast calls to private methods on the worker object" do
63
+ expect { child.cast.eval }.to raise_error(NoMethodError)
64
+ end
65
+
66
+ it "accepts calls to methods that exist on the worker object" do
67
+ expect { child.cast.to_s }.to_not raise_error
68
+ end
69
+
70
+ it "calls the instance method only when process_castcall is called" do
71
+ child.cast.to_s
72
+ expect(instance).to_not have_received(:to_s)
73
+ instance.poke_processor
74
+ expect(instance).to have_received(:to_s)
75
+ end
76
+
77
+ it "processes all the queued method calls" do
78
+ child.cast.to_s
79
+ child.cast.to_s
80
+ child.cast.to_s
81
+ child.cast.to_s
82
+ child.cast.to_s
83
+ expect(instance).to_not have_received(:to_s)
84
+ instance.poke_processor
85
+ expect(instance).to have_received(:to_s).exactly(5).times
86
+ end
87
+
88
+ let(:cc_fd) { instance.__send__(:castcall_fd) }
89
+ it "marks the castcall_fd as readable only after cast is called" do
90
+ expect(IO.select([cc_fd], nil, nil, 0)).to eq(nil)
91
+
92
+ child.cast.to_s
93
+
94
+ expect(IO.select([cc_fd], nil, nil, 0)).to eq([[cc_fd], [], []])
95
+ end
96
+
97
+ it "does not have a readable castcall_fd after process_castcall" do
98
+ child.cast.to_s
99
+ expect(IO.select([cc_fd], nil, nil, 0)).to eq([[cc_fd], [], []])
100
+ instance.poke_processor
101
+ expect(IO.select([cc_fd], nil, nil, 0)).to eq(nil)
102
+ end
103
+
104
+ it "does not explode if the instance is dying" do
105
+ instance.instance_variable_get(:@ultravisor_child_castcall_queue).close
106
+
107
+ expect { child.cast.to_s }.to_not raise_error
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,21 @@
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
+
12
+ describe "#id" do
13
+ context "with minimal arguments" do
14
+ let(:args) { { id: :bob, klass: mock_class, method: :run } }
15
+
16
+ it "returns the child's ID" do
17
+ expect(child.id).to eq(:bob)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,152 @@
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; def sigterm; end } } }
11
+
12
+ describe ".new" do
13
+ context "with minimal arguments" do
14
+ let(:args) { { id: :bob, klass: mock_class, method: :run } }
15
+
16
+ it "does not explode" do
17
+ expect { Ultravisor::Child.new(**args) }.to_not raise_error
18
+ end
19
+
20
+ it "does not instantiate an instance of klass" do
21
+ expect(mock_class).to_not receive(:new)
22
+
23
+ Ultravisor::Child.new(**args)
24
+ end
25
+ end
26
+
27
+ context "with defined args" do
28
+ let(:args) { { id: :bob, klass: mock_class, args: [foo: "bar"], method: :run } }
29
+
30
+ it "explodes" do
31
+ expect { Ultravisor::Child.new(**args) }.to raise_error(Ultravisor::InvalidKAMError)
32
+ end
33
+ end
34
+
35
+ context "with a class that takes args" do
36
+ let(:mock_class) { Class.new.tap { |k| k.class_eval { def initialize(*x); end } } }
37
+ let(:args) { { id: :testy, klass: mock_class, args: [1, 2, 3], method: :to_s } }
38
+
39
+ it "doesn't explode" do
40
+ expect { Ultravisor::Child.new(**args) }.to_not raise_error
41
+ end
42
+ end
43
+
44
+ context "with a non-existent method" do
45
+ let(:args) { { id: :testy, klass: mock_class, method: :gogogo } }
46
+
47
+ it "explodes" do
48
+ expect { Ultravisor::Child.new(**args) }.to raise_error(Ultravisor::InvalidKAMError)
49
+ end
50
+ end
51
+
52
+ context "with a method that takes args" do
53
+ let(:args) { { id: :testy, klass: mock_class, method: :public_send } }
54
+
55
+ it "explodes" do
56
+ expect { Ultravisor::Child.new(**args) }.to raise_error(Ultravisor::InvalidKAMError)
57
+ end
58
+ end
59
+
60
+ context "with a valid restart value" do
61
+ it "is fine" do
62
+ %i{always on_failure never}.each do |v|
63
+ expect { Ultravisor::Child.new(id: :x, klass: Object, method: :to_s, restart: v) }.to_not raise_error
64
+ end
65
+ end
66
+ end
67
+
68
+ context "with an invalid restart value" do
69
+ it "explodes" do
70
+ [:sometimes, "always", 42, { max: 4 }].each do |v|
71
+ expect { Ultravisor::Child.new(id: :x, klass: Object, method: :to_s, restart: v) }.to raise_error(ArgumentError)
72
+ end
73
+ end
74
+ end
75
+
76
+ context "with a valid restart_policy" do
77
+ it "is happy" do
78
+ expect do
79
+ Ultravisor::Child.new(id: :rp, klass: Object, method: :to_s, restart_policy: { period: 5, max: 2, delay: 1 })
80
+ end.to_not raise_error
81
+ end
82
+ end
83
+
84
+ [
85
+ { when: :never },
86
+ { period: -1 },
87
+ { period: "never" },
88
+ { period: :sometimes },
89
+ { period: (0..10) },
90
+ { max: -1 },
91
+ { max: "powers" },
92
+ { max: (1..5) },
93
+ { delay: -1 },
94
+ { delay: "buses" },
95
+ { delay: (-1..3) },
96
+ { delay: (3..1) },
97
+ "whenever you're ready",
98
+ ].each do |p|
99
+ it "explodes with invalid restart_policy #{p.inspect}" do
100
+ expect do
101
+ Ultravisor::Child.new(id: :boom, klass: Object, method: :to_s, restart_policy: p)
102
+ end.to raise_error(ArgumentError)
103
+ end
104
+ end
105
+
106
+ context "with a valid shutdown spec" do
107
+ it "is happy" do
108
+ expect do
109
+ Ultravisor::Child.new(id: :rp, klass: mock_class, method: :to_s, shutdown: { method: :sigterm, timeout: 2 })
110
+ end.to_not raise_error
111
+ end
112
+ end
113
+
114
+ [
115
+ { method: "man" },
116
+ { method: :send },
117
+ { method: :nonexistent_method },
118
+ { timeout: -4 },
119
+ { timeout: (3..5) },
120
+ { timeout: "MC Hammer" },
121
+ { woogiewoogie: "boo!" },
122
+ "freddie!",
123
+ ].each do |s|
124
+ it "explodes with invalid shutdown spec #{s.inspect}" do
125
+ expect do
126
+ Ultravisor::Child.new(id: :boom, klass: Object, method: :to_s, shutdown: s)
127
+ end.to raise_error(ArgumentError)
128
+ end
129
+ end
130
+
131
+ context "with castcall enabled" do
132
+ it "is happy" do
133
+ expect do
134
+ Ultravisor::Child.new(id: :castcall, klass: Object, method: :to_s, enable_castcall: true)
135
+ end.to_not raise_error
136
+ end
137
+ end
138
+
139
+ [
140
+ :bob,
141
+ 42,
142
+ "unsafe",
143
+ { safe: :un },
144
+ ].each do |a|
145
+ it "explodes with invalid access spec #{a.inspect}" do
146
+ expect do
147
+ Ultravisor::Child.new(id: :boom, klass: Object, method: :to_s, access: a)
148
+ end.to raise_error(ArgumentError)
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../spec_helper"
4
+
5
+ require_relative "../../../lib/ultravisor/child"
6
+
7
+ describe Ultravisor::Child do
8
+ let(:base_args) { { id: :bob, klass: mock_class, method: :run } }
9
+ let(:child) { Ultravisor::Child.new(**args) }
10
+ let(:mock_class) { Class.new.tap { |k| k.class_eval { def run; end } } }
11
+
12
+ describe "#restart_delay" do
13
+ context "by default" do
14
+ let(:args) { base_args }
15
+
16
+ it "returns the default delay" do
17
+ expect(child.restart_delay).to eq(1)
18
+ end
19
+ end
20
+
21
+ context "with a specified numeric delay" do
22
+ let(:args) { base_args.merge(restart_policy: { delay: 3.14159 }) }
23
+
24
+ it "returns the specified delay" do
25
+ expect(child.restart_delay).to be_within(0.00001).of(3.14159)
26
+ end
27
+ end
28
+
29
+ context "with a delay range" do
30
+ let(:args) { base_args.merge(restart_policy: { delay: 2..5 }) }
31
+
32
+ it "returns a delay in the given range" do
33
+ delays = 10.times.map { child.restart_delay }
34
+
35
+ expect(delays.all? { |d| (2..5).include?(d) }).to be(true)
36
+ expect(delays.uniq.length).to eq(10)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,70 @@
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(:base_args) { { id: :bob, klass: Object, method: :to_s } }
10
+ let(:child) { Ultravisor::Child.new(**args) }
11
+
12
+ describe "#restart?" do
13
+ context "when restart: :always" do
14
+ # Yes, this is the default, but I do like to be explicit
15
+ let(:args) { base_args.merge(restart: :always) }
16
+
17
+ it "is true always" do
18
+ expect(child.restart?).to be(true)
19
+ end
20
+ end
21
+
22
+ context "when restart: :never" do
23
+ let(:args) { base_args.merge(restart: :never) }
24
+
25
+ it "is false always" do
26
+ expect(child.restart?).to be(false)
27
+ end
28
+ end
29
+
30
+ context "when restart: :on_failure" do
31
+ let(:args) { base_args.merge(restart: :on_failure) }
32
+
33
+ it "is true if the child terminated with an exception" do
34
+ expect(child).to receive(:termination_exception).and_return(Exception.new("boom"))
35
+
36
+ expect(child.restart?).to be(true)
37
+ end
38
+
39
+ it "is false if the child didn't terminate with an exception" do
40
+ expect(child).to receive(:termination_exception).and_return(nil)
41
+
42
+ expect(child.restart?).to be(false)
43
+ end
44
+ end
45
+
46
+ context "with a restart history that isn't blown" do
47
+ let(:args) { base_args.merge(restart: :always, restart_policy: { period: 10, max: 3 }) }
48
+
49
+ before(:each) do
50
+ child.instance_variable_set(:@runtime_history, [4.99, 4.99, 4.99])
51
+ end
52
+
53
+ it "still returns true" do
54
+ expect(child.restart?).to be(true)
55
+ end
56
+ end
57
+
58
+ context "with a restart history that is blown" do
59
+ let(:args) { base_args.merge(restart: :always, restart_policy: { period: 10, max: 2 }) }
60
+
61
+ before(:each) do
62
+ child.instance_variable_set(:@runtime_history, [4.99, 4.99, 4.99])
63
+ end
64
+
65
+ it "explodes" do
66
+ expect { child.restart? }.to raise_error(Ultravisor::BlownRestartPolicyError)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,95 @@
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 "#run" 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
+ context "when the worker object's run method raises an exception" do
72
+ before(:each) do
73
+ allow(mock_instance).to receive(:run).and_raise(RuntimeError.new("FWACKOOM"))
74
+ end
75
+
76
+ it "makes a note of the exception" do
77
+ child.spawn(term_queue)
78
+
79
+ expect(child.termination_exception).to be_a(RuntimeError)
80
+ end
81
+ end
82
+ end
83
+
84
+ context "with a worker class that takes args" do
85
+ let(:args) { { id: :testy, klass: mock_class, args: ["foo", "bar", baz: "wombat"], method: :run } }
86
+ let(:mock_class) { Class.new.tap { |k| k.class_eval { def initialize(*x); end; def run; end } } }
87
+
88
+ it "creates the class instance with args" do
89
+ expect(mock_class).to receive(:new).with("foo", "bar", baz: "wombat").and_call_original
90
+
91
+ child.spawn(term_queue).wait
92
+ end
93
+ end
94
+ end
95
+ end