Skip to content

Commit 89d16bd

Browse files
author
Ilya Chizhanov
committed
Add Rails Integration
1 parent 06c0012 commit 89d16bd

File tree

6 files changed

+196
-90
lines changed

6 files changed

+196
-90
lines changed

bin/gruf

+9-2
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,12 @@ end
2525
load 'config/environment.rb' if defined?(Rails)
2626
require 'gruf'
2727

28-
cli = Gruf::Cli::Executor.new
29-
cli.run
28+
begin
29+
cli = Gruf::Cli::Executor.new
30+
cli.run(ARGV)
31+
rescue => e
32+
raise e if $DEBUG
33+
STDERR.puts e.message
34+
STDERR.puts e.backtrace.join("\n")
35+
exit 1
36+
end

lib/gruf/cli/executor.rb

+98-24
Original file line numberDiff line numberDiff line change
@@ -23,59 +23,87 @@ module Cli
2323
# Handles execution of the gruf binstub, along with command-line arguments
2424
#
2525
class Executor
26+
include Gruf::Loggable
27+
2628
class NoServicesBoundError < StandardError; end
2729

30+
KILL_SIGNALS = %w[INT TERM QUIT].freeze
31+
32+
# Run CLI inside the current process and shutdown at exit
33+
def self.embed!(args = [])
34+
new(embedded: true).tap do |cli|
35+
cli.run(args)
36+
at_exit { cli.shutdown }
37+
end
38+
end
39+
40+
attr_reader :server, :embedded
41+
alias embedded? embedded
42+
2843
##
29-
# @param [Hash|ARGV]
44+
# @param [Boolean] embedded
3045
# @param [::Gruf::Server|NilClass] server
3146
# @param [Array<Class>|NilClass] services
3247
# @param [Gruf::Hooks::Executor|NilClass] hook_executor
33-
# @param [Logger|NilClass] logger
3448
#
3549
def initialize(
36-
args = ARGV,
50+
embedded: false,
3751
server: nil,
3852
services: nil,
39-
hook_executor: nil,
40-
logger: nil
53+
hook_executor: nil
4154
)
42-
@args = args
43-
setup! # ensure we set some defaults from CLI here so we can allow configuration
55+
@embedded = embedded
4456
@services = services.is_a?(Array) ? services : []
45-
@hook_executor = hook_executor || Gruf::Hooks::Executor.new(hooks: Gruf.hooks&.prepare)
46-
@server = server || Gruf::Server.new(Gruf.server_options)
47-
@logger = logger || Gruf.logger || ::Logger.new($stderr)
57+
@hook_executor = hook_executor
58+
@server = server
4859
end
4960

5061
##
5162
# Run the server
63+
# @param [Hash|ARGV] args
5264
#
53-
def run
54-
exception = nil
55-
# wait to load controllers until last possible second to allow late configuration
56-
::Gruf.autoloaders.load!(controllers_path: Gruf.controllers_path)
57-
# allow lazy registering globally as late as possible, this allows more flexible binstub injections
58-
@services = ::Gruf.services unless @services&.any?
65+
def run(args = [])
66+
@args = args
67+
setup! # ensure we set some defaults from CLI here so we can allow configuration
5968

60-
unless @services.any?
61-
raise NoServicesBoundError,
62-
'No services bound to this gruf process; please bind a service to a Gruf controller ' \
63-
'to start the server successfully'
64-
end
69+
exception = nil
6570

71+
update_proc_title(:starting)
6672
begin
6773
@services.each { |s| @server.add_service(s) }
6874
@hook_executor.call(:before_server_start, server: @server)
69-
@server.start!
75+
@server.start
7076
rescue StandardError => e
7177
exception = e
7278
# Catch the exception here so that we always ensure the post hook runs
7379
# This allows systems wanting to provide external server instrumentation
7480
# the ability to properly handle server failures
75-
@logger.fatal("FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}")
81+
logger.fatal "FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}"
82+
end
83+
update_proc_title(:serving)
84+
85+
if exception
86+
shutdown
87+
raise exception
88+
end
89+
90+
return if embedded?
91+
92+
begin
93+
wait_till_terminated
94+
rescue Interrupt => e
95+
logger.info "[gruf] Stopping... #{e.message}"
96+
97+
shutdown
98+
99+
logger.info '[gruf] Stopped. Good-bye!'
76100
end
101+
end
102+
103+
def shutdown
104+
server&.stop
77105
@hook_executor.call(:after_server_stop, server: @server)
78-
raise exception if exception
106+
update_proc_title(:stopped)
79107
end
80108

