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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 34b95584309b550018488327792c81595cc1c5a2483098001dc2daae636fad0e
|
4
|
+
data.tar.gz: 58607818cac88add344df5edd697bcc157bf187f6c49cd4b30ec1e6e29d1d89b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5d5224d0a1cff03380b0d4913eadc547db7f7a1f58e758b7decfa3a4820348aaef50c8323b4a580a7f94016adbe63ec8ec5d3a03ab6e23410cf175200720c1c7
|
7
|
+
data.tar.gz: e52faff83deea12ff2a7a084b6b9fa2346779f4cbc859aacfd96f6a5ddc2b0d208a12bc36fa8add31cf9a1fe2209e3a63685e89b47d5ae730fd11501681f9cae
|
data/CHANGELOG.md
ADDED
File without changes
|
data/LICENSE
ADDED
File without changes
|
data/README.md
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
# NatsWork::Client
|
2
|
+
|
3
|
+
Job dispatching client for the NatsWork distributed job processing system.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'natswork-client'
|
11
|
+
```
|
12
|
+
|
13
|
+
And execute:
|
14
|
+
|
15
|
+
$ bundle install
|
16
|
+
|
17
|
+
## Configuration
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
NatsWork::Client.configure do |config|
|
21
|
+
config.nats_url = ENV['NATS_URL'] || 'nats://localhost:4222'
|
22
|
+
config.default_timeout = 30.0
|
23
|
+
config.logger = Rails.logger
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
### Defining Jobs
|
30
|
+
|
31
|
+
Jobs are defined as classes that include `NatsWork::Job`:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class ProcessPaymentJob < NatsWork::Job
|
35
|
+
queue 'payments'
|
36
|
+
retries 3
|
37
|
+
timeout 30
|
38
|
+
|
39
|
+
def perform(order_id, amount)
|
40
|
+
order = Order.find(order_id)
|
41
|
+
PaymentService.charge(order, amount)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
### Dispatching Jobs
|
47
|
+
|
48
|
+
#### Asynchronous Execution
|
49
|
+
|
50
|
+
Fire-and-forget job execution:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
# Basic async dispatch
|
54
|
+
ProcessPaymentJob.perform_async(order.id, 99.99)
|
55
|
+
|
56
|
+
# With options
|
57
|
+
ProcessPaymentJob.perform_in(5.minutes, order.id, 99.99)
|
58
|
+
ProcessPaymentJob.perform_at(Time.now + 1.hour, order.id, 99.99)
|
59
|
+
```
|
60
|
+
|
61
|
+
#### Synchronous Execution
|
62
|
+
|
63
|
+
Wait for job completion and get result:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
# Wait for result with timeout
|
67
|
+
result = ProcessPaymentJob.perform_sync(order.id, 99.99, timeout: 10.0)
|
68
|
+
|
69
|
+
# Handle timeout
|
70
|
+
begin
|
71
|
+
result = ProcessPaymentJob.perform_sync(order.id, 99.99, timeout: 5.0)
|
72
|
+
rescue NatsWork::TimeoutError => e
|
73
|
+
Rails.logger.error "Job timed out: #{e.message}"
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
### Advanced Features
|
78
|
+
|
79
|
+
#### Job Options
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
class CustomJob < NatsWork::Job
|
83
|
+
# Routing
|
84
|
+
queue 'critical'
|
85
|
+
|
86
|
+
# Retries
|
87
|
+
retries 5
|
88
|
+
retry_backoff :exponential
|
89
|
+
|
90
|
+
# Timeout
|
91
|
+
timeout 60
|
92
|
+
|
93
|
+
# Unique jobs
|
94
|
+
unique_for 1.hour
|
95
|
+
|
96
|
+
def perform(*)
|
97
|
+
# Job logic
|
98
|
+
end
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
#### Middleware
|
103
|
+
|
104
|
+
Add custom middleware for cross-cutting concerns:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
class LoggingMiddleware
|
108
|
+
def call(job, message)
|
109
|
+
Rails.logger.info "Dispatching #{job.class.name}"
|
110
|
+
yield
|
111
|
+
ensure
|
112
|
+
Rails.logger.info "Dispatched #{job.class.name}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
NatsWork::Client.configure do |config|
|
117
|
+
config.client_middleware do |chain|
|
118
|
+
chain.add LoggingMiddleware
|
119
|
+
end
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
#### Callbacks
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
class NotificationJob < NatsWork::Job
|
127
|
+
before_enqueue :validate_recipient
|
128
|
+
after_enqueue :log_dispatch
|
129
|
+
|
130
|
+
def perform(recipient_id, message)
|
131
|
+
# Send notification
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def validate_recipient
|
137
|
+
# Validation logic
|
138
|
+
end
|
139
|
+
|
140
|
+
def log_dispatch
|
141
|
+
Rails.logger.info "Notification queued"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
```
|
145
|
+
|
146
|
+
## Rails Integration
|
147
|
+
|
148
|
+
### Generators
|
149
|
+
|
150
|
+
```bash
|
151
|
+
# Generate a new job
|
152
|
+
rails generate natswork:job ProcessOrder
|
153
|
+
|
154
|
+
# Generate configuration
|
155
|
+
rails generate natswork:install
|
156
|
+
```
|
157
|
+
|
158
|
+
### ActiveJob Adapter
|
159
|
+
|
160
|
+
Use NatsWork as an ActiveJob backend:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
# config/application.rb
|
164
|
+
config.active_job.queue_adapter = :natswork
|
165
|
+
```
|
166
|
+
|
167
|
+
### Auto-loading
|
168
|
+
|
169
|
+
Jobs in `app/jobs` are automatically loaded in Rails.
|
170
|
+
|
171
|
+
## Testing
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
# In your test helper
|
175
|
+
require 'natswork/testing'
|
176
|
+
|
177
|
+
# Inline mode - jobs run immediately
|
178
|
+
NatsWork::Testing.inline! do
|
179
|
+
ProcessPaymentJob.perform_async(order.id, 99.99)
|
180
|
+
# Job has already completed
|
181
|
+
end
|
182
|
+
|
183
|
+
# Fake mode - jobs are stored for assertions
|
184
|
+
NatsWork::Testing.fake! do
|
185
|
+
ProcessPaymentJob.perform_async(order.id, 99.99)
|
186
|
+
|
187
|
+
expect(ProcessPaymentJob).to have_enqueued_job(order.id, 99.99)
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
## API Reference
|
192
|
+
|
193
|
+
See the [API documentation](https://rubydoc.info/gems/natswork-client) for detailed class and method documentation.
|
194
|
+
|
195
|
+
## Contributing
|
196
|
+
|
197
|
+
Bug reports and pull requests are welcome at https://github.com/yourusername/natswork.
|
198
|
+
|
199
|
+
## License
|
200
|
+
|
201
|
+
MIT License
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_job'
|
4
|
+
require 'natswork/client'
|
5
|
+
|
6
|
+
module ActiveJob
|
7
|
+
module QueueAdapters
|
8
|
+
class NatsworkAdapter
|
9
|
+
def enqueue(job)
|
10
|
+
NatsWork::Client.push(
|
11
|
+
job_class: job.class.name,
|
12
|
+
queue: job.queue_name || 'default',
|
13
|
+
arguments: job.serialize,
|
14
|
+
job_id: job.job_id,
|
15
|
+
metadata: {
|
16
|
+
'active_job' => true,
|
17
|
+
'priority' => job.priority,
|
18
|
+
'enqueued_at' => Time.now.iso8601
|
19
|
+
}
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def enqueue_at(job, timestamp)
|
24
|
+
NatsWork::Client.perform_at(
|
25
|
+
Time.at(timestamp),
|
26
|
+
job_class: job.class.name,
|
27
|
+
queue: job.queue_name || 'default',
|
28
|
+
arguments: job.serialize,
|
29
|
+
job_id: job.job_id,
|
30
|
+
metadata: {
|
31
|
+
'active_job' => true,
|
32
|
+
'priority' => job.priority,
|
33
|
+
'scheduled_at' => Time.at(timestamp).iso8601
|
34
|
+
}
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
class << self
|
39
|
+
def enqueue(job)
|
40
|
+
new.enqueue(job)
|
41
|
+
end
|
42
|
+
|
43
|
+
def enqueue_at(job, timestamp)
|
44
|
+
new.enqueue_at(job, timestamp)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators/base'
|
4
|
+
|
5
|
+
module Natswork
|
6
|
+
module Generators
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
8
|
+
desc 'Installs NatsWork and creates configuration files'
|
9
|
+
|
10
|
+
source_root File.expand_path('templates', __dir__)
|
11
|
+
|
12
|
+
def create_initializer_file
|
13
|
+
template 'natswork.rb.erb', 'config/initializers/natswork.rb'
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_config_file
|
17
|
+
template 'natswork.yml.erb', 'config/natswork.yml'
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_natswork_to_application
|
21
|
+
environment do
|
22
|
+
<<~RUBY
|
23
|
+
# Configure ActiveJob to use NatsWork
|
24
|
+
config.active_job.queue_adapter = :natswork
|
25
|
+
RUBY
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def create_jobs_directory
|
30
|
+
empty_directory 'app/jobs'
|
31
|
+
end
|
32
|
+
|
33
|
+
def display_post_install_message
|
34
|
+
readme 'POST_INSTALL' if behavior == :invoke
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators/base'
|
4
|
+
require 'rails/generators/active_job/job_generator'
|
5
|
+
|
6
|
+
module Natswork
|
7
|
+
module Generators
|
8
|
+
class JobGenerator < Rails::Generators::NamedBase
|
9
|
+
desc 'Creates a new NatsWork job class'
|
10
|
+
|
11
|
+
source_root File.expand_path('templates', __dir__)
|
12
|
+
|
13
|
+
argument :arguments, type: :array, default: [], banner: 'argument:type argument:type'
|
14
|
+
|
15
|
+
class_option :queue, type: :string, default: 'default', desc: 'Queue name for the job'
|
16
|
+
class_option :retries, type: :numeric, default: 3, desc: 'Number of retries'
|
17
|
+
class_option :timeout, type: :numeric, default: 30, desc: 'Job timeout in seconds'
|
18
|
+
|
19
|
+
def create_job_file
|
20
|
+
template 'job.rb.erb', File.join('app/jobs', class_path, "#{file_name}_job.rb")
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_test_file
|
24
|
+
if Rails.application.config.generators.test_framework == :rspec
|
25
|
+
template 'job_spec.rb.erb', File.join('spec/jobs', class_path, "#{file_name}_job_spec.rb")
|
26
|
+
else
|
27
|
+
template 'job_test.rb.erb', File.join('test/jobs', class_path, "#{file_name}_job_test.rb")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def file_name
|
34
|
+
@file_name ||= name.underscore
|
35
|
+
end
|
36
|
+
|
37
|
+
def class_name
|
38
|
+
@class_name ||= name.camelize
|
39
|
+
end
|
40
|
+
|
41
|
+
def job_class_name
|
42
|
+
"#{class_name}Job"
|
43
|
+
end
|
44
|
+
|
45
|
+
def arguments_signature
|
46
|
+
arguments.map { |arg| arg.split(':').first }.join(', ')
|
47
|
+
end
|
48
|
+
|
49
|
+
def arguments_with_types
|
50
|
+
arguments.map do |arg|
|
51
|
+
name, type = arg.split(':')
|
52
|
+
type ? "#{name} # #{type}" : name
|
53
|
+
end.join(', ')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class <%= job_class_name %> < NatsWork::Job
|
2
|
+
queue '<%= options[:queue] %>'
|
3
|
+
retries <%= options[:retries] %>
|
4
|
+
timeout <%= options[:timeout] %>
|
5
|
+
|
6
|
+
def perform(<%= arguments_with_types %>)
|
7
|
+
# TODO: Implement your job logic here
|
8
|
+
<% if arguments.any? -%>
|
9
|
+
# Arguments available: <%= arguments_signature %>
|
10
|
+
<% end -%>
|
11
|
+
|
12
|
+
# Example:
|
13
|
+
# process_data(<%= arguments_signature %>)
|
14
|
+
# send_notification
|
15
|
+
# update_records
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# Add your helper methods here
|
21
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
RSpec.describe <%= job_class_name %>, type: :job do
|
4
|
+
describe '#perform' do
|
5
|
+
<% if arguments.any? -%>
|
6
|
+
let(:job) { described_class.new }
|
7
|
+
<% arguments.each do |arg| -%>
|
8
|
+
<% name, type = arg.split(':') -%>
|
9
|
+
let(:<%= name %>) { <%= type == 'integer' ? '123' : type == 'string' ? "'test'" : 'nil' %> }
|
10
|
+
<% end -%>
|
11
|
+
|
12
|
+
it 'performs the job successfully' do
|
13
|
+
expect {
|
14
|
+
job.perform(<%= arguments.map { |a| a.split(':').first }.join(', ') %>)
|
15
|
+
}.not_to raise_error
|
16
|
+
end
|
17
|
+
<% else -%>
|
18
|
+
it 'performs the job successfully' do
|
19
|
+
job = described_class.new
|
20
|
+
expect { job.perform }.not_to raise_error
|
21
|
+
end
|
22
|
+
<% end -%>
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'job configuration' do
|
26
|
+
it 'uses the correct queue' do
|
27
|
+
expect(described_class.get_queue).to eq('<%= options[:queue] %>')
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'has the correct retry count' do
|
31
|
+
expect(described_class.get_retries).to eq(<%= options[:retries] %>)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'has the correct timeout' do
|
35
|
+
expect(described_class.get_timeout).to eq(<%= options[:timeout] %>)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '.perform_async' do
|
40
|
+
it 'enqueues the job' do
|
41
|
+
<% if arguments.any? -%>
|
42
|
+
job_id = described_class.perform_async(<%= arguments.map { |a| a.split(':').first }.join(', ') %>)
|
43
|
+
<% else -%>
|
44
|
+
job_id = described_class.perform_async
|
45
|
+
<% end -%>
|
46
|
+
expect(job_id).to be_present
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# NatsWork Configuration
|
2
|
+
require 'natswork'
|
3
|
+
|
4
|
+
NatsWork::Client.instance.configure do |config|
|
5
|
+
# NATS server connection
|
6
|
+
config.servers = ENV.fetch('NATSWORK_SERVERS', 'nats://localhost:4222').split(',')
|
7
|
+
|
8
|
+
# Connection pool settings
|
9
|
+
config.pool_size = ENV.fetch('NATSWORK_POOL_SIZE', 5).to_i
|
10
|
+
config.pool_timeout = ENV.fetch('NATSWORK_POOL_TIMEOUT', 5).to_i
|
11
|
+
|
12
|
+
# Job settings
|
13
|
+
config.max_retries = ENV.fetch('NATSWORK_MAX_RETRIES', 3).to_i
|
14
|
+
config.job_timeout = ENV.fetch('NATSWORK_JOB_TIMEOUT', 30).to_i
|
15
|
+
config.sync_timeout = ENV.fetch('NATSWORK_SYNC_TIMEOUT', 30).to_i
|
16
|
+
|
17
|
+
# Namespace for NATS subjects
|
18
|
+
config.namespace = ENV.fetch('NATSWORK_NAMESPACE', Rails.env)
|
19
|
+
|
20
|
+
# JetStream settings
|
21
|
+
config.use_jetstream = ENV.fetch('NATSWORK_USE_JETSTREAM', 'true') == 'true'
|
22
|
+
|
23
|
+
# Logging
|
24
|
+
config.logger = Rails.logger
|
25
|
+
|
26
|
+
# Authentication (optional)
|
27
|
+
# config.user = ENV['NATSWORK_USER']
|
28
|
+
# config.password = ENV['NATSWORK_PASSWORD']
|
29
|
+
# config.token = ENV['NATSWORK_TOKEN']
|
30
|
+
end
|
31
|
+
|
32
|
+
# Auto-load jobs from app/jobs in development
|
33
|
+
if Rails.env.development?
|
34
|
+
Rails.application.config.to_prepare do
|
35
|
+
Dir[Rails.root.join('app/jobs/**/*_job.rb')].each { |f| require f }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Register jobs with the registry
|
40
|
+
Rails.application.config.after_initialize do
|
41
|
+
# Auto-discover and register all job classes
|
42
|
+
Dir[Rails.root.join('app/jobs/**/*_job.rb')].each do |file|
|
43
|
+
require file
|
44
|
+
class_name = File.basename(file, '.rb').camelize
|
45
|
+
job_class = class_name.safe_constantize
|
46
|
+
|
47
|
+
if job_class && job_class < NatsWork::Job
|
48
|
+
Rails.logger.info "[NatsWork] Registered job: #{job_class.name}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NatsWork
|
4
|
+
class CircuitBreakerError < Error; end
|
5
|
+
class CircuitOpenError < CircuitBreakerError; end
|
6
|
+
|
7
|
+
class CircuitBreaker
|
8
|
+
STATES = %i[closed open half_open].freeze
|
9
|
+
|
10
|
+
attr_reader :state, :failure_count, :last_failure_time
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
@failure_threshold = options[:failure_threshold] || 5
|
14
|
+
@success_threshold = options[:success_threshold] || 2
|
15
|
+
@timeout = options[:timeout] || 60
|
16
|
+
@reset_timeout = options[:reset_timeout] || 60
|
17
|
+
|
18
|
+
@state = :closed
|
19
|
+
@failure_count = 0
|
20
|
+
@success_count = 0
|
21
|
+
@last_failure_time = nil
|
22
|
+
@mutex = Mutex.new
|
23
|
+
@on_open = options[:on_open]
|
24
|
+
@on_close = options[:on_close]
|
25
|
+
@on_half_open = options[:on_half_open]
|
26
|
+
end
|
27
|
+
|
28
|
+
def call
|
29
|
+
@mutex.synchronize do
|
30
|
+
case @state
|
31
|
+
when :open
|
32
|
+
raise CircuitOpenError, 'Circuit breaker is open' unless can_attempt_reset?
|
33
|
+
|
34
|
+
transition_to(:half_open)
|
35
|
+
|
36
|
+
when :half_open
|
37
|
+
# Allow the attempt in half-open state
|
38
|
+
when :closed
|
39
|
+
# Allow the attempt in closed state
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
begin
|
44
|
+
result = yield
|
45
|
+
on_success
|
46
|
+
result
|
47
|
+
rescue StandardError => e
|
48
|
+
on_failure(e)
|
49
|
+
raise
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def closed?
|
54
|
+
@state == :closed
|
55
|
+
end
|
56
|
+
|
57
|
+
def open?
|
58
|
+
@state == :open
|
59
|
+
end
|
60
|
+
|
61
|
+
def half_open?
|
62
|
+
@state == :half_open
|
63
|
+
end
|
64
|
+
|
65
|
+
def reset
|
66
|
+
@mutex.synchronize do
|
67
|
+
@failure_count = 0
|
68
|
+
@success_count = 0
|
69
|
+
@last_failure_time = nil
|
70
|
+
transition_to(:closed)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def trip
|
75
|
+
@mutex.synchronize do
|
76
|
+
transition_to(:open)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def stats
|
81
|
+
@mutex.synchronize do
|
82
|
+
{
|
83
|
+
state: @state,
|
84
|
+
failure_count: @failure_count,
|
85
|
+
success_count: @success_count,
|
86
|
+
last_failure_time: @last_failure_time,
|
87
|
+
time_until_retry: time_until_retry
|
88
|
+
}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def on_success
|
95
|
+
@mutex.synchronize do
|
96
|
+
case @state
|
97
|
+
when :half_open
|
98
|
+
@success_count += 1
|
99
|
+
if @success_count >= @success_threshold
|
100
|
+
@failure_count = 0
|
101
|
+
@success_count = 0
|
102
|
+
transition_to(:closed)
|
103
|
+
end
|
104
|
+
when :closed
|
105
|
+
@failure_count = 0 if @failure_count.positive?
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def on_failure(_error)
|
111
|
+
@mutex.synchronize do
|
112
|
+
@last_failure_time = Time.now
|
113
|
+
|
114
|
+
case @state
|
115
|
+
when :half_open
|
116
|
+
@failure_count += 1
|
117
|
+
transition_to(:open)
|
118
|
+
when :closed
|
119
|
+
@failure_count += 1
|
120
|
+
transition_to(:open) if @failure_count >= @failure_threshold
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def can_attempt_reset?
|
126
|
+
return false unless @last_failure_time
|
127
|
+
|
128
|
+
Time.now - @last_failure_time >= @reset_timeout
|
129
|
+
end
|
130
|
+
|
131
|
+
def time_until_retry
|
132
|
+
return 0 unless @last_failure_time && @state == :open
|
133
|
+
|
134
|
+
elapsed = Time.now - @last_failure_time
|
135
|
+
remaining = @reset_timeout - elapsed
|
136
|
+
remaining.positive? ? remaining : 0
|
137
|
+
end
|
138
|
+
|
139
|
+
def transition_to(new_state)
|
140
|
+
return if @state == new_state
|
141
|
+
|
142
|
+
@state = new_state
|
143
|
+
@success_count = 0 if new_state == :half_open
|
144
|
+
|
145
|
+
case new_state
|
146
|
+
when :open
|
147
|
+
@on_open&.call
|
148
|
+
when :closed
|
149
|
+
@on_close&.call
|
150
|
+
when :half_open
|
151
|
+
@on_half_open&.call
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class CircuitBreakedConnection
|
157
|
+
attr_reader :connection, :circuit_breaker
|
158
|
+
|
159
|
+
def initialize(connection, circuit_breaker_options = {})
|
160
|
+
@connection = connection
|
161
|
+
@circuit_breaker = CircuitBreaker.new(circuit_breaker_options)
|
162
|
+
end
|
163
|
+
|
164
|
+
def connect
|
165
|
+
@circuit_breaker.call { @connection.connect }
|
166
|
+
end
|
167
|
+
|
168
|
+
def disconnect
|
169
|
+
@connection.disconnect
|
170
|
+
end
|
171
|
+
|
172
|
+
def connected?
|
173
|
+
@connection.connected?
|
174
|
+
end
|
175
|
+
|
176
|
+
def publish(subject, payload)
|
177
|
+
@circuit_breaker.call { @connection.publish(subject, payload) }
|
178
|
+
end
|
179
|
+
|
180
|
+
def subscribe(subject, opts = {}, &block)
|
181
|
+
@circuit_breaker.call { @connection.subscribe(subject, opts, &block) }
|
182
|
+
end
|
183
|
+
|
184
|
+
def request(subject, payload, opts = {})
|
185
|
+
@circuit_breaker.call { @connection.request(subject, payload, opts) }
|
186
|
+
end
|
187
|
+
|
188
|
+
def unsubscribe(sid)
|
189
|
+
@circuit_breaker.call { @connection.unsubscribe(sid) }
|
190
|
+
end
|
191
|
+
|
192
|
+
def with_connection(&block)
|
193
|
+
@circuit_breaker.call { @connection.with_connection(&block) }
|
194
|
+
end
|
195
|
+
|
196
|
+
def jetstream
|
197
|
+
@circuit_breaker.call { @connection.jetstream }
|
198
|
+
end
|
199
|
+
|
200
|
+
def stats
|
201
|
+
connection_stats = @connection.stats
|
202
|
+
connection_stats[:circuit_breaker] = @circuit_breaker.stats
|
203
|
+
connection_stats
|
204
|
+
end
|
205
|
+
|
206
|
+
def healthy?
|
207
|
+
@circuit_breaker.closed? && @connection.healthy?
|
208
|
+
end
|
209
|
+
|
210
|
+
def ping
|
211
|
+
@circuit_breaker.call { @connection.ping }
|
212
|
+
rescue CircuitOpenError
|
213
|
+
false
|
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
|
+
end
|
229
|
+
end
|