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