81109
private
@@ -92,6 +120,20 @@ def setup!
92120
Gruf.interceptors.remove(Gruf::Interceptors::Instrumentation::OutputMetadataTimer)
93121
end
94122
Gruf.backtrace_on_error = true if opts.backtrace_on_error?
123+
124+
@server ||= Gruf::Server.new(Gruf.server_options)
125+
@hook_executor ||= Gruf::Hooks::Executor.new(hooks: Gruf.hooks&.prepare)
126+
127+
# wait to load controllers until last possible second to allow late configuration
128+
::Gruf.autoloaders.load!(controllers_path: Gruf.controllers_path)
129+
# allow lazy registering globally as late as possible, this allows more flexible binstub injections
130+
@services = ::Gruf.services unless @services&.any?
131+
132+
unless @services.any?
133+
raise NoServicesBoundError,
134+
'No services bound to this gruf process; please bind a service to a Gruf controller ' \
135+
'to start the server successfully'
136+
end
95137
end
96138

97139
##
@@ -114,6 +156,38 @@ def parse_options
114156
end
115157
end
116158
end
159+
160+
def wait_till_terminated
161+
self_read = setup_signals
162+
163+
readable_io = IO.select([self_read]) # rubocop:disable Lint/IncompatibleIoSelectWithFiberScheduler
164+
signal = readable_io.first[0].gets.strip
165+
raise Interrupt, "SIG#{signal} received"
166+
end
167+
168+
def setup_signals
169+
self_read, self_write = IO.pipe
170+
171+
KILL_SIGNALS.each do |signal|
172+
trap signal do
173+
self_write.puts signal
174+
end
175+
end
176+
177+
self_read
178+
end
179+
180+
##
181+
# Updates proc name/title
182+
#
183+
# @param [Symbol] state
184+
#
185+
# :nocov:
186+
def update_proc_title(state)
187+
Process.setproctitle("gruf #{Gruf::VERSION} -- #{state}")
188+
end
189+
190+
# :nocov:
117191
end
118192
end
119193
end

lib/gruf/integrations/rails/railtie.rb

+8
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ class Railtie < ::Rails::Railtie
3434
end
3535
end
3636
end
37+
38+
# Since Rails 6.1
39+
if respond_to?(:server)
40+
server do
41+
require 'gruf'
42+
Gruf::Cli::Executor.embed!
43+
end
44+
end
3745
end
3846
end
3947
end

lib/gruf/server.rb

+44-32
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ module Gruf
2323
class Server
2424
class ServerAlreadyStartedError < StandardError; end
2525

26-
KILL_SIGNALS = %w[INT TERM QUIT].freeze
27-
2826
include Gruf::Loggable
2927

3028
# @!attribute [r] port
@@ -44,7 +42,6 @@ def initialize(opts = {})
4442
@interceptors = opts.fetch(:interceptor_registry, Gruf.interceptors)
4543
@interceptors = Gruf::Interceptors::Registry.new unless @interceptors.is_a?(Gruf::Interceptors::Registry)
4644
@services = nil
47-
@started = false
4845
@hostname = opts.fetch(:hostname, Gruf.server_binding_url)
4946
@event_listener_proc = opts.fetch(:event_listener_proc, Gruf.event_listener_proc)
5047
end
@@ -89,31 +86,55 @@ def server
8986
# Start the gRPC server
9087
#
9188
# :nocov:
92-
def start!
93-
update_proc_title(:starting)
89+
def start
90+
return if running?
9491

