invoker 1.0.4 → 1.1.0
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/.gitignore +5 -0
- data/.rubocop.yml +30 -0
- data/.travis.yml +1 -0
- data/Gemfile +1 -0
- data/bin/invoker +4 -8
- data/invoker.gemspec +10 -11
- data/lib/invoker.rb +95 -21
- data/lib/invoker/cli.rb +126 -0
- data/lib/invoker/cli/pinger.rb +23 -0
- data/lib/invoker/cli/question.rb +15 -0
- data/lib/invoker/cli/tail.rb +34 -0
- data/lib/invoker/cli/tail_watcher.rb +34 -0
- data/lib/invoker/command_worker.rb +28 -2
- data/lib/invoker/commander.rb +34 -236
- data/lib/invoker/config.rb +5 -0
- data/lib/invoker/daemon.rb +126 -0
- data/lib/invoker/dns_cache.rb +23 -0
- data/lib/invoker/errors.rb +1 -0
- data/lib/invoker/ipc.rb +45 -0
- data/lib/invoker/ipc/add_command.rb +12 -0
- data/lib/invoker/ipc/add_http_command.rb +10 -0
- data/lib/invoker/ipc/base_command.rb +24 -0
- data/lib/invoker/ipc/client_handler.rb +26 -0
- data/lib/invoker/ipc/dns_check_command.rb +16 -0
- data/lib/invoker/ipc/list_command.rb +11 -0
- data/lib/invoker/ipc/message.rb +170 -0
- data/lib/invoker/ipc/message/list_response.rb +33 -0
- data/lib/invoker/ipc/message/tail_response.rb +10 -0
- data/lib/invoker/ipc/ping_command.rb +10 -0
- data/lib/invoker/ipc/reload_command.rb +12 -0
- data/lib/invoker/ipc/remove_command.rb +12 -0
- data/lib/invoker/{command_listener → ipc}/server.rb +6 -11
- data/lib/invoker/ipc/tail_command.rb +11 -0
- data/lib/invoker/ipc/unix_client.rb +60 -0
- data/lib/invoker/parsers/config.rb +1 -0
- data/lib/invoker/power/balancer.rb +17 -7
- data/lib/invoker/power/config.rb +6 -3
- data/lib/invoker/power/dns.rb +22 -21
- data/lib/invoker/power/http_response.rb +1 -1
- data/lib/invoker/power/power.rb +3 -0
- data/lib/invoker/power/powerup.rb +3 -2
- data/lib/invoker/power/setup.rb +6 -4
- data/lib/invoker/process_manager.rb +187 -0
- data/lib/invoker/process_printer.rb +27 -38
- data/lib/invoker/reactor.rb +19 -38
- data/lib/invoker/reactor/reader.rb +53 -0
- data/lib/invoker/version.rb +1 -1
- data/readme.md +1 -1
- data/spec/invoker/cli/pinger_spec.rb +22 -0
- data/spec/invoker/cli/tail_watcher_spec.rb +39 -0
- data/spec/invoker/cli_spec.rb +27 -0
- data/spec/invoker/command_worker_spec.rb +30 -0
- data/spec/invoker/commander_spec.rb +57 -127
- data/spec/invoker/config_spec.rb +21 -0
- data/spec/invoker/daemon_spec.rb +34 -0
- data/spec/invoker/invoker_spec.rb +31 -0
- data/spec/invoker/ipc/client_handler_spec.rb +44 -0
- data/spec/invoker/ipc/dns_check_command_spec.rb +32 -0
- data/spec/invoker/ipc/message/list_response_spec.rb +22 -0
- data/spec/invoker/ipc/message_spec.rb +45 -0
- data/spec/invoker/ipc/unix_client_spec.rb +29 -0
- data/spec/invoker/power/setup_spec.rb +1 -1
- data/spec/invoker/process_manager_spec.rb +98 -0
- data/spec/invoker/reactor_spec.rb +6 -0
- data/spec/spec_helper.rb +15 -24
- metadata +107 -77
- data/lib/invoker/command_listener/client.rb +0 -45
- data/lib/invoker/parsers/option_parser.rb +0 -106
- data/lib/invoker/power.rb +0 -7
- data/lib/invoker/runner.rb +0 -98
- data/spec/invoker/command_listener/client_spec.rb +0 -52
data/spec/invoker/config_spec.rb
CHANGED
@@ -146,4 +146,25 @@ web: bundle exec rails s -p $PORT
|
|
146
146
|
end
|
147
147
|
end
|
148
148
|
end
|
149
|
+
|
150
|
+
describe "Copy of DNS information" do
|
151
|
+
it "should allow copy of DNS information" do
|
152
|
+
begin
|
153
|
+
File.open("/tmp/Procfile", "w") {|fl|
|
154
|
+
fl.write <<-EOD
|
155
|
+
web: bundle exec rails s -p $PORT
|
156
|
+
EOD
|
157
|
+
}
|
158
|
+
Invoker.load_invoker_config("/tmp/Procfile", 9000)
|
159
|
+
dns_cache = Invoker::DNSCache.new(Invoker.config)
|
160
|
+
|
161
|
+
expect(dns_cache.dns_data).to_not be_empty
|
162
|
+
expect(dns_cache.dns_data['web']).to_not be_empty
|
163
|
+
expect(dns_cache.dns_data['web']['port']).to eql 9001
|
164
|
+
ensure
|
165
|
+
File.delete("/tmp/Procfile")
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
149
170
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Invoker::Daemon do
|
4
|
+
let(:daemon) { Invoker::Daemon.new}
|
5
|
+
|
6
|
+
describe "#start" do
|
7
|
+
context "when daemon is aleady running" do
|
8
|
+
it "exits without any error" do
|
9
|
+
daemon.expects(:running?).returns(true)
|
10
|
+
begin
|
11
|
+
daemon.start
|
12
|
+
rescue SystemExit => e
|
13
|
+
expect(e.status).to be(0)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context "when daemon is not running" do
|
19
|
+
it "starts the daemon" do
|
20
|
+
daemon.expects(:dead?).returns(false)
|
21
|
+
daemon.expects(:running?).returns(false)
|
22
|
+
daemon.expects(:daemonize)
|
23
|
+
daemon.start
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#stop" do
|
29
|
+
it "stops the daemon" do
|
30
|
+
daemon.expects(:kill_process)
|
31
|
+
daemon.stop
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -49,5 +49,36 @@ describe "Invoker" do
|
|
49
49
|
Invoker.can_run_balancer?(false)
|
50
50
|
end
|
51
51
|
end
|
52
|
+
|
53
|
+
describe "#setup_config_location" do
|
54
|
+
before do
|
55
|
+
Dir.stubs(:home).returns('/tmp')
|
56
|
+
@config_location = File.join('/tmp', '.invoker')
|
57
|
+
FileUtils.rm_rf(@config_location)
|
58
|
+
end
|
59
|
+
|
60
|
+
context "when the old config file does not exist" do
|
61
|
+
it "creates the new config directory" do
|
62
|
+
Invoker.setup_config_location
|
63
|
+
expect(Dir.exist?(@config_location)).to be_true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "when the old config file exists" do
|
68
|
+
before do
|
69
|
+
File.open(@config_location, 'w') do |file|
|
70
|
+
file.write('invoker config')
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
it "moves the file to the new directory" do
|
75
|
+
Invoker.setup_config_location
|
76
|
+
expect(Dir.exist?(@config_location)).to be_true
|
77
|
+
new_config_file = File.join(@config_location, 'config')
|
78
|
+
expect(File.exist?(new_config_file)).to be_true
|
79
|
+
expect(File.read(new_config_file)).to match('invoker config')
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
52
83
|
end
|
53
84
|
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Invoker::IPC::ClientHandler do
|
4
|
+
let(:client_socket) { StringIO.new }
|
5
|
+
let(:client) { Invoker::IPC::ClientHandler.new(client_socket) }
|
6
|
+
|
7
|
+
describe "add command" do
|
8
|
+
let(:message_object) { MM::Add.new(process_name: 'foo') }
|
9
|
+
it "should run if read from socket" do
|
10
|
+
invoker_commander.expects(:on_next_tick).with("foo")
|
11
|
+
client_socket.string = message_object.encoded_message
|
12
|
+
|
13
|
+
client.read_and_execute
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "remove command" do
|
18
|
+
it "with specific signal" do
|
19
|
+
message_object = MM::Remove.new(process_name: 'foo', signal: 'INT')
|
20
|
+
invoker_commander.expects(:on_next_tick)
|
21
|
+
client_socket.string = message_object.encoded_message
|
22
|
+
|
23
|
+
client.read_and_execute
|
24
|
+
end
|
25
|
+
|
26
|
+
it "with default signal" do
|
27
|
+
message_object = MM::Remove.new(process_name: 'foo')
|
28
|
+
invoker_commander.expects(:on_next_tick)
|
29
|
+
client_socket.string = message_object.encoded_message
|
30
|
+
|
31
|
+
client.read_and_execute
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "add_http command" do
|
36
|
+
let(:message_object) { MM::AddHttp.new(process_name: 'foo', port: 9000)}
|
37
|
+
it "adds the process name and port to dns cache" do
|
38
|
+
invoker_dns_cache.expects(:add).with('foo', 9000)
|
39
|
+
client_socket.string = message_object.encoded_message
|
40
|
+
|
41
|
+
client.read_and_execute
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Invoker::IPC::DnsCheckCommand do
|
4
|
+
let(:client_socket) { StringIO.new }
|
5
|
+
let(:client) { Invoker::IPC::ClientHandler.new(client_socket) }
|
6
|
+
|
7
|
+
describe "dns check for valid process" do
|
8
|
+
let(:message_object) { MM::DnsCheck.new(process_name: 'lolbro') }
|
9
|
+
it "should response with dns check response" do
|
10
|
+
invoker_dns_cache.expects(:[]).returns('port' => 9000)
|
11
|
+
client_socket.string = message_object.encoded_message
|
12
|
+
|
13
|
+
client.read_and_execute
|
14
|
+
|
15
|
+
dns_check_response = client_socket.string
|
16
|
+
expect(dns_check_response).to match(/9000/)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "dns check for invalid process" do
|
21
|
+
let(:message_object) { MM::DnsCheck.new(process_name: 'foo') }
|
22
|
+
it "should response with dns check response" do
|
23
|
+
invoker_dns_cache.expects(:[]).returns('port' => nil)
|
24
|
+
client_socket.string = message_object.encoded_message
|
25
|
+
|
26
|
+
client.read_and_execute
|
27
|
+
|
28
|
+
dns_check_response = client_socket.string
|
29
|
+
expect(dns_check_response).to match(/null/)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe MM::ListResponse do
|
4
|
+
context "serializing a response" do
|
5
|
+
let(:process_array) do
|
6
|
+
[
|
7
|
+
{ shell_command: 'foo', process_name: 'foo', dir: '/tmp', pid: 100 },
|
8
|
+
{ shell_command: 'bar', process_name: 'bar', dir: '/tmp', pid: 200 }
|
9
|
+
]
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:message) { MM::ListResponse.new(processes: process_array) }
|
13
|
+
|
14
|
+
it "should prepare proper json" do
|
15
|
+
json_hash = message.as_json
|
16
|
+
expect(json_hash[:type]).to eql "list_response"
|
17
|
+
expect(json_hash[:processes]).to have(2).elements
|
18
|
+
expect(json_hash[:processes][0]).to be_a(Hash)
|
19
|
+
expect(json_hash[:processes][1]).to be_a(Hash)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Invoker::IPC::Message do
|
4
|
+
describe "test equality of objects" do
|
5
|
+
context "for simple messages" do
|
6
|
+
let(:message) { MM::Add.new(process_name: 'foo') }
|
7
|
+
|
8
|
+
it "object should be reported same if same value" do
|
9
|
+
m2 = MM::Add.new(process_name: 'foo')
|
10
|
+
expect(message).to eql m2
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should report objects to be not eql if differnt value" do
|
14
|
+
m2 = MM::Add.new(process_name: 'bar')
|
15
|
+
expect(message).to_not eql m2
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context "for nested messages" do
|
20
|
+
let(:process_array) do
|
21
|
+
[
|
22
|
+
{ shell_command: 'foo', process_name: 'foo', dir: '/tmp', pid: 100 },
|
23
|
+
{ shell_command: 'bar', process_name: 'bar', dir: '/tmp', pid: 200 }
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
let(:message) { MM::ListResponse.new(processes: process_array) }
|
28
|
+
|
29
|
+
it "should report eql for eql objects" do
|
30
|
+
m2 = MM::ListResponse.new(processes: process_array)
|
31
|
+
expect(message).to eql m2
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should report not equal for different objects" do
|
35
|
+
another_process_array = [
|
36
|
+
{ shell_command: 'baz', process_name: 'foo', dir: '/tmp', pid: 100 },
|
37
|
+
{ shell_command: 'bar', process_name: 'bar', dir: '/tmp', pid: 200 }
|
38
|
+
]
|
39
|
+
|
40
|
+
m2 = MM::ListResponse.new(processes: another_process_array)
|
41
|
+
expect(message).to_not eql m2
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Invoker::IPC::UnixClient do
|
4
|
+
let(:unix_client) { described_class.new }
|
5
|
+
let(:socket) { StringIO.new }
|
6
|
+
|
7
|
+
describe "serializing a " do
|
8
|
+
it "list request should work" do
|
9
|
+
unix_client.expects(:open_client_socket).yields(socket)
|
10
|
+
unix_client.send_command("list")
|
11
|
+
|
12
|
+
expect(socket.string).to match(/list/)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "add request should work" do
|
16
|
+
unix_client.expects(:open_client_socket).yields(socket)
|
17
|
+
unix_client.send_command("add", process_name: "hello")
|
18
|
+
|
19
|
+
expect(socket.string).to match(/hello/)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe ".send_command" do
|
24
|
+
it "calls the send_command instance method" do
|
25
|
+
Invoker::IPC::UnixClient.any_instance.expects(:send_command).once
|
26
|
+
Invoker::IPC::UnixClient.send_command("list")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -72,7 +72,7 @@ describe "Setup" do
|
|
72
72
|
it "should uninstall firewall rules and remove all files created by setup" do
|
73
73
|
setup = Invoker::Power::Setup.new
|
74
74
|
|
75
|
-
|
75
|
+
Invoker::CLI::Question.expects(:agree).returns(true)
|
76
76
|
setup.expects(:remove_resolver_file).once
|
77
77
|
setup.expects(:unload_firewall_rule).with(true).once
|
78
78
|
setup.expects(:flush_dns_rules).once
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Invoker::ProcessManager do
|
4
|
+
let(:process_manager) { Invoker::ProcessManager.new }
|
5
|
+
describe "#start_process_by_name" do
|
6
|
+
it "should find command by label and start it, if found" do
|
7
|
+
invoker_config.stubs(:processes).returns([OpenStruct.new(:label => "resque", :cmd => "foo", :dir => "bar")])
|
8
|
+
invoker_config.expects(:process).returns(OpenStruct.new(:label => "resque", :cmd => "foo", :dir => "bar"))
|
9
|
+
process_manager.expects(:start_process).returns(true)
|
10
|
+
|
11
|
+
process_manager.start_process_by_name("resque")
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should not start already running process" do
|
15
|
+
process_manager.workers.expects(:[]).returns(OpenStruct.new(:pid => "bogus"))
|
16
|
+
expect(process_manager.start_process_by_name("resque")).to be_false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#stop_process" do
|
21
|
+
let(:message) { MM::Remove.new(options) }
|
22
|
+
describe "when a worker is found" do
|
23
|
+
before do
|
24
|
+
process_manager.workers.expects(:[]).returns(OpenStruct.new(:pid => "bogus"))
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "if a signal is specified" do
|
28
|
+
let(:options) { { process_name: 'bogus', signal: 'HUP' } }
|
29
|
+
it "should use that signal to kill the worker" do
|
30
|
+
process_manager.expects(:process_kill).with("bogus", "HUP").returns(true)
|
31
|
+
expect(process_manager.stop_process(message)).to be_true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "if no signal is specified" do
|
36
|
+
let(:options) { { process_name: 'bogus' } }
|
37
|
+
it "should use INT signal" do
|
38
|
+
process_manager.expects(:process_kill).with("bogus", "INT").returns(true)
|
39
|
+
expect(process_manager.stop_process(message)).to be_true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "when no worker is found" do
|
45
|
+
let(:options) { { process_name: 'bogus', signal: 'HUP' } }
|
46
|
+
before do
|
47
|
+
process_manager.workers.expects(:[]).returns(nil)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should not kill anything" do
|
51
|
+
process_manager.expects(:process_kill).never
|
52
|
+
process_manager.stop_process(message)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#load_env" do
|
58
|
+
it "should load .env file from the specified directory" do
|
59
|
+
dir = "/tmp"
|
60
|
+
begin
|
61
|
+
env_file = File.new("#{dir}/.env", "w")
|
62
|
+
env_data =<<-EOD
|
63
|
+
FOO=foo
|
64
|
+
BAR=bar
|
65
|
+
EOD
|
66
|
+
env_file.write(env_data)
|
67
|
+
env_file.close
|
68
|
+
env_options = process_manager.load_env(dir)
|
69
|
+
expect(env_options).to include("FOO" => "foo", "BAR" => "bar")
|
70
|
+
ensure
|
71
|
+
File.delete(env_file.path)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should default to current directory if no directory is specified" do
|
76
|
+
dir = ENV["HOME"]
|
77
|
+
ENV.stubs(:[]).with("PWD").returns(dir)
|
78
|
+
begin
|
79
|
+
env_file = File.new("#{dir}/.env", "w")
|
80
|
+
env_data =<<-EOD
|
81
|
+
FOO=bar
|
82
|
+
BAR=foo
|
83
|
+
EOD
|
84
|
+
env_file.write(env_data)
|
85
|
+
env_file.close
|
86
|
+
env_options = process_manager.load_env
|
87
|
+
expect(env_options).to include("FOO" => "bar", "BAR" => "foo")
|
88
|
+
ensure
|
89
|
+
File.delete(env_file.path)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should return empty hash if there is no .env file" do
|
94
|
+
dir = "/tmp"
|
95
|
+
expect(process_manager.load_env(dir)).to eq({})
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,19 +1,15 @@
|
|
1
1
|
require "pry"
|
2
|
-
require
|
3
|
-
|
2
|
+
require "simplecov"
|
3
|
+
SimpleCov.start do
|
4
|
+
add_filter "/spec/"
|
5
|
+
end
|
6
|
+
|
4
7
|
|
5
8
|
require "invoker"
|
9
|
+
require "invoker/power/power"
|
10
|
+
MM = Invoker::IPC::Message
|
6
11
|
|
7
|
-
# This file was generated by the `rspec --init` command. Conventionally, all
|
8
|
-
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
9
|
-
# Require this file using `require "spec_helper"` to ensure that it is only
|
10
|
-
# loaded once.
|
11
|
-
#
|
12
|
-
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
13
12
|
RSpec.configure do |config|
|
14
|
-
config.expect_with :rspec do |c|
|
15
|
-
c.syntax = :expect
|
16
|
-
end
|
17
13
|
config.treat_symbols_as_metadata_keys_with_true_values = true
|
18
14
|
config.run_all_when_everything_filtered = true
|
19
15
|
config.filter_run :focus
|
@@ -23,7 +19,8 @@ RSpec.configure do |config|
|
|
23
19
|
@original_verbosity = $VERBOSE
|
24
20
|
$VERBOSE = nil
|
25
21
|
@old_config = Invoker::Power::Config::CONFIG_LOCATION
|
26
|
-
Invoker::Power::Config.const_set(:CONFIG_LOCATION, "/tmp/.invoker")
|
22
|
+
Invoker::Power::Config.const_set(:CONFIG_LOCATION, "/tmp/.invoker/config")
|
23
|
+
FileUtils.mkdir("/tmp/.invoker") unless Dir.exist?("/tmp/.invoker")
|
27
24
|
|
28
25
|
File.exists?(Invoker::Power::Config::CONFIG_LOCATION) &&
|
29
26
|
File.delete(Invoker::Power::Config::CONFIG_LOCATION)
|
@@ -65,19 +62,13 @@ end
|
|
65
62
|
ENV["INVOKER_TESTS"] = "true"
|
66
63
|
|
67
64
|
def invoker_config
|
68
|
-
|
69
|
-
Invoker::CONFIG
|
70
|
-
else
|
71
|
-
Invoker.const_set(:CONFIG, mock())
|
72
|
-
Invoker::CONFIG
|
73
|
-
end
|
65
|
+
Invoker.config ||= mock
|
74
66
|
end
|
75
67
|
|
76
68
|
def invoker_commander
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
end
|
69
|
+
Invoker.commander ||= mock
|
70
|
+
end
|
71
|
+
|
72
|
+
def invoker_dns_cache
|
73
|
+
Invoker.dns_cache ||= mock
|
83
74
|
end
|