gruf 2.6.1 → 2.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7280830d85a916cbc0f80677e072402b4336c59396927b6ea110943b59e5732
4
- data.tar.gz: 9e2de4546c670e19f7007fc713d70485e2f45d0936c508b13512f9896efeac83
3
+ metadata.gz: 8fb4c36a5bd87553944bb4056fcca8875620cffe1d623f8757db6fa0e88f9aee
4
+ data.tar.gz: b3903a27dd46302ac99db4cbfa512b7ffc6b3ad9a4faafe83ac22fe83469ae29
5
5
  SHA512:
6
- metadata.gz: b87c53908df9b5f577ff374f404f520168865092117678f4ce32b4b3264e226272965e27a243a32548bcbd32bf496fe5139801c19af549d9d641f071cda544b7
7
- data.tar.gz: b9b719dc54a055313ab7ee76d95438305989aa961c4c78332555423580371842bc51ed621eae782a2d45df3cab3c9f231d6f4e512afedfda033ed7150a65fa26
6
+ metadata.gz: ba2ccb4ffe9915044fa8026eefa6e078b746d392af52401a779ad62bcd12ae2ae92cdfce73dc8680564d635bad0f2412b2ff3c740dd0fbfb5291a6a3c3fe70f6
7
+ data.tar.gz: c3db4263ae977cede3b9f3adb9cd90984e7906f72c1d005d3b61f977bb64eb3b442e8b4a092475f6fdd9af4ad5510d7e316255bbe59613c4f19d0d8231a48edf
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@ Changelog for the gruf gem. This includes internal history before the gem was ma
2
2
 
3
3
  ### Pending release
4
4
 
5
+ ### 2.7.0
6
+
7
+ - Add hook support for executing code paths before a server is started, and after a server stops
8
+
5
9
  ### 2.6.1
6
10
 
7
11
  - Add frozen_string_literal: true to files, update rubocop to 0.68
data/README.md CHANGED
@@ -382,6 +382,46 @@ By default, the ActiveRecord Connection Reset interceptor and Output Metadata Ti
382
382
  are loaded into gruf unless explicitly told not to via the `use_default_interceptors` configuration
383
383
  parameter.
384
384
 
385
+ ## Hooks
386
+
387
+ Hooks, unlike interceptors, are executed outside of the request chain, such as when a server starts
388
+ or stops. They run in FIFO order sequentially and do not wrap one another. They can be used to provide
389
+ custom boot sequences, external instrumentation support, or shutdown alerting.
390
+
391
+ You can create a hook by extending the `Gruf::Hooks::Base` class and defining the methods
392
+ on the hook you wish to implement:
393
+
394
+ ```ruby
395
+ class MyHook < Gruf::Hooks::Base
396
+ def before_server_start(server:)
397
+ # do my thing before the server starts
398
+ end
399
+
400
+ def after_server_stop(server:)
401
+ # do my thing after the server stops
402
+ end
403
+ end
404
+
405
+ # Then in an initializer:
406
+
407
+ Gruf.configure do |c|
408
+ c.hooks.use(MyHook, option_foo: 'value 123')
409
+ end
410
+ ```
411
+
412
+ Exceptions raised in hooks will halt the execution chain and bubble up the stack appropriately.
413
+
414
+ ### Available Hook Insertion Points
415
+
416
+ Current hook insertion points are:
417
+
418
+ * `before_server_start` - Right before the gRPC server starts
419
+ * `after_server_stop` - Right after the gRPC server is shutdown
420
+
421
+ Note that exceptions raised in `before_server_start` will halt the execution chain for the remaining
422
+ `before_server_start` hooks, but will still execute the `after_server_stop` hooks as expected. Exceptions raised
423
+ in `after_server_stop` will prevent further `after_server_stop` hooks from running.
424
+
385
425
  ## Instrumentation
386
426
 
387
427
  gruf comes out of the box with a couple of instrumentation interceptors packed in:
data/lib/gruf.rb CHANGED
@@ -30,6 +30,9 @@ require_relative 'gruf/controllers/base'
30
30
  require_relative 'gruf/outbound/request_context'
31
31
  require_relative 'gruf/interceptors/registry'
32
32
  require_relative 'gruf/interceptors/base'
33
+ require_relative 'gruf/hooks/registry'
34
+ require_relative 'gruf/hooks/executor'
35
+ require_relative 'gruf/hooks/base'
33
36
  require_relative 'gruf/timer'
34
37
  require_relative 'gruf/response'
35
38
  require_relative 'gruf/error'
@@ -26,22 +26,39 @@ module Gruf
26
26
  ##