95-
server_thread = Thread.new do
96-
logger.info { "[gruf] Starting gruf server at #{@hostname}..." }
97-
server.run_till_terminated_or_interrupted(KILL_SIGNALS)
98-
end
99-
@started = true
100-
update_proc_title(:serving)
101-
server_thread.join
102-
@started = false
92+
raise 'Cannot re-start stopped server' if stopped?
93+
94+
logger.info "[gruf] Starting gruf server at #{@hostname}..."
10395

104-
update_proc_title(:stopped)
105-
logger.info { '[gruf] Goodbye!' }
96+
@server_thread = Thread.new { server.run }
97+
98+
server.wait_till_running
10699
end
100+
107101
# :nocov:
108102

103+
def wait_till_terminated
104+
raise 'Server is not running' unless running?
105+
106+
server_thread.join
107+
end
108+
109+
# Stop gRPC server if it's running
110+
def stop
111+
return unless running?
112+
113+
server.stop
114+
115+
logger.info '[gruf] gRPC server stopped'
116+
end
117+
118+
def running?
119+
return false if @server.nil?
120+
121+
server.running_state == :running
122+
end
123+
124+
def stopped?
125+
return false if @server.nil?
126+
127+
server.running_state == :stopped
128+
end
129+
109130
##
110131
# Add a gRPC service stub to be served by gruf
111132
#
112133
# @param [Class] klass
113134
# @raise [ServerAlreadyStartedError] if the server is already started
114135
#
115136
def add_service(klass)
116-
raise ServerAlreadyStartedError if @started
137+
raise ServerAlreadyStartedError if running?
117138

118139
@services << klass unless services.include?(klass)
119140
end
@@ -126,7 +147,7 @@ def add_service(klass)
126147
# @raise [ServerAlreadyStartedError] if the server is already started
127148
#
128149
def add_interceptor(klass, opts = {})
129-
raise ServerAlreadyStartedError if @started
150+
raise ServerAlreadyStartedError if running?
130151

131152
@interceptors.use(klass, opts)
132153
end
@@ -139,7 +160,7 @@ def add_interceptor(klass, opts = {})
139160
# @param [Hash] opts A hash of options for the interceptor
140161
#
141162
def insert_interceptor_before(before_class, interceptor_class, opts = {})
142-
raise ServerAlreadyStartedError if @started
163+
raise ServerAlreadyStartedError if running?
143164

144165
@interceptors.insert_before(before_class, interceptor_class, opts)
145166
end
@@ -152,7 +173,7 @@ def insert_interceptor_before(before_class, interceptor_class, opts = {})
152173
# @param [Hash] opts A hash of options for the interceptor
153174
#
154175
def insert_interceptor_after(after_class, interceptor_class, opts = {})
155-
raise ServerAlreadyStartedError if @started
176+
raise ServerAlreadyStartedError if running?
156177

157178
@interceptors.insert_after(after_class, interceptor_class, opts)
158179
end
@@ -172,7 +193,7 @@ def list_interceptors
172193
# @param [Class] klass
173194
#
174195
def remove_interceptor(klass)
175-
raise ServerAlreadyStartedError if @started
196+
raise ServerAlreadyStartedError if running?
176197

177198
@interceptors.remove(klass)
178199
end
@@ -181,13 +202,15 @@ def remove_interceptor(klass)
181202
# Clear the interceptor registry of interceptors
182203
#
183204
def clear_interceptors
184-
raise ServerAlreadyStartedError if @started
205+
raise ServerAlreadyStartedError if running?
185206

186207
@interceptors.clear
187208
end
188209

189210
private
190211

212+
attr_reader :server_thread
213+
191214
##
192215
# @return [Array<Class>]
193216
#
@@ -216,19 +239,8 @@ def ssl_credentials
216239
certs = [nil, [{ private_key: private_key, cert_chain: cert_chain }], false]
217240
GRPC::Core::ServerCredentials.new(*certs)
218241
end
219-
# :nocov:
220242

221-
##
222-
# Updates proc name/title
223-
#
224-
# @param [Symbol] state
225-
#
226243
# :nocov:
227-
def update_proc_title(state)
228-
Process.setproctitle("gruf #{Gruf::VERSION} -- #{state}")
229-
end
230-
# :nocov:
231-
#
232244

233245
##
234246
# Handle thread-safe access to the server

0 commit comments

Comments
 (0)