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.
- checksums.yaml +4 -4
- data/.git-blame-ignore-revs +2 -0
- data/.github/workflows/ci.yml +50 -0
- data/.gitignore +0 -7
- data/.rubocop.yml +11 -1
- data/README.md +1 -53
- data/lib/service_skeleton/config.rb +20 -13
- data/lib/service_skeleton/generator.rb +4 -4
- data/lib/service_skeleton/runner.rb +3 -3
- data/lib/service_skeleton/ultravisor_children.rb +2 -1
- data/lib/service_skeleton/ultravisor_loggerstash.rb +9 -1
- data/service_skeleton.gemspec +4 -14
- data/ultravisor/.yardopts +1 -0
- data/ultravisor/Guardfile +9 -0
- data/ultravisor/README.md +404 -0
- data/ultravisor/lib/ultravisor.rb +216 -0
- data/ultravisor/lib/ultravisor/child.rb +485 -0
- data/ultravisor/lib/ultravisor/child/call.rb +21 -0
- data/ultravisor/lib/ultravisor/child/call_receiver.rb +14 -0
- data/ultravisor/lib/ultravisor/child/cast.rb +16 -0
- data/ultravisor/lib/ultravisor/child/cast_receiver.rb +11 -0
- data/ultravisor/lib/ultravisor/child/process_cast_call.rb +39 -0
- data/ultravisor/lib/ultravisor/error.rb +25 -0
- data/ultravisor/lib/ultravisor/logging_helpers.rb +32 -0
- data/ultravisor/spec/example_group_methods.rb +19 -0
- data/ultravisor/spec/example_methods.rb +8 -0
- data/ultravisor/spec/spec_helper.rb +56 -0
- data/ultravisor/spec/ultravisor/add_child_spec.rb +79 -0
- data/ultravisor/spec/ultravisor/child/call_spec.rb +121 -0
- data/ultravisor/spec/ultravisor/child/cast_spec.rb +111 -0
- data/ultravisor/spec/ultravisor/child/id_spec.rb +21 -0
- data/ultravisor/spec/ultravisor/child/new_spec.rb +152 -0
- data/ultravisor/spec/ultravisor/child/restart_delay_spec.rb +40 -0
- data/ultravisor/spec/ultravisor/child/restart_spec.rb +70 -0
- data/ultravisor/spec/ultravisor/child/run_spec.rb +95 -0
- data/ultravisor/spec/ultravisor/child/shutdown_spec.rb +124 -0
- data/ultravisor/spec/ultravisor/child/spawn_spec.rb +216 -0
- data/ultravisor/spec/ultravisor/child/unsafe_instance_spec.rb +55 -0
- data/ultravisor/spec/ultravisor/child/wait_spec.rb +32 -0
- data/ultravisor/spec/ultravisor/new_spec.rb +71 -0
- data/ultravisor/spec/ultravisor/remove_child_spec.rb +49 -0
- data/ultravisor/spec/ultravisor/run_spec.rb +334 -0
- data/ultravisor/spec/ultravisor/shutdown_spec.rb +106 -0
- metadata +48 -64
- data/.travis.yml +0 -11
@@ -0,0 +1,21 @@
|
|
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
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Ultravisor::Child::CallReceiver < BasicObject
|
3
|
+
def initialize(&blk)
|
4
|
+
@blk = blk
|
5
|
+
end
|
6
|
+
|
7
|
+
def method_missing(name, *args, &blk)
|
8
|
+
rv_q = ::Queue.new
|
9
|
+
rv_fail = ::Object.new
|
10
|
+
callback = ::Ultravisor::Child::Call.new(name, args, blk, rv_q, rv_fail)
|
11
|
+
@blk.call(callback)
|
12
|
+
rv_q.pop.tap { |rv| ::Kernel.raise ::Ultravisor::ChildRestartedError.new if rv == rv_fail }
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Ultravisor::Child::Cast
|
3
|
+
attr_reader :method_name
|
4
|
+
|
5
|
+
def initialize(method_name, args, blk)
|
6
|
+
@method_name, @args, @blk = method_name, args, blk
|
7
|
+
end
|
8
|
+
|
9
|
+
def go!(receiver)
|
10
|
+
receiver.__send__(@method_name, *@args, &@blk)
|
11
|
+
end
|
12
|
+
|
13
|
+
def child_restarted!
|
14
|
+
# Meh
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Ultravisor::Child::CastReceiver < BasicObject
|
3
|
+
def initialize(&blk)
|
4
|
+
@blk = blk
|
5
|
+
end
|
6
|
+
|
7
|
+
def method_missing(name, *args, &blk)
|
8
|
+
castback = ::Ultravisor::Child::Cast.new(name, args, blk)
|
9
|
+
@blk.call(castback)
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Ultravisor::Child::ProcessCastCall
|
3
|
+
private
|
4
|
+
|
5
|
+
def process_castcall
|
6
|
+
begin
|
7
|
+
loop do
|
8
|
+
item = @ultravisor_child_castcall_queue.pop(true)
|
9
|
+
|
10
|
+
# Queue has been closed, which is a polite way of saying "we're done here"
|
11
|
+
return if item.nil?
|
12
|
+
|
13
|
+
item.go!(self)
|
14
|
+
|
15
|
+
castcall_fd.getc
|
16
|
+
end
|
17
|
+
rescue ThreadError => ex
|
18
|
+
if ex.message != "queue empty"
|
19
|
+
#:nocov:
|
20
|
+
raise
|
21
|
+
#:nocov:
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def castcall_fd
|
27
|
+
@ultravisor_child_castcall_fd
|
28
|
+
end
|
29
|
+
|
30
|
+
def process_castcall_loop
|
31
|
+
#:nocov:
|
32
|
+
loop do
|
33
|
+
IO.select([castcall_fd], nil, nil)
|
34
|
+
|
35
|
+
process_castcall
|
36
|
+
end
|
37
|
+
#:nocov:
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Ultravisor
|
3
|
+
# Base class of all Ultravisor-specific errors
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
# Tried to register a child with an ID of a child that already exists
|
7
|
+
class DuplicateChildError < Error; end
|
8
|
+
|
9
|
+
# Tried to call `#run` on an ultravisor that is already running
|
10
|
+
class AlreadyRunningError < Error; end
|
11
|
+
|
12
|
+
# A `child.call.<method>` was interrupted by the child instance runner terminating
|
13
|
+
class ChildRestartedError < Error; end
|
14
|
+
|
15
|
+
# Something was wrong with the Klass/Args/Method (KAM) passed
|
16
|
+
class InvalidKAMError < Error; end
|
17
|
+
|
18
|
+
# A child's restart policy was exceeded, and the Ultravisor should
|
19
|
+
# terminate
|
20
|
+
class BlownRestartPolicyError < Error; end
|
21
|
+
|
22
|
+
# An internal programming error (aka "a bug") caused a violation of thread safety
|
23
|
+
# requirements
|
24
|
+
class ThreadSafetyError < Error; end
|
25
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Ultravisor
|
4
|
+
module LoggingHelpers
|
5
|
+
private
|
6
|
+
|
7
|
+
attr_reader :logger
|
8
|
+
|
9
|
+
def log_exception(ex, progname = nil)
|
10
|
+
#:nocov:
|
11
|
+
progname ||= "#{self.class.to_s}##{caller_locations(2, 1).first.label}"
|
12
|
+
|
13
|
+
logger.error(progname) do
|
14
|
+
explanation = if block_given?
|
15
|
+
yield
|
16
|
+
else
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
(["#{explanation}#{explanation ? ": " : ""}#{ex.message} (#{ex.class})"] + ex.backtrace).join("\n ")
|
21
|
+
end
|
22
|
+
#:nocov:
|
23
|
+
end
|
24
|
+
|
25
|
+
def logloc
|
26
|
+
#:nocov:
|
27
|
+
loc = caller_locations.first
|
28
|
+
"#{self.class}##{loc.label}"
|
29
|
+
#:nocov:
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module ExampleGroupMethods
|
6
|
+
def uses_logger
|
7
|
+
let(:logger) { instance_double(Logger, 'mock') }
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
allow(Logger).to receive(:new).and_return(logger)
|
11
|
+
allow(logger).to receive(:debug).with(instance_of(String))
|
12
|
+
allow(logger).to receive(:info).with(instance_of(String))
|
13
|
+
allow(logger).to receive(:error) { |p, &m| puts "#{p}: #{m.call}" }
|
14
|
+
allow(logger).to receive(:level=).with(Logger::INFO)
|
15
|
+
allow(logger).to receive(:formatter=).with(an_instance_of(Proc))
|
16
|
+
allow(logger).to receive(:kind_of?).with(Logger).and_return(true)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'bundler'
|
3
|
+
Bundler.setup(:default, :development)
|
4
|
+
require 'rspec/core'
|
5
|
+
require 'rspec/mocks'
|
6
|
+
|
7
|
+
Thread.report_on_exception = false
|
8
|
+
|
9
|
+
require 'simplecov'
|
10
|
+
SimpleCov.start do
|
11
|
+
add_filter('spec')
|
12
|
+
end
|
13
|
+
|
14
|
+
class ListIncompletelyCoveredFiles
|
15
|
+
def format(result)
|
16
|
+
incompletes = result.files.select { |f| f.covered_percent < 100 }
|
17
|
+
|
18
|
+
unless incompletes.empty?
|
19
|
+
puts
|
20
|
+
puts "Files with incomplete test coverage:"
|
21
|
+
incompletes.each do |f|
|
22
|
+
printf " %2.01f%% %s\n", f.covered_percent, f.filename
|
23
|
+
end
|
24
|
+
puts; puts
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([
|
30
|
+
SimpleCov::Formatter::HTMLFormatter,
|
31
|
+
ListIncompletelyCoveredFiles
|
32
|
+
])
|
33
|
+
|
34
|
+
require_relative "./example_group_methods"
|
35
|
+
require_relative "./example_methods"
|
36
|
+
|
37
|
+
RSpec.configure do |config|
|
38
|
+
config.extend ExampleGroupMethods
|
39
|
+
config.include ExampleMethods
|
40
|
+
|
41
|
+
config.order = :random
|
42
|
+
config.fail_fast = !!ENV["RSPEC_CONFIG_FAIL_FAST"]
|
43
|
+
config.full_backtrace = !!ENV["RSPEC_CONFIG_FULL_BACKTRACE"]
|
44
|
+
|
45
|
+
config.expect_with :rspec do |c|
|
46
|
+
c.syntax = :expect
|
47
|
+
end
|
48
|
+
|
49
|
+
config.mock_with :rspec do |mocks|
|
50
|
+
mocks.verify_partial_doubles = true
|
51
|
+
end
|
52
|
+
|
53
|
+
config.after(:each) do
|
54
|
+
Thread.current.name = nil
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "../spec_helper"
|
3
|
+
|
4
|
+
require_relative "../../lib/ultravisor"
|
5
|
+
|
6
|
+
describe Ultravisor do
|
7
|
+
let(:args) { {} }
|
8
|
+
let(:ultravisor) { Ultravisor.new(**args) }
|
9
|
+
let!(:child) { Ultravisor::Child.new(id: :xtra, klass: Class, method: :to_s) }
|
10
|
+
|
11
|
+
describe "#add_child" do
|
12
|
+
before(:each) do
|
13
|
+
allow(Ultravisor::Child).to receive(:new).and_return(child)
|
14
|
+
end
|
15
|
+
|
16
|
+
context "when the ultravisor isn't running" do
|
17
|
+
it "creates a new Child object" do
|
18
|
+
expect(Ultravisor::Child).to receive(:new).with(id: :xtra, klass: Class, method: :to_s, logger: instance_of(Logger))
|
19
|
+
|
20
|
+
ultravisor.add_child(id: :xtra, klass: Class, method: :to_s)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "registers the new child" do
|
24
|
+
ultravisor.add_child(id: :xtra, klass: Class, method: :to_s)
|
25
|
+
|
26
|
+
expect(ultravisor[:xtra]).to be_a(Ultravisor::Child)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "explodes if a dupe child ID is used" do
|
30
|
+
ultravisor.add_child(id: :xtra, klass: Class, method: :to_s)
|
31
|
+
|
32
|
+
expect do
|
33
|
+
ultravisor.add_child(id: :xtra, klass: Object, method: :to_s)
|
34
|
+
end.to raise_error(Ultravisor::DuplicateChildError)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "doesn't spawn a child thread" do
|
38
|
+
expect(child).to_not receive(:spawn)
|
39
|
+
|
40
|
+
ultravisor.add_child(id: :xtra, klass: Class, method: :to_s)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "while the ultravisor *is* running" do
|
45
|
+
let(:mock_thread) { instance_double(Thread) }
|
46
|
+
|
47
|
+
before(:each) do
|
48
|
+
allow(child).to receive(:spawn)
|
49
|
+
ultravisor.instance_variable_set(:@running_thread, mock_thread)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "creates a new Child object" do
|
53
|
+
expect(Ultravisor::Child).to receive(:new).with(id: :xtra, klass: Class, method: :to_s, logger: instance_of(Logger))
|
54
|
+
|
55
|
+
ultravisor.add_child(id: :xtra, klass: Class, method: :to_s)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "registers the new child" do
|
59
|
+
ultravisor.add_child(id: :xtra, klass: Class, method: :to_s)
|
60
|
+
|
61
|
+
expect(ultravisor[:xtra]).to be_a(Ultravisor::Child)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "explodes if a dupe child ID is used" do
|
65
|
+
ultravisor.add_child(id: :xtra, klass: Class, method: :to_s)
|
66
|
+
|
67
|
+
expect do
|
68
|
+
ultravisor.add_child(id: :xtra, klass: Object, method: :to_s)
|
69
|
+
end.to raise_error(Ultravisor::DuplicateChildError)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "spawns a child thread" do
|
73
|
+
ultravisor.add_child(id: :xtra, klass: Class, method: :to_s)
|
74
|
+
|
75
|
+
expect(child).to have_received(:spawn)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,121 @@
|
|
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
|
+
class FakeError < RuntimeError; end
|
10
|
+
|
11
|
+
def run
|
12
|
+
end
|
13
|
+
|
14
|
+
def poke_processor
|
15
|
+
process_castcall
|
16
|
+
end
|
17
|
+
|
18
|
+
def failing_method
|
19
|
+
raise FakeError
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe Ultravisor::Child do
|
24
|
+
let(:child) { Ultravisor::Child.new(**args) }
|
25
|
+
let(:instance) { child.__send__(:new_instance) }
|
26
|
+
|
27
|
+
describe "#call" do
|
28
|
+
context "without enable_castcall" do
|
29
|
+
let(:args) do
|
30
|
+
{
|
31
|
+
id: :call_child,
|
32
|
+
klass: CastCallTest,
|
33
|
+
method: :run,
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
it "does not accept calls to #call" do
|
38
|
+
expect { child.call }.to raise_error(NoMethodError)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "with enable_castcall" do
|
43
|
+
let(:args) do
|
44
|
+
{
|
45
|
+
id: :call_child,
|
46
|
+
klass: CastCallTest,
|
47
|
+
method: :run,
|
48
|
+
enable_castcall: true,
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
before(:each) do
|
53
|
+
# Got to have an instance otherwise all hell breaks loose
|
54
|
+
child.instance_variable_set(:@instance, instance)
|
55
|
+
|
56
|
+
# So we can check if and when it's been called
|
57
|
+
allow(instance).to receive(:to_s).and_call_original
|
58
|
+
allow(instance).to receive(:failing_method).and_call_original
|
59
|
+
end
|
60
|
+
|
61
|
+
it "accepts calls to #call" do
|
62
|
+
expect { child.call }.to_not raise_error
|
63
|
+
end
|
64
|
+
|
65
|
+
it "does not accept call calls to methods that do not exist on the worker object" do
|
66
|
+
expect { child.call.flibbetygibbets }.to raise_error(NoMethodError)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "does not accept call calls to private methods on the worker object" do
|
70
|
+
expect { child.call.eval }.to raise_error(NoMethodError)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "accepts calls to methods that exist on the worker object" do
|
74
|
+
th = Thread.new { child.call.to_s }
|
75
|
+
th.join(0.01)
|
76
|
+
instance.poke_processor
|
77
|
+
expect { th.value }.to_not raise_error
|
78
|
+
end
|
79
|
+
|
80
|
+
it "calls the instance method only when process_castcall is called" do
|
81
|
+
th = Thread.new { child.call.to_s }
|
82
|
+
th.join(0.001)
|
83
|
+
|
84
|
+
# Thread should be ticking along, not dead
|
85
|
+
expect(th.status).to eq("sleep")
|
86
|
+
expect(instance).to_not have_received(:to_s)
|
87
|
+
instance.poke_processor
|
88
|
+
expect(instance).to have_received(:to_s)
|
89
|
+
expect(th.value).to be_a(String)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "raises a relevant error if the call itself causes an exception is dying" do
|
93
|
+
th = Thread.new { child.call.failing_method }
|
94
|
+
th.join(0.001)
|
95
|
+
|
96
|
+
expect(instance).to_not have_received(:failing_method)
|
97
|
+
expect { instance.poke_processor }.to raise_error(CastCallTest::FakeError)
|
98
|
+
expect(instance).to have_received(:failing_method)
|
99
|
+
|
100
|
+
expect { th.value }.to raise_error(Ultravisor::ChildRestartedError)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "raises a relevant error if the instance is dying" do
|
104
|
+
instance.instance_variable_get(:@ultravisor_child_castcall_queue).close
|
105
|
+
|
106
|
+
expect { child.call.to_s }.to raise_error(Ultravisor::ChildRestartedError)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "raises an error to all incomplete calls if the instance terminates" do
|
110
|
+
th = Thread.new { child.call.to_s }
|
111
|
+
|
112
|
+
th.join(0.001) until th.status == "sleep"
|
113
|
+
|
114
|
+
expect(instance).to_not have_received(:to_s)
|
115
|
+
child.instance_variable_get(:@spawn_m).synchronize { child.__send__(:termination_cleanup) }
|
116
|
+
|
117
|
+
expect { th.value }.to raise_error(Ultravisor::ChildRestartedError)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|