27
27
  # @param [Hash|ARGV]
28
28
  #
29
- def initialize(args = ARGV)
29
+ def initialize(
30
+ args = ARGV,
31
+ server: nil,
32
+ services: nil,
33
+ hook_executor: nil,
34
+ logger: nil
35
+ )
30
36
  @args = args
31
- setup!
37
+ setup! # ensure we set some defaults from CLI here so we can allow configuration
38
+ @services = services || Gruf.services
39
+ @hook_executor = hook_executor || Gruf::Hooks::Executor.new(hooks: Gruf.hooks&.prepare)
40
+ @server = server || Gruf::Server.new(Gruf.server_options)
41
+ @logger = logger || Gruf.logger || ::Logger.new(STDERR)
32
42
  end
33
43
 
34
44
  ##
35
45
  # Run the server
36
46
  #
37
47
  def run
38
- server = Gruf::Server.new(Gruf.server_options)
39
- Gruf.services.each { |s| server.add_service(s) }
40
- server.start!
41
- rescue StandardError => e
42
- msg = "FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}"
43
- logger = Gruf.logger || ::Logger.new(STDERR)
44
- logger.fatal msg
48
+ exception = nil
49
+ begin
50
+ @services.each { |s| @server.add_service(s) }
51
+ @hook_executor.call(:before_server_start, server: @server)
52
+ @server.start!
53
+ rescue StandardError => e
54
+ exception = e
55
+ # Catch the exception here so that we always ensure the post hook runs
56
+ # This allows systems wanting to provide external server instrumentation
57
+ # the ability to properly handle server failures
58
+ @logger.fatal("FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}")
59
+ end
60
+ @hook_executor.call(:after_server_stop, server: @server)
61
+ raise exception if exception
45
62
  end
46
63
 
47
64
  private
@@ -25,6 +25,7 @@ module Gruf
25
25
  server_binding_url: '0.0.0.0:9001',
26
26
  server_options: {},
27
27
  interceptors: nil,
28
+ hooks: nil,
28
29
  default_client_host: '',
29
30
  use_ssl: false,
30
31
  ssl_crt_file: '',
@@ -99,6 +100,7 @@ module Gruf
99
100
  send((k.to_s + '='), v)
100
101
  end
101
102
  self.interceptors = Gruf::Interceptors::Registry.new
103
+ self.hooks = Gruf::Hooks::Registry.new
102
104
  self.root_path = Rails.root.to_s.chomp('/') if defined?(Rails)
103
105
  if defined?(Rails) && Rails.logger
