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,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,8 @@
1
+ # frozen_string_literal: true
2
+ module ExampleMethods
3
+ def tmptrace
4
+ require "tracer"
5
+ Tracer.add_filter { |event, file, line, id, binding, klass, *rest| klass.to_s =~ /Ultravisor/ }
6
+ Tracer.on { yield }
7
+ end
8
+ end
@@ -0,0 +1,52 @@
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
+ 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