natswork-client 0.0.1
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 +7 -0
- data/CHANGELOG.md +0 -0
- data/LICENSE +0 -0
- data/README.md +201 -0
- data/lib/active_job/queue_adapters/natswork_adapter.rb +49 -0
- data/lib/generators/natswork/install_generator.rb +38 -0
- data/lib/generators/natswork/job_generator.rb +57 -0
- data/lib/generators/natswork/templates/job.rb.erb +21 -0
- data/lib/generators/natswork/templates/job_spec.rb.erb +49 -0
- data/lib/generators/natswork/templates/natswork.rb.erb +51 -0
- data/lib/natswork/circuit_breaker.rb +229 -0
- data/lib/natswork/client/version.rb +7 -0
- data/lib/natswork/client.rb +397 -0
- data/lib/natswork/compression.rb +58 -0
- data/lib/natswork/configuration.rb +117 -0
- data/lib/natswork/connection.rb +214 -0
- data/lib/natswork/connection_pool.rb +153 -0
- data/lib/natswork/errors.rb +28 -0
- data/lib/natswork/jetstream_manager.rb +243 -0
- data/lib/natswork/job.rb +100 -0
- data/lib/natswork/logging.rb +245 -0
- data/lib/natswork/message.rb +131 -0
- data/lib/natswork/rails/console_helpers.rb +208 -0
- data/lib/natswork/rails/generators/job_generator.rb +39 -0
- data/lib/natswork/rails/generators/templates/job.rb.erb +19 -0
- data/lib/natswork/rails/generators/templates/job_spec.rb.erb +27 -0
- data/lib/natswork/rails/generators/templates/job_test.rb.erb +28 -0
- data/lib/natswork/railtie.rb +37 -0
- data/lib/natswork/registry.rb +133 -0
- data/lib/natswork/serializer.rb +68 -0
- data/lib/natswork/version.rb +5 -0
- data/lib/natswork-client.rb +4 -0
- data/lib/natswork.rb +43 -0
- metadata +159 -0
data/lib/natswork/job.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'natswork/registry'
|
4
|
+
|
5
|
+
module NatsWork
|
6
|
+
class Job
|
7
|
+
class << self
|
8
|
+
def inherited(subclass)
|
9
|
+
super
|
10
|
+
return unless subclass.name && !subclass.name.empty?
|
11
|
+
|
12
|
+
Registry.instance.register(
|
13
|
+
subclass.name,
|
14
|
+
subclass,
|
15
|
+
queue: subclass.instance_variable_get(:@queue) || 'default',
|
16
|
+
retries: subclass.instance_variable_get(:@retries),
|
17
|
+
timeout: subclass.instance_variable_get(:@timeout)
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def queue(name = nil)
|
22
|
+
if name
|
23
|
+
@queue = name
|
24
|
+
else
|
25
|
+
@queue
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def retries(count = nil)
|
30
|
+
if count
|
31
|
+
@retries = count
|
32
|
+
else
|
33
|
+
@retries
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def timeout(seconds = nil)
|
38
|
+
if seconds
|
39
|
+
@timeout = seconds
|
40
|
+
else
|
41
|
+
@timeout
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_queue
|
46
|
+
@queue || (superclass.respond_to?(:get_queue) && superclass.get_queue) || 'default'
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_retries
|
50
|
+
@retries || (superclass.respond_to?(:get_retries) && superclass.get_retries) || 3
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_timeout
|
54
|
+
@timeout || (superclass.respond_to?(:get_timeout) && superclass.get_timeout) || 30
|
55
|
+
end
|
56
|
+
|
57
|
+
def timeout_value
|
58
|
+
get_timeout
|
59
|
+
end
|
60
|
+
|
61
|
+
def perform_async(**args)
|
62
|
+
# Accept keyword arguments directly
|
63
|
+
|
64
|
+
Client.push(
|
65
|
+
job_class: name,
|
66
|
+
queue: get_queue,
|
67
|
+
max_retries: get_retries,
|
68
|
+
timeout: get_timeout,
|
69
|
+
arguments: args
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
def perform_sync(**args)
|
74
|
+
# Accept keyword arguments directly
|
75
|
+
|
76
|
+
Client.perform_sync(
|
77
|
+
job_class: name,
|
78
|
+
queue: get_queue,
|
79
|
+
max_retries: get_retries,
|
80
|
+
timeout: get_timeout,
|
81
|
+
arguments: args
|
82
|
+
)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
attr_reader :job_id, :retry_count, :metadata
|
87
|
+
attr_accessor :context
|
88
|
+
|
89
|
+
def initialize
|
90
|
+
@job_id = nil
|
91
|
+
@retry_count = 0
|
92
|
+
@metadata = {}
|
93
|
+
@context = {}
|
94
|
+
end
|
95
|
+
|
96
|
+
def perform(*args)
|
97
|
+
raise NotImplementedError, "#{self.class} must implement #perform"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,245 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module NatsWork
|
7
|
+
module Logging
|
8
|
+
def self.included(base)
|
9
|
+
base.extend(ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def logger
|
14
|
+
@logger ||= NatsWork.logger
|
15
|
+
end
|
16
|
+
|
17
|
+
def logger=(logger)
|
18
|
+
@logger = logger
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def logger
|
23
|
+
self.class.logger
|
24
|
+
end
|
25
|
+
|
26
|
+
def log_debug(message, **context)
|
27
|
+
logger.debug(format_log_message(message, context))
|
28
|
+
end
|
29
|
+
|
30
|
+
def log_info(message, **context)
|
31
|
+
logger.info(format_log_message(message, context))
|
32
|
+
end
|
33
|
+
|
34
|
+
def log_warn(message, **context)
|
35
|
+
logger.warn(format_log_message(message, context))
|
36
|
+
end
|
37
|
+
|
38
|
+
def log_error(message, error = nil, **context)
|
39
|
+
if error
|
40
|
+
context[:error] = {
|
41
|
+
class: error.class.name,
|
42
|
+
message: error.message,
|
43
|
+
backtrace: error.backtrace&.first(5)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
logger.error(format_log_message(message, context))
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def format_log_message(message, context = {})
|
53
|
+
if logger.respond_to?(:formatter) && logger.formatter.is_a?(JSONFormatter)
|
54
|
+
context
|
55
|
+
else
|
56
|
+
msg = message.dup
|
57
|
+
msg += " | #{context.to_json}" unless context.empty?
|
58
|
+
msg
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class JSONFormatter < ::Logger::Formatter
|
64
|
+
def call(severity, timestamp, progname, msg)
|
65
|
+
log_entry = {
|
66
|
+
timestamp: timestamp.utc.iso8601,
|
67
|
+
severity: severity,
|
68
|
+
progname: progname
|
69
|
+
}
|
70
|
+
|
71
|
+
if msg.is_a?(Hash)
|
72
|
+
log_entry.merge!(msg)
|
73
|
+
else
|
74
|
+
log_entry[:message] = msg
|
75
|
+
end
|
76
|
+
|
77
|
+
"#{log_entry.to_json}\n"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class << self
|
82
|
+
attr_writer :logger
|
83
|
+
|
84
|
+
def logger
|
85
|
+
@logger ||= begin
|
86
|
+
logger = ::Logger.new($stdout)
|
87
|
+
logger.level = ENV['NATSWORK_LOG_LEVEL'] ? ::Logger.const_get(ENV['NATSWORK_LOG_LEVEL'].upcase) : ::Logger::INFO
|
88
|
+
logger.progname = 'NatsWork'
|
89
|
+
|
90
|
+
logger.formatter = JSONFormatter.new if ENV['NATSWORK_LOG_FORMAT'] == 'json'
|
91
|
+
|
92
|
+
logger
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def configure_logging(options = {})
|
97
|
+
if options[:logger]
|
98
|
+
self.logger = options[:logger]
|
99
|
+
else
|
100
|
+
output = options[:output] || $stdout
|
101
|
+
level = options[:level] || ::Logger::INFO
|
102
|
+
format = options[:format] || :text
|
103
|
+
|
104
|
+
self.logger = ::Logger.new(output)
|
105
|
+
logger.level = level
|
106
|
+
logger.progname = options[:progname] || 'NatsWork'
|
107
|
+
|
108
|
+
logger.formatter = JSONFormatter.new if format == :json
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class LoggedConnection
|
114
|
+
include Logging
|
115
|
+
|
116
|
+
attr_reader :connection
|
117
|
+
|
118
|
+
def initialize(connection)
|
119
|
+
@connection = connection
|
120
|
+
setup_logging_callbacks
|
121
|
+
end
|
122
|
+
|
123
|
+
def connect
|
124
|
+
log_info 'Connecting to NATS', servers: @connection.servers
|
125
|
+
result = @connection.connect
|
126
|
+
log_info 'Connected to NATS successfully'
|
127
|
+
result
|
128
|
+
rescue StandardError => e
|
129
|
+
log_error 'Failed to connect to NATS', e, servers: @connection.servers
|
130
|
+
raise
|
131
|
+
end
|
132
|
+
|
133
|
+
def disconnect
|
134
|
+
log_info 'Disconnecting from NATS'
|
135
|
+
@connection.disconnect
|
136
|
+
log_info 'Disconnected from NATS'
|
137
|
+
end
|
138
|
+
|
139
|
+
def connected?
|
140
|
+
@connection.connected?
|
141
|
+
end
|
142
|
+
|
143
|
+
def publish(subject, payload)
|
144
|
+
log_debug 'Publishing message', subject: subject, payload_size: payload.to_s.bytesize
|
145
|
+
@connection.publish(subject, payload)
|
146
|
+
rescue StandardError => e
|
147
|
+
log_error 'Failed to publish message', e, subject: subject
|
148
|
+
raise
|
149
|
+
end
|
150
|
+
|
151
|
+
def subscribe(subject, opts = {}, &block)
|
152
|
+
log_info 'Subscribing to subject', subject: subject, options: opts
|
153
|
+
|
154
|
+
wrapped_block = proc do |msg, reply, subject, sid|
|
155
|
+
log_debug 'Received message', subject: subject, reply: reply
|
156
|
+
block.call(msg, reply, subject, sid)
|
157
|
+
rescue StandardError => e
|
158
|
+
log_error 'Error processing message', e, subject: subject
|
159
|
+
raise
|
160
|
+
end
|
161
|
+
|
162
|
+
sid = @connection.subscribe(subject, opts, &wrapped_block)
|
163
|
+
log_info 'Subscribed successfully', subject: subject, sid: sid
|
164
|
+
sid
|
165
|
+
rescue StandardError => e
|
166
|
+
log_error 'Failed to subscribe', e, subject: subject
|
167
|
+
raise
|
168
|
+
end
|
169
|
+
|
170
|
+
def request(subject, payload, opts = {})
|
171
|
+
log_debug 'Sending request', subject: subject, timeout: opts[:timeout]
|
172
|
+
start_time = Time.now
|
173
|
+
|
174
|
+
response = @connection.request(subject, payload, opts)
|
175
|
+
|
176
|
+
duration = Time.now - start_time
|
177
|
+
log_debug 'Received response', subject: subject, duration: duration
|
178
|
+
|
179
|
+
response
|
180
|
+
rescue NatsWork::TimeoutError
|
181
|
+
log_warn 'Request timed out', subject: subject, timeout: opts[:timeout]
|
182
|
+
raise
|
183
|
+
rescue StandardError => e
|
184
|
+
log_error 'Request failed', e, subject: subject
|
185
|
+
raise
|
186
|
+
end
|
187
|
+
|
188
|
+
def unsubscribe(sid)
|
189
|
+
log_debug 'Unsubscribing', sid: sid
|
190
|
+
@connection.unsubscribe(sid)
|
191
|
+
log_debug 'Unsubscribed successfully', sid: sid
|
192
|
+
end
|
193
|
+
|
194
|
+
def with_connection(&block)
|
195
|
+
@connection.with_connection(&block)
|
196
|
+
end
|
197
|
+
|
198
|
+
def jetstream
|
199
|
+
@connection.jetstream
|
200
|
+
end
|
201
|
+
|
202
|
+
def stats
|
203
|
+
@connection.stats
|
204
|
+
end
|
205
|
+
|
206
|
+
def healthy?
|
207
|
+
@connection.healthy?
|
208
|
+
end
|
209
|
+
|
210
|
+
def ping
|
211
|
+
result = @connection.ping
|
212
|
+
log_debug 'Ping result', success: result
|
213
|
+
result
|
214
|
+
end
|
215
|
+
|
216
|
+
# Delegate callback methods
|
217
|
+
def on_reconnect(&block)
|
218
|
+
@connection.on_reconnect(&block)
|
219
|
+
end
|
220
|
+
|
221
|
+
def on_disconnect(&block)
|
222
|
+
@connection.on_disconnect(&block)
|
223
|
+
end
|
224
|
+
|
225
|
+
def on_error(&block)
|
226
|
+
@connection.on_error(&block)
|
227
|
+
end
|
228
|
+
|
229
|
+
private
|
230
|
+
|
231
|
+
def setup_logging_callbacks
|
232
|
+
@connection.on_reconnect do
|
233
|
+
log_info 'Reconnected to NATS'
|
234
|
+
end
|
235
|
+
|
236
|
+
@connection.on_disconnect do
|
237
|
+
log_warn 'Disconnected from NATS'
|
238
|
+
end
|
239
|
+
|
240
|
+
@connection.on_error do |error|
|
241
|
+
log_error 'NATS connection error', error
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'securerandom'
|
5
|
+
require 'time'
|
6
|
+
require 'natswork/compression'
|
7
|
+
|
8
|
+
module NatsWork
|
9
|
+
class Message
|
10
|
+
PROTOCOL_VERSION = '1.0.0'
|
11
|
+
|
12
|
+
TYPE_JOB_DISPATCH = 'job.dispatch'
|
13
|
+
TYPE_JOB_RESULT = 'job.result'
|
14
|
+
TYPE_JOB_ERROR = 'job.error'
|
15
|
+
|
16
|
+
VALID_TYPES = [
|
17
|
+
TYPE_JOB_DISPATCH,
|
18
|
+
TYPE_JOB_RESULT,
|
19
|
+
TYPE_JOB_ERROR
|
20
|
+
].freeze
|
21
|
+
|
22
|
+
attr_accessor :type, :job_id, :job_class, :queue, :arguments,
|
23
|
+
:retry_count, :max_retries, :timeout, :reply_to,
|
24
|
+
:metadata, :created_at, :enqueued_at, :result, :error
|
25
|
+
|
26
|
+
def initialize(options = {})
|
27
|
+
@type = options[:type] || TYPE_JOB_DISPATCH
|
28
|
+
@job_id = options[:job_id] || SecureRandom.uuid
|
29
|
+
@job_class = options[:job_class]
|
30
|
+
@queue = options[:queue]
|
31
|
+
@arguments = options[:arguments] || []
|
32
|
+
@retry_count = options[:retry_count] || 0
|
33
|
+
@max_retries = options[:max_retries] || 3
|
34
|
+
@timeout = options[:timeout] || 30
|
35
|
+
@reply_to = options[:reply_to]
|
36
|
+
@metadata = options[:metadata] || {}
|
37
|
+
@created_at = options[:created_at] || Time.now.iso8601
|
38
|
+
@enqueued_at = options[:enqueued_at] || @created_at
|
39
|
+
@result = options[:result]
|
40
|
+
@error = options[:error]
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_json(*args)
|
44
|
+
hash = to_hash
|
45
|
+
|
46
|
+
# Check if arguments should be compressed
|
47
|
+
if Compression.should_compress?(@arguments)
|
48
|
+
compression_result = Compression.compress(@arguments)
|
49
|
+
if compression_result[:compressed]
|
50
|
+
hash[:arguments] = compression_result[:data]
|
51
|
+
hash[:metadata] ||= {}
|
52
|
+
hash[:metadata]['compressed'] = true
|
53
|
+
hash[:metadata]['compression_ratio'] = Compression.compression_ratio(
|
54
|
+
compression_result[:original_size],
|
55
|
+
compression_result[:compressed_size]
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
hash.to_json(*args)
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_hash
|
64
|
+
hash = {
|
65
|
+
type: @type,
|
66
|
+
job_id: @job_id,
|
67
|
+
job_class: @job_class,
|
68
|
+
queue: @queue,
|
69
|
+
arguments: @arguments,
|
70
|
+
retry_count: @retry_count,
|
71
|
+
max_retries: @max_retries,
|
72
|
+
timeout: @timeout,
|
73
|
+
metadata: @metadata,
|
74
|
+
created_at: @created_at,
|
75
|
+
enqueued_at: @enqueued_at,
|
76
|
+
protocol_version: PROTOCOL_VERSION
|
77
|
+
}
|
78
|
+
|
79
|
+
hash[:reply_to] = @reply_to if @reply_to
|
80
|
+
hash[:result] = @result if @result
|
81
|
+
hash[:error] = @error if @error
|
82
|
+
|
83
|
+
hash
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.from_json(json_string)
|
87
|
+
begin
|
88
|
+
data = JSON.parse(json_string, symbolize_names: true)
|
89
|
+
rescue JSON::ParserError => e
|
90
|
+
raise InvalidMessageError, "Invalid JSON: #{e.message}"
|
91
|
+
end
|
92
|
+
|
93
|
+
if data[:protocol_version] && data[:protocol_version] != PROTOCOL_VERSION
|
94
|
+
major_version = data[:protocol_version].split('.').first
|
95
|
+
supported_major = PROTOCOL_VERSION.split('.').first
|
96
|
+
|
97
|
+
if major_version != supported_major
|
98
|
+
raise InvalidMessageError, "Unsupported protocol version: #{data[:protocol_version]}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Decompress arguments if compressed
|
103
|
+
if data[:metadata] && data[:metadata]['compressed']
|
104
|
+
compressed_payload = {
|
105
|
+
compressed: true,
|
106
|
+
data: data[:arguments]
|
107
|
+
}
|
108
|
+
data[:arguments] = Compression.decompress(compressed_payload)
|
109
|
+
end
|
110
|
+
|
111
|
+
new(data)
|
112
|
+
end
|
113
|
+
|
114
|
+
def valid?
|
115
|
+
return false unless @job_class && @queue
|
116
|
+
return false unless VALID_TYPES.include?(@type)
|
117
|
+
|
118
|
+
true
|
119
|
+
end
|
120
|
+
|
121
|
+
def validate!
|
122
|
+
raise InvalidMessageError, 'job_class is required' unless @job_class
|
123
|
+
|
124
|
+
raise InvalidMessageError, 'queue is required' unless @queue
|
125
|
+
|
126
|
+
raise InvalidMessageError, "Invalid message type: #{@type}" unless VALID_TYPES.include?(@type)
|
127
|
+
|
128
|
+
true
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NatsWork
|
4
|
+
module Rails
|
5
|
+
module ConsoleHelpers
|
6
|
+
module_function
|
7
|
+
|
8
|
+
# List all registered jobs
|
9
|
+
def jobs
|
10
|
+
Registry.instance.all_jobs
|
11
|
+
end
|
12
|
+
|
13
|
+
# Show jobs for a specific queue
|
14
|
+
def queue_jobs(queue_name)
|
15
|
+
Registry.instance.jobs_for_queue(queue_name)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Show all queues
|
19
|
+
def queues
|
20
|
+
Registry.instance.all_queues
|
21
|
+
end
|
22
|
+
|
23
|
+
# Get client status
|
24
|
+
def status
|
25
|
+
client = Client.instance
|
26
|
+
pool = client.connection_pool
|
27
|
+
|
28
|
+
{
|
29
|
+
connected: pool&.healthy? || false,
|
30
|
+
pool_size: pool&.size || 0,
|
31
|
+
active_connections: pool&.active_connections || 0,
|
32
|
+
available_connections: pool&.available_connections || 0,
|
33
|
+
configuration: {
|
34
|
+
servers: client.configuration.servers,
|
35
|
+
namespace: client.configuration.namespace,
|
36
|
+
use_jetstream: client.configuration.use_jetstream
|
37
|
+
}
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
# Test job execution
|
42
|
+
def test_job(job_class, *args)
|
43
|
+
job = job_class.is_a?(Class) ? job_class : job_class.constantize
|
44
|
+
job_id = job.perform_async(*args)
|
45
|
+
puts "Job #{job_id} enqueued to queue '#{job.get_queue}'"
|
46
|
+
job_id
|
47
|
+
end
|
48
|
+
|
49
|
+
# Execute job synchronously (for testing)
|
50
|
+
def run_job(job_class, *args)
|
51
|
+
job = job_class.is_a?(Class) ? job_class : job_class.constantize
|
52
|
+
instance = job.new
|
53
|
+
result = instance.perform(*args)
|
54
|
+
puts "Job completed with result: #{result.inspect}"
|
55
|
+
result
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check job result
|
59
|
+
def job_result(job_id)
|
60
|
+
Client.instance.job_status(job_id)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Clear all results
|
64
|
+
def clear_results
|
65
|
+
client = Client.instance
|
66
|
+
count = client.instance_variable_get(:@result_store).size
|
67
|
+
client.instance_variable_set(:@result_store, {})
|
68
|
+
client.instance_variable_set(:@result_expiration_times, {})
|
69
|
+
puts "Cleared #{count} job results"
|
70
|
+
end
|
71
|
+
|
72
|
+
# Show scheduled jobs
|
73
|
+
def scheduled_jobs
|
74
|
+
Client.instance.instance_variable_get(:@scheduled_jobs).map do |job|
|
75
|
+
{
|
76
|
+
job_id: job[:message].job_id,
|
77
|
+
job_class: job[:message].job_class,
|
78
|
+
scheduled_for: job[:scheduled_for],
|
79
|
+
status: job[:status]
|
80
|
+
}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Cancel a job
|
85
|
+
def cancel_job(job_id)
|
86
|
+
Client.instance.cancel_job(job_id)
|
87
|
+
puts "Cancellation message sent for job #{job_id}"
|
88
|
+
true
|
89
|
+
end
|
90
|
+
|
91
|
+
# Reconnect to NATS
|
92
|
+
def reconnect!
|
93
|
+
Client.instance.reset_connection!
|
94
|
+
puts 'Reconnected to NATS'
|
95
|
+
true
|
96
|
+
end
|
97
|
+
|
98
|
+
# Show connection pool statistics
|
99
|
+
def pool_stats
|
100
|
+
pool = Client.instance.connection_pool
|
101
|
+
pool&.stats || {}
|
102
|
+
end
|
103
|
+
|
104
|
+
# Enable/disable debug logging
|
105
|
+
def debug(enabled = true)
|
106
|
+
logger = enabled ? Logger.new($stdout) : nil
|
107
|
+
Client.instance.configuration.logger = logger
|
108
|
+
puts "Debug logging #{enabled ? 'enabled' : 'disabled'}"
|
109
|
+
end
|
110
|
+
|
111
|
+
# Perform a health check
|
112
|
+
def health_check
|
113
|
+
client = Client.instance
|
114
|
+
pool = client.connection_pool
|
115
|
+
|
116
|
+
if pool.nil?
|
117
|
+
puts '❌ Connection pool not initialized'
|
118
|
+
return false
|
119
|
+
end
|
120
|
+
|
121
|
+
healthy = pool.healthy?
|
122
|
+
puts healthy ? '✅ System healthy' : '❌ System unhealthy'
|
123
|
+
|
124
|
+
# Check individual connections
|
125
|
+
pool.with_connection do |conn|
|
126
|
+
if conn.connected?
|
127
|
+
puts '✅ NATS connection active'
|
128
|
+
else
|
129
|
+
puts '❌ NATS connection inactive'
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
healthy
|
134
|
+
rescue StandardError => e
|
135
|
+
puts "❌ Health check failed: #{e.message}"
|
136
|
+
false
|
137
|
+
end
|
138
|
+
|
139
|
+
# Show memory usage
|
140
|
+
def memory_usage
|
141
|
+
kb = `ps -o rss= -p #{Process.pid}`.to_i
|
142
|
+
mb = kb / 1024.0
|
143
|
+
puts "Memory usage: #{mb.round(2)} MB"
|
144
|
+
mb
|
145
|
+
end
|
146
|
+
|
147
|
+
# Benchmark job execution
|
148
|
+
def benchmark_job(job_class, *args, count: 100)
|
149
|
+
job = job_class.is_a?(Class) ? job_class : job_class.constantize
|
150
|
+
|
151
|
+
require 'benchmark'
|
152
|
+
|
153
|
+
time = Benchmark.realtime do
|
154
|
+
count.times do
|
155
|
+
job.perform_async(*args)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
rate = count / time
|
160
|
+
puts "Enqueued #{count} jobs in #{time.round(3)} seconds"
|
161
|
+
puts "Rate: #{rate.round(2)} jobs/second"
|
162
|
+
|
163
|
+
{ time: time, count: count, rate: rate }
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Make helpers available in Rails console
|
170
|
+
if defined?(Rails::Console)
|
171
|
+
module Rails
|
172
|
+
class Console
|
173
|
+
include NatsWork::Rails::ConsoleHelpers
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Also make them available as top-level methods
|
178
|
+
include NatsWork::Rails::ConsoleHelpers
|
179
|
+
|
180
|
+
puts "NatsWork console helpers loaded. Type 'natswork_help' for available commands."
|
181
|
+
|
182
|
+
def natswork_help
|
183
|
+
puts <<~HELP
|
184
|
+
NatsWork Console Helpers:
|
185
|
+
|
186
|
+
jobs - List all registered jobs
|
187
|
+
queue_jobs(name) - Show jobs for a specific queue
|
188
|
+
queues - Show all queues
|
189
|
+
status - Get client status
|
190
|
+
|
191
|
+
test_job(class, *args) - Enqueue a job
|
192
|
+
run_job(class, *args) - Run a job synchronously
|
193
|
+
job_result(id) - Check job result
|
194
|
+
cancel_job(id) - Cancel a job
|
195
|
+
|
196
|
+
scheduled_jobs - Show scheduled jobs
|
197
|
+
clear_results - Clear all job results
|
198
|
+
|
199
|
+
reconnect! - Reconnect to NATS
|
200
|
+
pool_stats - Show connection pool stats
|
201
|
+
health_check - Perform health check
|
202
|
+
memory_usage - Show memory usage
|
203
|
+
|
204
|
+
debug(enabled) - Enable/disable debug logging
|
205
|
+
benchmark_job(class, *args, count: 100) - Benchmark job execution
|
206
|
+
HELP
|
207
|
+
end
|
208
|
+
end
|