104
106
  self.logger = Rails.logger
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ module Hooks
20
+ ##
21
+ # Base class for a hook that allows execution at various points of Gruf server processes
22
+ #
23
+ class Base
24
+ include Gruf::Loggable
25
+
26
+ ##
27
+ # @param [Hash] options
28
+ #
29
+ def initialize(options: nil)
30
+ @options = options || {}
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ module Hooks
20
+ ##
21
+ # Base class for a hook that allows execution at various points of gRPC server processes
22
+ #
23
+ class Executor
24
+ include Gruf::Loggable
25
+
26
+ def initialize(hooks: nil)
27
+ @hooks = hooks || Gruf.hooks&.prepare || []
28
+ end
29
+
30
+ ##
31
+ # Execute a hook point for each registered hook in the registry
32
+ #
33
+ # @param [Symbol] name
34
+ # @param [Hash] arguments
35
+ #
36
+ def call(name, arguments = {})
37
+ name = name.to_sym
38
+
39
+ @hooks.each do |hook|
40
+ next unless hook.respond_to?(name)
41
+
42
+ hook.send(name, arguments)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ module Hooks
20
+ ##
21
+ # Handles registration of hooks
22
+ #
23
+ class Registry
24
+ class HookNotFoundError < StandardError; end
25
+
26
+ def initialize
27
+ @registry = []
28
+ end
29
+
30
+ ##
31
+ # Add a hook to the registry
32
+ #
33
+ # @param [Class] hook_class The class of the hook to add
34
+ # @param [Hash] options A hash of options to pass into the hook during initialization
35
+ #
36
+ def use(hook_class, options = {})
37
+ hooks_mutex do
38
+ @registry << {
39
+ klass: hook_class,
40
+ options: options
41
+ }
42
+ end
43
+ end
44
+
45
+ ##
46
+ # Remove a hook from the registry
47
+ #
48
+ # @param [Class] hook_class The hook class to remove
49
+ # @raise [HookNotFoundError] if the hook is not found
50
+ #
51
+ def remove(hook_class)
52
+ pos = @registry.find_index { |opts| opts.fetch(:klass, '') == hook_class }
53
+ raise HookNotFoundError if pos.nil?
54
+
55
+ @registry.delete_at(pos)
56
+ end
57
+
58
+ ##
59
+ # Insert a hook before another specified hook
60
+ #
61
+ # @param [Class] before_class The hook to insert before
62
+ # @param [Class] hook_class The class of the hook to add
63
+ # @param [Hash] options A hash of options to pass into the hook during initialization
64
+ # @raise [HookNotFoundError] if the before hook is not found
65
+ #
66
+ def insert_before(before_class, hook_class, options = {})
67
+ hooks_mutex do
68
+ pos = @registry.find_index { |opts| opts.fetch(:klass, '') == before_class }
69
+ raise HookNotFoundError if pos.nil?
70
+
71
+ @registry.insert(
72
+ pos,
73
+ klass: hook_class,
74
+ options: options
75
+ )
76
+ end
77
+ end
78
+
79
+ ##
80
+ # Insert a hook after another specified hook
81
+ #
82
+ # @param [Class] after_class The hook to insert after
83
+ # @param [Class] hook_class The class of the hook to add
84
+ # @param [Hash] options A hash of options to pass into the hook during initialization
85
+ # @raise [HookNotFoundError] if the after hook is not found
86
+ #
87
+ def insert_after(after_class, hook_class, options = {})
88
+ hooks_mutex do
89
+ pos = @registry.find_index { |opts| opts.fetch(:klass, '') == after_class }
90
+ raise HookNotFoundError if pos.nil?
91
+
92
+ @registry.insert(
93
+ (pos + 1),
94
+ klass: hook_class,
95
+ options: options
96
+ )
97
+ end
98
+ end
99
+
100
+ ##
101
+ # Return a list of the hook classes in the registry in their execution order
102
+ #
103
+ # @return [Array<Class>]
104
+ #
105
+ def list
106
+ hooks_mutex do
107
+ @registry.map { |h| h[:klass] }
108
+ end
109
+ end
110
+
111
+ ##
112
+ # Lazily load and return all hooks for the given request
113
+ #
114
+ # @return [Array<Gruf::Hooks::Base>]
115
+ #
116
+ def prepare
117
+ is = []
118
+ hooks_mutex do
119
+ @registry.each do |o|
120
+ is << o[:klass].new(options: o[:options])
121
+ end
122
+ end
123
+ is
124
+ end
125
+
126
+ ##
127
+ # Clear the registry
128
+ #
129
+ def clear
130
+ hooks_mutex do
131
+ @registry = []
132
+ end
133
+ end
134
+
135
+ ##
136
+ # @return [Integer] The number of hooks currently loaded
137
+ #
138
+ def count
139
+ hooks_mutex do
140
+ @registry ||= []
141
+ @registry.count
142
+ end
143
+ end
144
+
145
+ private
146
+
147
+ ##
148
+ # Handle mutations to the hook registry in a thread-safe manner
149
+ #
150
+ def hooks_mutex(&block)
151
+ @hooks_mutex ||= begin
152
+ require 'monitor'
153
+ Monitor.new
154
+ end
155
+ @hooks_mutex.synchronize(&block)
156
+ end
157
+ end
158
+ end
159
+ end
data/lib/gruf/version.rb CHANGED
@@ -16,5 +16,5 @@
16
16
  # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
17
  #
18
18
  module Gruf
19
- VERSION = '2.6.1'
19
+ VERSION = '2.7.0'
20
20
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gruf
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.1
4
+ version: 2.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shaun McCormick
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-01 00:00:00.000000000 Z
11
+ date: 2019-05-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -148,6 +148,9 @@ files:
148
148
  - lib/gruf/errors/debug_info.rb
149
149
  - lib/gruf/errors/field.rb
150
150
  - lib/gruf/errors/helpers.rb
151
+ - lib/gruf/hooks/base.rb
152
+ - lib/gruf/hooks/executor.rb
153
+ - lib/gruf/hooks/registry.rb
151
154
  - lib/gruf/instrumentable_grpc_server.rb
152
155
  - lib/gruf/integrations/rails/railtie.rb
153
156
  - lib/gruf/interceptors/active_record/connection_reset.rb
@@ -193,8 +196,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
193
196
  - !ruby/object:Gem::Version
194
197
  version: '0'
195
198
  requirements: []
196
- rubyforge_project:
197
- rubygems_version: 2.7.7
199
+ rubygems_version: 3.0.2
198
200
  signing_key:
199
201
  specification_version: 4
200
202
  summary: gRPC Ruby Framework