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