gruf-queue 0.1.1 → 0.1.2
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/CHANGELOG.md +31 -2
- data/lib/gruf/queue/configuration.rb +6 -49
- data/lib/gruf/queue/interceptors/connection_reset.rb +6 -78
- data/lib/gruf/queue/plugin.rb +8 -63
- data/lib/gruf/queue/pool.rb +7 -74
- data/lib/gruf/queue/pool_enhancements.rb +26 -0
- data/lib/gruf/queue/queued_rpc_server.rb +7 -30
- data/lib/gruf/queue/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 93e75c950c181dceecb26c7bb037014232fb190602c85800702c6b8bf29329c7
|
4
|
+
data.tar.gz: a81edb2a6d96d1c514c850b20ed0422eeed701e296497149df0e57597cc1875e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6d6f81af6800c4f8aca41a5400609e147b011b24ae9c6864b3caede6492889e7b9b8e9912274828f9bb35f57793b93355a50b918527d9383b3a719ad7346e509
|
7
|
+
data.tar.gz: e4efc9c13fe4c3867f65312175f65896e6dcd1f65c52ecbec99727ccb2a62e3c4f527ba7afc941ff3ec53faa9c212175b28bf286f753eee6190e47119eb41cc4
|
data/CHANGELOG.md
CHANGED
@@ -5,7 +5,36 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
-
## [0.1.
|
8
|
+
## [0.1.2]
|
9
|
+
|
10
|
+
### Changed
|
11
|
+
- **BREAKING**: Simplified logging system by removing custom log helpers
|
12
|
+
- Removed all custom logging methods (log_structured, log_debug, log_error)
|
13
|
+
- Simplified error handling to use Gruf.logger directly only where critical
|
14
|
+
- Eliminated redundant logging as it's not the gem's responsibility
|
15
|
+
- Removed excessive YARD documentation and inline comments for better readability
|
16
|
+
- Replaced const replacement with safer module prepend pattern
|
17
|
+
- Enhanced GRPC::Pool using PoolEnhancements module without class replacement
|
18
|
+
- Improved error handling and recovery mechanisms
|
19
|
+
- Updated test suite to handle new module prepend architecture
|
20
|
+
|
21
|
+
### Improved
|
22
|
+
- Code is now more readable with essential documentation only
|
23
|
+
- Reduced complexity following single responsibility principle
|
24
|
+
- Applications handle their own logging strategy
|
25
|
+
- Fixed all rubocop style issues
|
26
|
+
|
27
|
+
### Fixed
|
28
|
+
- **CRITICAL**: Fixed parameter passing to parent GRPC::RpcServer class
|
29
|
+
- Fixed Rails initialization timing issues with auto-install
|
30
|
+
- Fixed thread pool management with proper parameter inheritance
|
31
|
+
|
32
|
+
### Added
|
33
|
+
- PoolEnhancements module for clean GRPC::Pool extension
|
34
|
+
- Improved thread safety and pool management
|
35
|
+
- Better error isolation in worker threads
|
36
|
+
|
37
|
+
## [0.1.0]
|
9
38
|
|
10
39
|
### Added
|
11
40
|
- Initial release of gruf-queue
|
@@ -28,4 +57,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
28
57
|
- **Observability**: Structured logging with metadata for monitoring and debugging
|
29
58
|
- **Customization**: Flexible configuration options for different deployment scenarios
|
30
59
|
|
31
|
-
[0.1.0]: https://github.com/ether-moon/gruf-queue/releases/tag/v0.1.0
|
60
|
+
[0.1.0]: https://github.com/ether-moon/gruf-queue/releases/tag/v0.1.0
|
@@ -12,10 +12,6 @@ module Gruf
|
|
12
12
|
module Configuration
|
13
13
|
module_function
|
14
14
|
|
15
|
-
# Apply queue-specific configuration to Gruf.
|
16
|
-
#
|
17
|
-
# @return [void]
|
18
|
-
# @raise [StandardError] if configuration fails
|
19
15
|
def configure
|
20
16
|
return if configured?
|
21
17
|
|
@@ -25,83 +21,44 @@ module Gruf
|
|
25
21
|
end
|
26
22
|
|
27
23
|
@configured = true
|
28
|
-
log_debug('Gruf configuration enhanced with rpc_server support', version: Gruf::Queue::VERSION)
|
29
24
|
rescue StandardError => e
|
30
|
-
|
25
|
+
if defined?(Gruf) && Gruf.respond_to?(:logger)
|
26
|
+
Gruf.logger.error("Failed to enhance Gruf configuration: #{e.message}")
|
27
|
+
end
|
31
28
|
raise
|
32
29
|
end
|
33
30
|
end
|
34
31
|
|
35
|
-
# Check if configuration has been applied.
|
36
|
-
#
|
37
|
-
# @return [Boolean] true if configured, false otherwise
|
38
32
|
def configured?
|
39
33
|
!!@configured
|
40
34
|
end
|
41
35
|
|
42
|
-
# Reset configuration state for testing.
|
43
|
-
#
|
44
|
-
# @return [void]
|
45
|
-
# @raise [RuntimeError] if not in test environment
|
46
36
|
def reset!
|
47
37
|
raise 'reset! can only be called in test environments' unless test_environment?
|
48
38
|
|
49
39
|
@configured = false
|
50
40
|
|
51
|
-
|
52
|
-
if defined?(Gruf) && Gruf.respond_to?(:configuration)
|
53
|
-
config = Gruf.configuration
|
54
|
-
config.rpc_server = nil if config.respond_to?(:rpc_server=)
|
55
|
-
end
|
41
|
+
return unless defined?(Gruf) && Gruf.respond_to?(:configuration)
|
56
42
|
|
57
|
-
|
43
|
+
config = Gruf.configuration
|
44
|
+
config.rpc_server = nil if config.respond_to?(:rpc_server=)
|
58
45
|
end
|
59
46
|
|
60
|
-
# Enhance Gruf configuration with rpc_server support
|
61
47
|
def enhance_gruf_configuration(config)
|
62
48
|
config.define_singleton_method(:rpc_server) { @rpc_server }
|
63
49
|
config.define_singleton_method(:rpc_server=) { |val| @rpc_server = val }
|
64
50
|
config.rpc_server = nil unless config.respond_to?(:rpc_server)
|
65
51
|
|
66
|
-
# Make the configuration accessible via Gruf.configuration if not already available
|
67
52
|
return if Gruf.respond_to?(:configuration)
|
68
53
|
|
69
54
|
Gruf.define_singleton_method(:configuration) { config }
|
70
55
|
end
|
71
56
|
|
72
|
-
# Check if running in test environment
|
73
57
|
def test_environment?
|
74
58
|
ENV['RACK_ENV'] == 'test' ||
|
75
59
|
ENV['RAILS_ENV'] == 'test' ||
|
76
60
|
defined?(RSpec)
|
77
61
|
end
|
78
|
-
|
79
|
-
# Log debug message
|
80
|
-
def log_debug(message, **metadata)
|
81
|
-
return unless defined?(Gruf) && Gruf.respond_to?(:logger)
|
82
|
-
|
83
|
-
if metadata.any?
|
84
|
-
Gruf.logger.debug(metadata.merge(message: message))
|
85
|
-
else
|
86
|
-
Gruf.logger.debug(message)
|
87
|
-
end
|
88
|
-
rescue StandardError
|
89
|
-
# Ignore logging errors
|
90
|
-
end
|
91
|
-
|
92
|
-
# Log error message
|
93
|
-
def log_error(message, **metadata)
|
94
|
-
return unless defined?(Gruf) && Gruf.respond_to?(:logger)
|
95
|
-
return if test_environment? # Skip error logging in test environment
|
96
|
-
|
97
|
-
if metadata.any?
|
98
|
-
Gruf.logger.error(metadata.merge(message: message))
|
99
|
-
else
|
100
|
-
Gruf.logger.error(message)
|
101
|
-
end
|
102
|
-
rescue StandardError
|
103
|
-
# Ignore logging errors
|
104
|
-
end
|
105
62
|
end
|
106
63
|
end
|
107
64
|
end
|
@@ -14,145 +14,73 @@ module Gruf
|
|
14
14
|
# target_classes: [ActiveRecord::Base]
|
15
15
|
# )
|
16
16
|
class ConnectionReset < ::Gruf::Interceptors::ServerInterceptor
|
17
|
-
# Initialize interceptor with request context and options.
|
18
|
-
#
|
19
|
-
# @param request [Object] gRPC request object
|
20
|
-
# @param call [Object] gRPC call object
|
21
|
-
# @param method [Object] gRPC method object
|
22
|
-
# @param options [Hash] Configuration options
|
23
|
-
# @option options [Boolean] :enabled (true) Enable/disable interceptor
|
24
|
-
# @option options [Array<Class>] :target_classes Classes to reset connections for
|
25
17
|
def initialize(request, call, method, options = {})
|
26
18
|
@request = request
|
27
19
|
@call = call
|
28
20
|
@method = method
|
29
21
|
@options = options || {}
|
30
|
-
# Set default enabled value if not provided
|
31
22
|
@options[:enabled] = true unless @options.key?(:enabled)
|
32
23
|
end
|
33
24
|
|
34
|
-
# Execute request and reset connections on completion.
|
35
|
-
#
|
36
|
-
# @yield Block containing the actual request handling
|
37
|
-
# @return [Object] Result of yielded block
|
38
25
|
def call
|
39
26
|
yield
|
40
27
|
ensure
|
41
28
|
begin
|
42
29
|
reset_connections if enabled?
|
43
|
-
rescue StandardError
|
44
|
-
#
|
45
|
-
log_message(:error, "Connection reset failed: #{e.message}")
|
30
|
+
rescue StandardError
|
31
|
+
# Ignore connection reset errors silently
|
46
32
|
end
|
47
33
|
end
|
48
34
|
|
49
35
|
private
|
50
36
|
|
51
|
-
# Get interceptor configuration options.
|
52
|
-
#
|
53
|
-
# @return [Hash] Configuration options
|
54
37
|
attr_reader :options
|
55
38
|
|
56
|
-
# Check if interceptor is enabled and has valid target classes.
|
57
|
-
#
|
58
|
-
# @return [Boolean] true if should execute, false otherwise
|
59
39
|
def enabled?
|
60
|
-
# Check if explicitly disabled
|
61
40
|
return false if options[:enabled] == false
|
62
|
-
|
63
|
-
# If no target classes are provided and ActiveRecord is not available, disable
|
64
41
|
return false if target_classes.empty?
|
65
42
|
|
66
43
|
true
|
67
44
|
end
|
68
45
|
|
69
|
-
# Reset connections for all valid target classes.
|
70
|
-
#
|
71
|
-
# @return [void]
|
72
46
|
def reset_connections
|
73
47
|
validated_classes = validate_target_classes
|
74
48
|
return if validated_classes.empty?
|
75
49
|
|
76
|
-
reset_count = 0
|
77
50
|
validated_classes.each do |klass|
|
78
|
-
|
51
|
+
reset_connection_for_class(klass)
|
79
52
|
end
|
80
|
-
|
81
|
-
log_reset_summary(reset_count, validated_classes.size) if reset_count.positive?
|
82
53
|
end
|
83
54
|
|
84
55
|
def target_classes
|
85
|
-
# Use instance variable if set (for testing)
|
86
56
|
return Array(@target_classes).compact if instance_variable_defined?(:@target_classes)
|
87
57
|
|
88
|
-
# Otherwise use options or default
|
89
58
|
default_classes = defined?(::ActiveRecord::Base) ? [::ActiveRecord::Base] : []
|
90
59
|
classes = options.fetch(:target_classes, default_classes)
|
91
60
|
Array(classes).compact
|
92
61
|
end
|
93
62
|
|
94
|
-
# Validate and filter target classes to ensure they support connection handling
|
95
63
|
def validate_target_classes
|
96
64
|
target_classes.select do |klass|
|
97
65
|
valid_class?(klass)
|
98
66
|
end
|
99
67
|
end
|
100
68
|
|
101
|
-
# Check if a class is valid for connection reset capability
|
102
69
|
def valid_class?(klass)
|
103
|
-
unless klass.is_a?(Class)
|
104
|
-
|
105
|
-
return false
|
106
|
-
end
|
107
|
-
|
108
|
-
unless klass.respond_to?(:connection_handler)
|
109
|
-
log_validation_warning(klass, 'does not respond to connection_handler')
|
110
|
-
return false
|
111
|
-
end
|
70
|
+
return false unless klass.is_a?(Class)
|
71
|
+
return false unless klass.respond_to?(:connection_handler)
|
112
72
|
|
113
73
|
true
|
114
74
|
end
|
115
75
|
|
116
|
-
# Reset connections for a single class
|
117
76
|
def reset_connection_for_class(klass)
|
118
77
|
handler = klass.connection_handler
|
119
78
|
handler.clear_active_connections!(:all) if handler.respond_to?(:clear_active_connections!)
|
120
79
|
handler.clear_reloadable_connections! if handler.respond_to?(:clear_reloadable_connections!)
|
121
80
|
true
|
122
|
-
rescue StandardError
|
123
|
-
log_reset_error(klass, e)
|
81
|
+
rescue StandardError
|
124
82
|
false
|
125
83
|
end
|
126
|
-
|
127
|
-
# Log validation warnings
|
128
|
-
def log_validation_warning(klass, reason)
|
129
|
-
message = "Skipping connection reset for #{klass.inspect}: #{reason}"
|
130
|
-
log_message(:debug, message)
|
131
|
-
end
|
132
|
-
|
133
|
-
# Log connection reset errors
|
134
|
-
def log_reset_error(klass, error)
|
135
|
-
message = "Failed to reset connections for #{klass}: #{error.message}"
|
136
|
-
log_message(:warn, message)
|
137
|
-
end
|
138
|
-
|
139
|
-
# Log reset operation summary
|
140
|
-
def log_reset_summary(reset_count, total_count)
|
141
|
-
message = "Reset connections for #{reset_count}/#{total_count} classes"
|
142
|
-
log_message(:debug, message)
|
143
|
-
end
|
144
|
-
|
145
|
-
# Centralized logging with fallback chain
|
146
|
-
def log_message(level, message)
|
147
|
-
if defined?(Rails) && Rails.logger.respond_to?(level)
|
148
|
-
Rails.logger.public_send(level, message)
|
149
|
-
elsif defined?(GRPC) && GRPC.logger.respond_to?(level)
|
150
|
-
GRPC.logger.public_send(level, message)
|
151
|
-
elsif level == :warn
|
152
|
-
# Fallback to stderr for critical messages
|
153
|
-
warn(message)
|
154
|
-
end
|
155
|
-
end
|
156
84
|
end
|
157
85
|
end
|
158
86
|
end
|
data/lib/gruf/queue/plugin.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'pool_enhancements'
|
4
|
+
|
3
5
|
module Gruf
|
4
6
|
module Queue
|
5
7
|
# Plugin management for gruf-queue integration.
|
@@ -14,18 +16,12 @@ module Gruf
|
|
14
16
|
# Gruf::Queue::Plugin.installed? # => true
|
15
17
|
class Plugin
|
16
18
|
class << self
|
17
|
-
# Install and configure the gruf-queue plugin.
|
18
|
-
# This method is idempotent and can be called multiple times safely.
|
19
|
-
#
|
20
|
-
# @return [Boolean] true if installation completed, false if already installed
|
21
19
|
def install!
|
22
20
|
return false if @installed
|
23
21
|
|
24
22
|
begin
|
25
|
-
# Configure Gruf with queue-specific settings
|
26
23
|
Configuration.configure unless Configuration.configured?
|
27
|
-
|
28
|
-
# Configure Gruf with all queue-specific settings
|
24
|
+
enhance_grpc_pool
|
29
25
|
Gruf.configure do |config|
|
30
26
|
configure_rpc_server(config)
|
31
27
|
configure_interceptors(config)
|
@@ -33,45 +29,26 @@ module Gruf
|
|
33
29
|
|
34
30
|
@installed = true
|
35
31
|
true
|
36
|
-
rescue StandardError
|
37
|
-
# Log the error but don't raise to avoid breaking the application startup
|
38
|
-
log_installation_error(e)
|
32
|
+
rescue StandardError
|
39
33
|
false
|
40
34
|
end
|
41
35
|
end
|
42
36
|
|
43
|
-
# Check installation status.
|
44
|
-
#
|
45
|
-
# @return [Boolean] true if installed, false otherwise
|
46
37
|
def installed?
|
47
38
|
!!@installed
|
48
39
|
end
|
49
40
|
|
50
|
-
# Reset installation state for testing.
|
51
|
-
#
|
52
|
-
# @return [void]
|
53
41
|
def reset!
|
54
42
|
@installed = false
|
55
|
-
# Also reset the configuration state
|
56
43
|
Configuration.reset! if Configuration.respond_to?(:reset!)
|
57
44
|
end
|
58
45
|
|
59
46
|
private
|
60
47
|
|
61
|
-
# Configure RPC server to use QueuedRpcServer.
|
62
|
-
#
|
63
|
-
# @param config [Object] Gruf configuration object
|
64
|
-
# @return [void]
|
65
|
-
# @api private
|
66
48
|
def configure_rpc_server(config)
|
67
49
|
config.rpc_server = QueuedRpcServer
|
68
50
|
end
|
69
51
|
|
70
|
-
# Configure interceptors if ActiveRecord is available.
|
71
|
-
#
|
72
|
-
# @param config [Object] Gruf configuration object
|
73
|
-
# @return [void]
|
74
|
-
# @api private
|
75
52
|
def configure_interceptors(config)
|
76
53
|
return unless defined?(ActiveRecord)
|
77
54
|
|
@@ -85,31 +62,15 @@ module Gruf
|
|
85
62
|
)
|
86
63
|
end
|
87
64
|
|
88
|
-
# Validate ActiveRecord connection handler availability.
|
89
|
-
#
|
90
|
-
# @raise [StandardError] if ActiveRecord doesn't support connection_handler
|
91
|
-
# @return [void]
|
92
|
-
# @api private
|
93
65
|
def validate_active_record_availability
|
94
66
|
return if ActiveRecord::Base.respond_to?(:connection_handler)
|
95
67
|
|
96
68
|
raise StandardError, 'ActiveRecord::Base does not support connection_handler'
|
97
69
|
end
|
98
70
|
|
99
|
-
# Ensure interceptors registry is available and initialized.
|
100
|
-
#
|
101
|
-
# Handles Rails initialization timing issues where gruf-queue loads
|
102
|
-
# before Gruf's configuration is fully initialized.
|
103
|
-
#
|
104
|
-
# @param config [Object] Gruf configuration object
|
105
|
-
# @return [void]
|
106
|
-
# @raise [StandardError] if interceptors registry cannot be initialized
|
107
|
-
# @api private
|
108
71
|
def ensure_interceptors_registry_available(config)
|
109
|
-
# Check if interceptors registry exists and is initialized
|
110
72
|
return if config.respond_to?(:interceptors) && !config.interceptors.nil?
|
111
73
|
|
112
|
-
# Initialize interceptors registry if missing
|
113
74
|
begin
|
114
75
|
require 'gruf/interceptors/registry'
|
115
76
|
rescue LoadError => e
|
@@ -119,7 +80,6 @@ module Gruf
|
|
119
80
|
if config.respond_to?(:interceptors=)
|
120
81
|
config.interceptors = ::Gruf::Interceptors::Registry.new
|
121
82
|
elsif config.respond_to?(:define_singleton_method)
|
122
|
-
# Fallback: define interceptors accessor methods dynamically
|
123
83
|
registry = ::Gruf::Interceptors::Registry.new
|
124
84
|
config.define_singleton_method(:interceptors) { registry }
|
125
85
|
config.define_singleton_method(:interceptors=) { |val| registry = val }
|
@@ -127,30 +87,15 @@ module Gruf
|
|
127
87
|
raise StandardError, 'Cannot initialize interceptors registry: configuration object lacks required methods'
|
128
88
|
end
|
129
89
|
|
130
|
-
# Verify initialization was successful
|
131
90
|
return if config.respond_to?(:interceptors) && config.interceptors.respond_to?(:use)
|
132
91
|
|
133
92
|
raise StandardError, 'Interceptors registry initialization failed: missing use method'
|
134
93
|
end
|
135
94
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
# @api private
|
141
|
-
def log_installation_error(error)
|
142
|
-
message = "Failed to install gruf-queue plugin: #{error.message}"
|
143
|
-
|
144
|
-
# Skip error logging in test environment
|
145
|
-
return if ENV['RACK_ENV'] == 'test' || ENV['RAILS_ENV'] == 'test' || defined?(RSpec)
|
146
|
-
|
147
|
-
if defined?(Rails) && Rails.logger.respond_to?(:error)
|
148
|
-
Rails.logger.error(message)
|
149
|
-
elsif defined?(GRPC) && GRPC.logger.respond_to?(:error)
|
150
|
-
GRPC.logger.error(message)
|
151
|
-
else
|
152
|
-
warn message
|
153
|
-
end
|
95
|
+
def enhance_grpc_pool
|
96
|
+
return if ::GRPC::Pool.included_modules.include?(Gruf::Queue::PoolEnhancements)
|
97
|
+
|
98
|
+
::GRPC::Pool.prepend(Gruf::Queue::PoolEnhancements)
|
154
99
|
end
|
155
100
|
end
|
156
101
|
end
|
data/lib/gruf/queue/pool.rb
CHANGED
@@ -15,141 +15,74 @@ module Gruf
|
|
15
15
|
# Default keep-alive time for worker threads (in seconds)
|
16
16
|
DEFAULT_KEEP_ALIVE = 600
|
17
17
|
|
18
|
-
# Initialize pool with specified size and keep-alive time.
|
19
|
-
#
|
20
|
-
# @param size [Integer] Number of worker threads to maintain
|
21
|
-
# @param keep_alive [Integer] Thread keep-alive time in seconds
|
22
18
|
def initialize(size, keep_alive: DEFAULT_KEEP_ALIVE)
|
23
19
|
super
|
24
20
|
end
|
25
21
|
|
26
|
-
# Get number of jobs currently waiting in queue.
|
27
|
-
#
|
28
|
-
# @return [Integer] Count of queued jobs
|
29
22
|
def jobs_waiting
|
30
23
|
@jobs&.size || 0
|
31
24
|
end
|
32
25
|
|
33
|
-
# Schedule block for execution on worker thread.
|
34
|
-
#
|
35
|
-
# @param args [Array] Arguments to pass to block
|
36
|
-
# @param blk [Proc] Block to execute
|
37
|
-
# @return [Boolean] true if scheduled, false if stopped or nil block
|
38
26
|
def schedule(*args, &blk)
|
39
27
|
return false if blk.nil?
|
40
28
|
|
41
29
|
@stop_mutex.synchronize do
|
42
|
-
if @stopped
|
43
|
-
log_structured(:warn, 'Job scheduling rejected: pool already stopped')
|
44
|
-
return false
|
45
|
-
end
|
46
|
-
log_structured(:debug, 'Job scheduled for execution')
|
30
|
+
return false if @stopped
|
47
31
|
|
48
32
|
@jobs << [blk, args]
|
49
33
|
true
|
50
34
|
end
|
51
|
-
rescue StandardError
|
52
|
-
log_structured(:error, 'Failed to schedule job', error: e.message)
|
35
|
+
rescue StandardError
|
53
36
|
false
|
54
37
|
end
|
55
38
|
|
56
|
-
# Start thread pool and create worker threads.
|
57
|
-
#
|
58
|
-
# @raise [RuntimeError] if pool is already stopped
|
59
|
-
# @return [void]
|
60
39
|
def start
|
61
40
|
@stop_mutex.synchronize do
|
62
41
|
raise 'Pool already stopped' if @stopped
|
63
42
|
end
|
64
43
|
|
65
44
|
target_size = @size.to_i
|
66
|
-
log_structured(:info, 'Starting thread pool', target_size: target_size)
|
67
45
|
|
68
46
|
until @workers.size == target_size
|
69
47
|
next_thread = create_worker_thread
|
70
48
|
@workers << next_thread if next_thread
|
71
49
|
end
|
72
|
-
|
73
|
-
log_structured(:info, 'Thread pool started', worker_count: @workers.size)
|
74
|
-
rescue StandardError => e
|
75
|
-
log_structured(:error, 'Failed to start thread pool', error: e.message)
|
76
|
-
raise
|
77
50
|
end
|
78
51
|
|
79
52
|
protected
|
80
53
|
|
81
|
-
# Create new worker thread with error handling.
|
82
|
-
#
|
83
|
-
# @return [Thread, nil] Worker thread or nil on error
|
84
54
|
def create_worker_thread
|
85
55
|
Thread.new do
|
86
56
|
catch(:exit) do
|
87
|
-
# allows { throw :exit } to kill a thread
|
88
57
|
_loop_execute_jobs
|
89
58
|
end
|
90
59
|
remove_current_thread
|
91
60
|
end
|
92
|
-
rescue StandardError
|
93
|
-
log_structured(:error, 'Failed to create worker thread', error: e.message)
|
61
|
+
rescue StandardError
|
94
62
|
nil
|
95
63
|
end
|
96
64
|
|
97
|
-
# Main worker thread loop for job execution.
|
98
|
-
#
|
99
|
-
# @return [void]
|
100
|
-
# @api private
|
101
65
|
def _loop_execute_jobs
|
102
66
|
Thread.current.name = "gruf-queue-worker-#{Thread.current.object_id}"
|
103
|
-
log_structured(:debug, 'Worker thread started', thread_id: Thread.current.object_id)
|
104
67
|
|
105
68
|
loop do
|
106
69
|
begin
|
107
70
|
blk, args = @jobs.pop
|
108
71
|
execute_job_safely(blk, args)
|
109
|
-
rescue
|
110
|
-
|
111
|
-
error: e.message,
|
112
|
-
backtrace: e.backtrace&.first(5))
|
72
|
+
rescue ThreadError, SystemStackError, IOError
|
73
|
+
# Ignore system-level errors to prevent worker thread death
|
113
74
|
end
|
114
75
|
|
115
76
|
@stop_mutex.synchronize do
|
116
77
|
return if @stopped
|
117
78
|
end
|
118
79
|
end
|
119
|
-
ensure
|
120
|
-
log_structured(:debug, 'Worker thread stopping', thread_id: Thread.current.object_id)
|
121
80
|
end
|
122
81
|
|
123
|
-
# Execute job with error isolation and logging.
|
124
|
-
#
|
125
|
-
# @param blk [Proc] Job block to execute
|
126
|
-
# @param args [Array] Arguments for the block
|
127
|
-
# @return [void]
|
128
82
|
def execute_job_safely(blk, args)
|
129
83
|
blk.call(*args)
|
130
|
-
rescue
|
131
|
-
|
132
|
-
error: e.message,
|
133
|
-
error_class: e.class.name)
|
134
|
-
# Log full backtrace only in debug mode to avoid log spam
|
135
|
-
log_structured(:debug, 'Job execution backtrace', backtrace: e.backtrace) if e.backtrace
|
136
|
-
end
|
137
|
-
|
138
|
-
# Log message with structured metadata.
|
139
|
-
#
|
140
|
-
# @param level [Symbol] Log level (:info, :warn, :error, :debug)
|
141
|
-
# @param message [String] Base log message
|
142
|
-
# @param metadata [Hash] Additional metadata to include
|
143
|
-
# @return [void]
|
144
|
-
def log_structured(level, message, **metadata)
|
145
|
-
return unless GRPC.logger.respond_to?(level)
|
146
|
-
|
147
|
-
if metadata.any?
|
148
|
-
formatted_message = "#{message} #{metadata.map { |k, v| "#{k}=#{v}" }.join(' ')}"
|
149
|
-
GRPC.logger.public_send(level, formatted_message)
|
150
|
-
else
|
151
|
-
GRPC.logger.public_send(level, message)
|
152
|
-
end
|
84
|
+
rescue SystemStackError, IOError, GRPC::Core::CallError, GRPC::BadStatus, Timeout::Error
|
85
|
+
# Ignore system/network/gRPC errors to prevent worker thread death
|
153
86
|
end
|
154
87
|
end
|
155
88
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gruf
|
4
|
+
module Queue
|
5
|
+
# Enhanced functionality for GRPC::Pool without intrusive modifications.
|
6
|
+
#
|
7
|
+
# Provides essential queue management features while preserving the original
|
8
|
+
# GRPC::Pool class structure and inheritance hierarchy.
|
9
|
+
#
|
10
|
+
# @example Usage
|
11
|
+
# GRPC::Pool.prepend(Gruf::Queue::PoolEnhancements)
|
12
|
+
module PoolEnhancements
|
13
|
+
def jobs_waiting
|
14
|
+
@jobs&.size || 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def schedule(*args, &blk)
|
18
|
+
return false if blk.nil?
|
19
|
+
|
20
|
+
super
|
21
|
+
rescue StandardError
|
22
|
+
false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -30,32 +30,19 @@ module Gruf
|
|
30
30
|
# Error message for resource exhaustion
|
31
31
|
RESOURCE_EXHAUSTED_MESSAGE = 'No free threads in thread pool'
|
32
32
|
|
33
|
-
# Initialize the QueuedRpcServer with custom pool settings.
|
34
|
-
#
|
35
|
-
# @param pool_size [Integer] Number of worker threads in the pool
|
36
|
-
# @param max_waiting_requests [Integer] Maximum requests to queue before rejecting
|
37
|
-
# @param pool_keep_alive [Integer] Thread keep-alive time in seconds
|
38
|
-
# @param poll_period [Integer] Server polling interval in seconds
|
39
|
-
# @param args [Hash] Additional arguments passed to GRPC::RpcServer
|
40
33
|
def initialize(pool_size: DEFAULT_POOL_SIZE,
|
41
34
|
max_waiting_requests: DEFAULT_MAX_WAITING_REQUESTS,
|
42
35
|
pool_keep_alive: Pool::DEFAULT_KEEP_ALIVE,
|
43
36
|
poll_period: DEFAULT_POLL_PERIOD,
|
44
37
|
**args)
|
45
|
-
super
|
38
|
+
super
|
46
39
|
|
47
40
|
@pool_size = pool_size
|
48
41
|
@max_waiting_requests = max_waiting_requests
|
49
42
|
@pool_keep_alive = pool_keep_alive
|
50
43
|
@poll_period = poll_period
|
51
|
-
|
52
|
-
@pool = Gruf::Queue::Pool.new(@pool_size, keep_alive: @pool_keep_alive)
|
53
44
|
end
|
54
45
|
|
55
|
-
# Check if server can handle the request based on current queue load.
|
56
|
-
#
|
57
|
-
# @param an_rpc [Object] The incoming RPC request
|
58
|
-
# @return [Object, false] The RPC if available, false if resource exhausted
|
59
46
|
def available?(an_rpc)
|
60
47
|
job_count = safe_job_count
|
61
48
|
return an_rpc if job_count < @max_waiting_requests
|
@@ -66,26 +53,19 @@ module Gruf
|
|
66
53
|
|
67
54
|
private
|
68
55
|
|
69
|
-
# Get current job count from pool with error handling.
|
70
|
-
#
|
71
|
-
# @return [Integer] Number of jobs waiting, or max_waiting_requests on error
|
72
56
|
def safe_job_count
|
57
|
+
return 0 unless @pool
|
58
|
+
|
73
59
|
@pool.jobs_waiting
|
74
60
|
rescue StandardError => e
|
75
|
-
|
76
|
-
# Assume maximum load if we can't determine the actual count
|
61
|
+
Gruf.logger.warn("Failed to get job count: #{e.message}") if defined?(Gruf) && Gruf.respond_to?(:logger)
|
77
62
|
@max_waiting_requests
|
78
63
|
end
|
79
64
|
|
80
|
-
# Send RESOURCE_EXHAUSTED status to client with error handling.
|
81
|
-
#
|
82
|
-
# @param an_rpc [Object] The RPC to send error response to
|
83
|
-
# @return [void]
|
84
65
|
def send_resource_exhausted_response(an_rpc)
|
85
|
-
|
66
|
+
Gruf.logger.warn('no free worker threads currently') if defined?(Gruf) && Gruf.respond_to?(:logger)
|
86
67
|
|
87
68
|
begin
|
88
|
-
# Try calling send_status directly on the RPC object first (for test mocks)
|
89
69
|
if an_rpc.respond_to?(:send_status)
|
90
70
|
an_rpc.send_status(
|
91
71
|
::GRPC::Core::StatusCodes::RESOURCE_EXHAUSTED,
|
@@ -95,7 +75,6 @@ module Gruf
|
|
95
75
|
return
|
96
76
|
end
|
97
77
|
|
98
|
-
# Create a new active call that knows that metadata hasn't been sent yet
|
99
78
|
active_call = ::GRPC::ActiveCall.new(
|
100
79
|
an_rpc.call,
|
101
80
|
NOOP_PROC,
|
@@ -110,15 +89,13 @@ module Gruf
|
|
110
89
|
RESOURCE_EXHAUSTED_MESSAGE,
|
111
90
|
)
|
112
91
|
rescue TypeError => e
|
113
|
-
# Handle test scenario where an_rpc.call is a mock object
|
114
92
|
raise unless e.message.include?('Core::Call')
|
115
93
|
|
116
|
-
# In test environment, just log the response
|
117
94
|
warn "RESOURCE_EXHAUSTED: #{RESOURCE_EXHAUSTED_MESSAGE}" if defined?(RSpec)
|
118
95
|
end
|
119
96
|
rescue StandardError => e
|
120
|
-
if
|
121
|
-
|
97
|
+
if defined?(Gruf) && Gruf.respond_to?(:logger)
|
98
|
+
Gruf.logger.error("Failed to send resource exhausted response: #{e.message}")
|
122
99
|
else
|
123
100
|
warn "Failed to send resource exhausted response: #{e.message}"
|
124
101
|
end
|
data/lib/gruf/queue/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gruf-queue
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ether Moon
|
@@ -139,6 +139,7 @@ files:
|
|
139
139
|
- lib/gruf/queue/interceptors/connection_reset.rb
|
140
140
|
- lib/gruf/queue/plugin.rb
|
141
141
|
- lib/gruf/queue/pool.rb
|
142
|
+
- lib/gruf/queue/pool_enhancements.rb
|
142
143
|
- lib/gruf/queue/queued_rpc_server.rb
|
143
144
|
- lib/gruf/queue/server_factory.rb
|
144
145
|
- lib/gruf/queue/version.rb
|