carrot_rpc 0.2.3.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module CarrotRpc
2
+ VERSION = "0.2.3.pre".freeze
3
+ 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