carrot_rpc 0.2.3.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +50 -0
- data/.rspec +2 -0
- data/.rubocop.yml +35 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +149 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +9 -0
- data/LICENSE +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +172 -0
- data/Rakefile +7 -0
- data/bin/carrot_rpc +19 -0
- data/bin/console +20 -0
- data/bin/setup +7 -0
- data/carrot_rpc.gemspec +49 -0
- data/circle.yml +8 -0
- data/lib/carrot_rpc/cli.rb +110 -0
- data/lib/carrot_rpc/client_server.rb +25 -0
- data/lib/carrot_rpc/configuration.rb +17 -0
- data/lib/carrot_rpc/error/code.rb +17 -0
- data/lib/carrot_rpc/error.rb +43 -0
- data/lib/carrot_rpc/hash_extensions.rb +22 -0
- data/lib/carrot_rpc/rpc_client.rb +117 -0
- data/lib/carrot_rpc/rpc_server/jsonapi_resources/actions.rb +108 -0
- data/lib/carrot_rpc/rpc_server/jsonapi_resources.rb +172 -0
- data/lib/carrot_rpc/rpc_server.rb +77 -0
- data/lib/carrot_rpc/server_runner/autoload_rails.rb +41 -0
- data/lib/carrot_rpc/server_runner/logger.rb +31 -0
- data/lib/carrot_rpc/server_runner/pid.rb +142 -0
- data/lib/carrot_rpc/server_runner/signals.rb +21 -0
- data/lib/carrot_rpc/server_runner.rb +156 -0
- data/lib/carrot_rpc/tagged_log.rb +24 -0
- data/lib/carrot_rpc/version.rb +3 -0
- data/lib/carrot_rpc.rb +46 -0
- data/logs/.gitkeep +0 -0
- metadata +182 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
# Base RPC Server class. Other Servers should inherit from this.
|
2
|
+
class CarrotRpc::RpcServer
|
3
|
+
autoload :JSONAPIResources, "carrot_rpc/rpc_server/jsonapi_resources"
|
4
|
+
|
5
|
+
using CarrotRpc::HashExtensions
|
6
|
+
|
7
|
+
attr_reader :channel, :server_queue, :logger
|
8
|
+
# method_reciver => object that receives the method. can be a class or anything responding to send
|
9
|
+
|
10
|
+
extend CarrotRpc::ClientServer
|
11
|
+
|
12
|
+
# Documentation advises not to share a channel connection. Create new channel for each server instance.
|
13
|
+
def initialize(config: nil, block: true)
|
14
|
+
# create a channel and exchange that both client and server know about
|
15
|
+
config ||= CarrotRpc.configuration
|
16
|
+
@channel = config.bunny.create_channel
|
17
|
+
@logger = config.logger
|
18
|
+
@block = block
|
19
|
+
@server_queue = @channel.queue(self.class.queue_name)
|
20
|
+
@exchange = @channel.default_exchange
|
21
|
+
end
|
22
|
+
|
23
|
+
# start da server!
|
24
|
+
# method => object that receives the method. can be a class or anything responding to send
|
25
|
+
def start
|
26
|
+
# subscribe is like a callback
|
27
|
+
@server_queue.subscribe(block: @block) do |_delivery_info, properties, payload|
|
28
|
+
logger.debug "Receiving message: #{payload}"
|
29
|
+
|
30
|
+
request_message = JSON.parse(payload).with_indifferent_access
|
31
|
+
|
32
|
+
process_request(request_message, properties: properties)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def process_request(request_message, properties:)
|
37
|
+
result = send(request_message[:method], request_message[:params])
|
38
|
+
rescue CarrotRpc::Error => rpc_server_error
|
39
|
+
logger.error(rpc_server_error)
|
40
|
+
|
41
|
+
reply_error rpc_server_error.serialized_message,
|
42
|
+
properties: properties,
|
43
|
+
request_message: request_message
|
44
|
+
else
|
45
|
+
reply_result result,
|
46
|
+
properties: properties,
|
47
|
+
request_message: request_message
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def reply(properties:, response_message:)
|
53
|
+
@exchange.publish response_message.to_json,
|
54
|
+
correlation_id: properties.correlation_id,
|
55
|
+
routing_key: properties.reply_to
|
56
|
+
end
|
57
|
+
|
58
|
+
# See http://www.jsonrpc.org/specification#error_object
|
59
|
+
def reply_error(error, properties:, request_message:)
|
60
|
+
response_message = { error: error, id: request_message[:id], jsonrpc: "2.0" }
|
61
|
+
|
62
|
+
logger.debug "Publish error: #{error} to #{response_message}"
|
63
|
+
|
64
|
+
reply properties: properties,
|
65
|
+
response_message: response_message
|
66
|
+
end
|
67
|
+
|
68
|
+
# See http://www.jsonrpc.org/specification#response_object
|
69
|
+
def reply_result(result, properties:, request_message:)
|
70
|
+
response_message = { id: request_message[:id], result: result, jsonrpc: "2.0" }
|
71
|
+
|
72
|
+
logger.debug "Publishing result: #{result} to #{response_message}"
|
73
|
+
|
74
|
+
reply properties: properties,
|
75
|
+
response_message: response_message
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# Loads the Rails application so that the servers can use the Rails gems and environment.
|
2
|
+
module CarrotRpc::ServerRunner::AutoloadRails
|
3
|
+
# Path to the `config/environment.rb`, which is the file that must actually be `require`d to load Rails.
|
4
|
+
#
|
5
|
+
# @param root [String] path to the root of the Rails app
|
6
|
+
# @return [String]
|
7
|
+
def self.environment_path(root)
|
8
|
+
File.join(root, "config/environment.rb")
|
9
|
+
end
|
10
|
+
|
11
|
+
# Attempts to load Rails app at `root`.
|
12
|
+
#
|
13
|
+
# @param root [String] path to the root of the Rails app
|
14
|
+
# @param logger [Logger] logger to print success to.
|
15
|
+
# @return [true]
|
16
|
+
# @raise [LoadError] if rails cannot be loaded
|
17
|
+
def self.load_root(root, logger:)
|
18
|
+
rails_path = environment_path(root)
|
19
|
+
|
20
|
+
if File.exist?(rails_path)
|
21
|
+
logger.info "Rails app found at: #{rails_path}"
|
22
|
+
ENV["RACK_ENV"] ||= ENV["RAILS_ENV"] || "development"
|
23
|
+
require rails_path
|
24
|
+
::Rails.application.eager_load!
|
25
|
+
true
|
26
|
+
else
|
27
|
+
require rails_path
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Loads Rails app at `root` if `CarrotRpc.configuration.autoload_rails`
|
32
|
+
#
|
33
|
+
# @param (see load_root)
|
34
|
+
# @return (see load_root)
|
35
|
+
# @raise (see load_root)
|
36
|
+
def self.conditionally_load_root(root, logger:)
|
37
|
+
if CarrotRpc.configuration.autoload_rails
|
38
|
+
load_root(root, logger: logger)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Constructs a logger based on the {CarrotRpc.configuration} and Rails environment
|
2
|
+
module CarrotRpc::ServerRunner::Logger
|
3
|
+
# A `Logger` configured based on `CarrotRpc.configuration.logfile` and `CarrotRpc.configuration.loglevel`
|
4
|
+
#
|
5
|
+
# Fallbacks:
|
6
|
+
# * `Rails.logger` if `Rails` is loaded
|
7
|
+
# * `Logger` to `STDOUT` if `Rails` is not loaded
|
8
|
+
#
|
9
|
+
# @return [Logger]
|
10
|
+
def self.configured
|
11
|
+
logger = from_file
|
12
|
+
|
13
|
+
if logger.nil?
|
14
|
+
logger = if defined?(::Rails)
|
15
|
+
CarrotRpc::TaggedLog.new(logger: Rails.logger, tags: ["Carrot RPC"])
|
16
|
+
else
|
17
|
+
Logger.new(STDOUT)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
logger.level = CarrotRpc.configuration.loglevel
|
22
|
+
|
23
|
+
logger
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.from_file
|
27
|
+
return nil unless CarrotRpc.configuration.logfile
|
28
|
+
|
29
|
+
::Logger.new(CarrotRpc.configuration.logfile)
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# Pid and pid path for {CarrotRpc::ServerRunner}
|
2
|
+
class CarrotRpc::ServerRunner::Pid
|
3
|
+
# Attributes
|
4
|
+
|
5
|
+
# @return [Logger]
|
6
|
+
attr_reader :logger
|
7
|
+
|
8
|
+
# @return [String]
|
9
|
+
attr_reader :path
|
10
|
+
|
11
|
+
# Methods
|
12
|
+
|
13
|
+
## Class Methods
|
14
|
+
|
15
|
+
# The status of the given `pid` number.
|
16
|
+
#
|
17
|
+
# @param pid [Integer] a 0 or positive PID
|
18
|
+
# @return [:dead] if `pid` is `0`
|
19
|
+
# @return (see number_error_check_status)
|
20
|
+
def self.number_status(pid)
|
21
|
+
if pid == 0
|
22
|
+
:dead
|
23
|
+
else
|
24
|
+
number_error_check_status(pid)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# The status of the given `non_zero_pid` number.
|
29
|
+
#
|
30
|
+
# @param non_zero_pid [Integer] a non-zero pid
|
31
|
+
# @return [:dead] if `pid` cannot be contacted
|
32
|
+
# @return [:not_owned] if interacting with `pid` raises a permission error
|
33
|
+
# @return [:running] if `pid` is running
|
34
|
+
def self.number_error_check_status(non_zero_pid)
|
35
|
+
# sending signal `0` just performs error checking
|
36
|
+
Process.kill(0, non_zero_pid)
|
37
|
+
rescue Errno::ESRCH
|
38
|
+
# Invalid pid
|
39
|
+
:dead
|
40
|
+
rescue Errno::EPERM
|
41
|
+
# no privilege to interact with process
|
42
|
+
:not_owned
|
43
|
+
else
|
44
|
+
:running
|
45
|
+
end
|
46
|
+
|
47
|
+
# Status of the `pid` inside `path`.
|
48
|
+
#
|
49
|
+
# @return [:exited] if `path` does not exist, which indicates the server never ran or exited cleanly
|
50
|
+
# @return [:not_owned] if `path` cannot be read
|
51
|
+
# @return (see number_status)
|
52
|
+
def self.path_status(path)
|
53
|
+
pid = File.read(path).to_i
|
54
|
+
rescue Errno::ENOENT
|
55
|
+
# File does not exist
|
56
|
+
:exited
|
57
|
+
rescue Errno::EPERM
|
58
|
+
# File cannot be read
|
59
|
+
:not_owned
|
60
|
+
else
|
61
|
+
number_status(pid)
|
62
|
+
end
|
63
|
+
|
64
|
+
## Initialize
|
65
|
+
|
66
|
+
# @param path [Path, nil] path to pid path
|
67
|
+
def initialize(path:, logger:)
|
68
|
+
unless path.nil?
|
69
|
+
# daemonization will change CWD so expand relative paths now
|
70
|
+
@path = File.expand_path(path)
|
71
|
+
end
|
72
|
+
|
73
|
+
@logger = logger
|
74
|
+
end
|
75
|
+
|
76
|
+
## Instance Methods
|
77
|
+
|
78
|
+
# Exits if {#status} indicates server is already running, otherwise deletes {#path}.
|
79
|
+
#
|
80
|
+
# @return [void]
|
81
|
+
def check
|
82
|
+
if path?
|
83
|
+
case self.class.path_status(path)
|
84
|
+
when :running, :not_owned
|
85
|
+
logger.warn "A server is already running. Check #{path}"
|
86
|
+
exit(1)
|
87
|
+
when :dead
|
88
|
+
delete
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Deletes `path` if it is set
|
94
|
+
#
|
95
|
+
# @return [void]
|
96
|
+
def delete
|
97
|
+
if path? && File.exist?(path)
|
98
|
+
File.delete(path)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Registers an `at_exit` handler to {#delete}.
|
103
|
+
#
|
104
|
+
# @return [void]
|
105
|
+
def delete_at_exit
|
106
|
+
at_exit do
|
107
|
+
delete
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Keeps trying to write {#path} until it succeeds
|
112
|
+
def ensure_written
|
113
|
+
if path?
|
114
|
+
begin
|
115
|
+
write
|
116
|
+
rescue Errno::EEXIST
|
117
|
+
check
|
118
|
+
|
119
|
+
retry
|
120
|
+
else
|
121
|
+
delete_at_exit
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Whether {#path} is set.
|
127
|
+
#
|
128
|
+
# @return [true] if {#path} is not `nil`.
|
129
|
+
# @return [false] otherwise
|
130
|
+
def path?
|
131
|
+
!path.nil?
|
132
|
+
end
|
133
|
+
|
134
|
+
# Write to process id path
|
135
|
+
#
|
136
|
+
# @return [void]
|
137
|
+
def write
|
138
|
+
File.open(path, File::CREAT | File::EXCL | File::WRONLY) do |f|
|
139
|
+
f.write(Process.pid.to_s)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Traps all {the signals NAMES} that should be intercepted by a long-running background process.
|
2
|
+
module CarrotRpc::ServerRunner::Signals
|
3
|
+
# CONSTANTS
|
4
|
+
|
5
|
+
# The name of the signals to trap.
|
6
|
+
NAMES = %w(HUP INT QUIT TERM).freeze
|
7
|
+
|
8
|
+
# Traps all {NAMES}.
|
9
|
+
#
|
10
|
+
# @yield [name] Block to call when the signal is trapped.
|
11
|
+
# @yieldparam name [String] the name of the signal that was trapped
|
12
|
+
# @yieldreturn [void]
|
13
|
+
# @return [void]
|
14
|
+
def self.trap
|
15
|
+
NAMES.each do |name|
|
16
|
+
Kernel.trap(name) do
|
17
|
+
yield name
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require "active_support/core_ext/string/inflections"
|
2
|
+
|
3
|
+
# Automatically detects, loads, and runs all {CarrotRpc::RpcServer} subclasses under `app/servers` in the project root.
|
4
|
+
class CarrotRpc::ServerRunner
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
|
7
|
+
autoload :AutoloadRails
|
8
|
+
autoload :Logger
|
9
|
+
autoload :Pid
|
10
|
+
autoload :Signals
|
11
|
+
|
12
|
+
# Attributes
|
13
|
+
|
14
|
+
attr_reader :quit, :servers
|
15
|
+
|
16
|
+
# @return [CarrotRpc::ServerRunner::Pid]
|
17
|
+
attr_reader :pid
|
18
|
+
|
19
|
+
# Methods
|
20
|
+
|
21
|
+
# Instantiate the ServerRunner.
|
22
|
+
def initialize(rails_path: ".", pidfile: nil, runloop_sleep: 0, daemonize: false)
|
23
|
+
@runloop_sleep = runloop_sleep
|
24
|
+
@daemonize = daemonize
|
25
|
+
@servers = []
|
26
|
+
|
27
|
+
CarrotRpc::ServerRunner::AutoloadRails.conditionally_load_root(rails_path, logger: logger)
|
28
|
+
trap_signals
|
29
|
+
|
30
|
+
@pid = CarrotRpc::ServerRunner::Pid.new(
|
31
|
+
path: pidfile,
|
32
|
+
logger: logger
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Start the servers and the run loop.
|
37
|
+
def run!
|
38
|
+
pid.check
|
39
|
+
daemonize && suppress_output if daemonize?
|
40
|
+
pid.ensure_written
|
41
|
+
|
42
|
+
# Initialize the servers. Set logger.
|
43
|
+
run_servers
|
44
|
+
|
45
|
+
# Sleep for a split second.
|
46
|
+
until quit
|
47
|
+
sleep @runloop_sleep
|
48
|
+
end
|
49
|
+
# When runtime gets here, quit signal is received.
|
50
|
+
stop_servers
|
51
|
+
end
|
52
|
+
|
53
|
+
# Shutdown all servers defined.
|
54
|
+
def stop_servers
|
55
|
+
logger.info "#{@siginal_name} signal received!"
|
56
|
+
@servers.each do |s|
|
57
|
+
logger.info "Shutting Down Server Queue: #{s.server_queue.name}"
|
58
|
+
s.channel.close
|
59
|
+
end
|
60
|
+
# Close the connection once all the other servers are shutdown
|
61
|
+
CarrotRpc.configuration.bunny.close
|
62
|
+
end
|
63
|
+
|
64
|
+
# Find and require all servers in the app/servers dir.
|
65
|
+
# @param dirs [Array] directories relative to root of host application where RpcServers can be loaded
|
66
|
+
# @return [Array] of RpcServers loaded and initialized
|
67
|
+
def run_servers(dirs: %w(app servers))
|
68
|
+
files = server_files(dirs)
|
69
|
+
fail "No servers found!" if files.empty?
|
70
|
+
|
71
|
+
# Load each server defined in the project dir
|
72
|
+
files.each do |file|
|
73
|
+
@servers << run_server_file(file)
|
74
|
+
end
|
75
|
+
|
76
|
+
@servers
|
77
|
+
end
|
78
|
+
|
79
|
+
def run_server_file(file)
|
80
|
+
require file
|
81
|
+
server_klass_name = file.to_s.split("/").last.gsub(".rb", "").camelize
|
82
|
+
server_klass = server_klass_name.constantize
|
83
|
+
|
84
|
+
logger.info "Starting #{server_klass}..."
|
85
|
+
|
86
|
+
server = server_klass.new(block: false)
|
87
|
+
server.start
|
88
|
+
|
89
|
+
server
|
90
|
+
end
|
91
|
+
|
92
|
+
def server_files(dirs)
|
93
|
+
Dir[server_glob(dirs)]
|
94
|
+
end
|
95
|
+
|
96
|
+
def server_glob(dirs)
|
97
|
+
regex = %r{\A/.*/#{dirs.join("/")}\z}
|
98
|
+
$LOAD_PATH.find { |p|
|
99
|
+
p.match(regex)
|
100
|
+
} + "/*.rb"
|
101
|
+
end
|
102
|
+
|
103
|
+
# Convenience method to wrap the logger object.
|
104
|
+
def logger
|
105
|
+
@logger ||= set_logger
|
106
|
+
end
|
107
|
+
|
108
|
+
# attr_reader doesn't allow adding a `?` to the method name, so I think this is a false positive
|
109
|
+
# rubocop:disable Style/TrivialAccessors
|
110
|
+
|
111
|
+
# Attribute to determine when to daemonize the process.
|
112
|
+
def daemonize?
|
113
|
+
@daemonize
|
114
|
+
end
|
115
|
+
|
116
|
+
# rubocop:enable Style/TrivialAccessors
|
117
|
+
|
118
|
+
# Background the ruby process.
|
119
|
+
def daemonize
|
120
|
+
exit if fork
|
121
|
+
Process.setsid
|
122
|
+
exit if fork
|
123
|
+
Dir.chdir "/"
|
124
|
+
end
|
125
|
+
|
126
|
+
# Part of daemonizing process. Prevents application from outputting info to terminal.
|
127
|
+
def suppress_output
|
128
|
+
$stderr.reopen("/dev/null", "a")
|
129
|
+
$stdout.reopen($stderr)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Set a value to signal shutdown.
|
133
|
+
def shutdown(name)
|
134
|
+
@signal_name = name
|
135
|
+
@quit = true
|
136
|
+
end
|
137
|
+
|
138
|
+
# Handle signal events.
|
139
|
+
def trap_signals
|
140
|
+
CarrotRpc::ServerRunner::Signals.trap do |name|
|
141
|
+
# @note can't log from a trap context: since Ruby 2.0 traps don't allow mutexes as it could lead to a dead lock,
|
142
|
+
# so `logger.info` here would return "log writing failed. can't be called from trap context"
|
143
|
+
shutdown(name)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
# Determine how to create logger. Config can specify log file.
|
150
|
+
def set_logger
|
151
|
+
logger = CarrotRpc::ServerRunner::Logger.configured
|
152
|
+
CarrotRpc.configuration.logger = logger
|
153
|
+
|
154
|
+
logger
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Wrap the Logger object with convenience methods.
|
2
|
+
class CarrotRpc::TaggedLog
|
3
|
+
attr_reader :logger, :tags
|
4
|
+
|
5
|
+
def initialize(logger:, tags:)
|
6
|
+
@logger = logger
|
7
|
+
@tags = *tags
|
8
|
+
end
|
9
|
+
|
10
|
+
def level
|
11
|
+
logger.level
|
12
|
+
end
|
13
|
+
|
14
|
+
def level=(level)
|
15
|
+
logger.level = level
|
16
|
+
end
|
17
|
+
|
18
|
+
# Dyanmically define logger methods with a tagged reference. Makes filtering of logs possible.
|
19
|
+
%i(debug info warn error fatal unknown).each do |level|
|
20
|
+
define_method(level) do |msg|
|
21
|
+
logger.tagged(tags) { logger.send(level, msg) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/carrot_rpc.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# gem dependencies
|
2
|
+
require "active_support/core_ext/hash/indifferent_access"
|
3
|
+
require "active_support/core_ext/hash/except"
|
4
|
+
require "active_support/dependencies/autoload"
|
5
|
+
require "bunny"
|
6
|
+
|
7
|
+
# standard library
|
8
|
+
require "json"
|
9
|
+
require "optparse"
|
10
|
+
|
11
|
+
# project
|
12
|
+
require "carrot_rpc/version"
|
13
|
+
|
14
|
+
# An opinionated approach to doing Remote Procedure Call (RPC) with RabbitMQ and the bunny gem. CarrotRpc serves as a
|
15
|
+
# way to streamline the RPC workflow so developers can focus on the implementation and not the plumbing when working
|
16
|
+
# with RabbitMQ.
|
17
|
+
module CarrotRpc
|
18
|
+
extend ActiveSupport::Autoload
|
19
|
+
|
20
|
+
autoload :CLI
|
21
|
+
autoload :ClientServer
|
22
|
+
autoload :Configuration
|
23
|
+
autoload :Error
|
24
|
+
autoload :HashExtensions
|
25
|
+
autoload :RpcClient
|
26
|
+
autoload :RpcServer
|
27
|
+
autoload :ServerRunner
|
28
|
+
autoload :TaggedLog
|
29
|
+
|
30
|
+
class << self
|
31
|
+
attr_writer :configuration
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.configuration
|
35
|
+
@configuration ||= Configuration.new
|
36
|
+
end
|
37
|
+
|
38
|
+
# Resets the configuration back to a new instance. Should only be used in testing.
|
39
|
+
def self.reset
|
40
|
+
@configuration = Configuration.new
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.configure
|
44
|
+
yield configuration
|
45
|
+
end
|
46
|
+
end
|
data/logs/.gitkeep
ADDED
File without changes
|