nats_worker 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +116 -0
- data/bin/nats_worker +7 -0
- data/lib/nats_worker/cli.rb +69 -0
- data/lib/nats_worker/configuration.rb +22 -0
- data/lib/nats_worker/railtie.rb +18 -0
- data/lib/nats_worker/runner.rb +145 -0
- data/lib/nats_worker/tasks.rake +11 -0
- data/lib/nats_worker/version.rb +3 -0
- data/lib/nats_worker/worker.rb +78 -0
- data/lib/nats_worker.rb +35 -0
- data/nats_worker.gemspec +21 -0
- metadata +112 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 93b89424e0d096a2be58cd427a15fada391ab6666f21a628203e18c5af882cc0
|
|
4
|
+
data.tar.gz: d6252f84c3191eb33dc4f88d8423b5d272c7254d2585d6d7e61bb8875e0febe6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 75ab5a6630f6954ab17d333abb0c936359762646076047cb8435cbbe5e31ab593aeac500ec712fcc80a10ecc9ea55e087c9bff431bc13b427e8faa3807b93a7c
|
|
7
|
+
data.tar.gz: 7ef758ee93578d624bcc67ba7826383d4f00b740f9070ce2f7bc9bb179da65acaf36e60c4e312264dd03820d6bad37bb1fae3e994a6ab924dc41f3494f4f3a54
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexandr Prokopenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# nats_worker
|
|
2
|
+
|
|
3
|
+
Minimal Sneakers-like worker framework for **NATS JetStream**, powered by
|
|
4
|
+
[`nats-pure`](https://github.com/nats-io/nats-pure.rb). Designed to drop into
|
|
5
|
+
a Rails app: put workers in `app/workers`, run them via `bin/nats_worker` or
|
|
6
|
+
a rake task.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Add to your `Gemfile`:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
gem "nats_worker"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# app/workers/order_worker.rb
|
|
20
|
+
class OrderWorker
|
|
21
|
+
include NatsWorker::Worker
|
|
22
|
+
|
|
23
|
+
from_stream "ORDERS",
|
|
24
|
+
subject: "orders.>",
|
|
25
|
+
durable: "order_worker",
|
|
26
|
+
threads: 2,
|
|
27
|
+
fetch: 10,
|
|
28
|
+
ack_wait: 30
|
|
29
|
+
|
|
30
|
+
def work(msg)
|
|
31
|
+
payload = JSON.parse(msg.data)
|
|
32
|
+
Order.create!(payload)
|
|
33
|
+
# Auto-ack on success, auto-nak on raised exception.
|
|
34
|
+
# You can still call `ack!(msg)`, `nack!(msg)`, `term!(msg)` manually.
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Configuration
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
# config/initializers/nats_worker.rb
|
|
43
|
+
NatsWorker.configure do |c|
|
|
44
|
+
c.servers = ENV.fetch("NATS_URL", "nats://127.0.0.1:4222").split(",")
|
|
45
|
+
c.nats_options = { name: "my-app", reconnect_time_wait: 1 }
|
|
46
|
+
c.logger = Rails.logger
|
|
47
|
+
c.default_threads = 1
|
|
48
|
+
c.default_fetch_size = 10
|
|
49
|
+
c.default_fetch_timeout = 5
|
|
50
|
+
c.default_ack_wait = 30
|
|
51
|
+
c.shutdown_timeout = 25
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Running
|
|
56
|
+
|
|
57
|
+
Run all registered workers:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
bundle exec nats_worker
|
|
61
|
+
# or
|
|
62
|
+
bundle exec rake nats_worker:work
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Run a subset (works with both):
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
WORKERS=OrderWorker,PaymentWorker bundle exec nats_worker
|
|
69
|
+
WORKERS=OrderWorker bundle exec rake nats_worker:work
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Outside of Rails, point at a custom boot file:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
bundle exec nats_worker -r ./my_app.rb -w OrderWorker
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Ack semantics
|
|
79
|
+
|
|
80
|
+
* `work` returns normally → message is **acked**.
|
|
81
|
+
* `work` raises → message is **nak'd** (re-delivered subject to `ack_wait`).
|
|
82
|
+
* You may call `ack!(msg)` / `nack!(msg)` / `term!(msg)` explicitly; double-ack
|
|
83
|
+
is harmless.
|
|
84
|
+
|
|
85
|
+
### Signals
|
|
86
|
+
|
|
87
|
+
`SIGINT` / `SIGTERM` trigger graceful shutdown: the runner stops fetching new
|
|
88
|
+
batches, lets in-flight `work` calls finish (up to `shutdown_timeout` seconds),
|
|
89
|
+
drains the NATS connection and exits.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
|
94
|
+
|
|
95
|
+
## Tests
|
|
96
|
+
|
|
97
|
+
Unit tests:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
bundle exec rspec spec/worker_spec.rb
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
End-to-end tests run against a live NATS server with JetStream enabled. The
|
|
104
|
+
provided devcontainer (`.devcontainer/`) brings one up alongside the app
|
|
105
|
+
container, exposing it as `nats:4222`. To run them:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
# from the host:
|
|
109
|
+
cd .devcontainer && docker compose -p nats_worker up -d
|
|
110
|
+
docker exec -e NATS_URL=nats://nats:4222 nats_worker-app-1 \
|
|
111
|
+
bash -c "cd /workspaces/nats-worker && bundle exec rspec"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
If `NATS_URL` does not point at a reachable server, e2e specs are auto-skipped
|
|
115
|
+
(the `:integration` tag is filtered out).
|
|
116
|
+
|
data/bin/nats_worker
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
|
|
3
|
+
module NatsWorker
|
|
4
|
+
# Parses ARGV for `bin/nats_worker` and the rake task, loads the Rails
|
|
5
|
+
# environment when present, resolves which worker classes to run and
|
|
6
|
+
# hands them off to NatsWorker::Runner.
|
|
7
|
+
class CLI
|
|
8
|
+
def self.start(argv = ARGV)
|
|
9
|
+
new(argv).run
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(argv)
|
|
13
|
+
@argv = argv.dup
|
|
14
|
+
@options = { workers: nil, require: nil }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
parse!
|
|
19
|
+
boot_app
|
|
20
|
+
runner_classes = resolve_workers
|
|
21
|
+
Runner.new(runner_classes).run
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def parse!
|
|
27
|
+
OptionParser.new do |o|
|
|
28
|
+
o.banner = "Usage: nats_worker [options]"
|
|
29
|
+
o.on("-w", "--workers W1,W2", Array, "Worker class names to run") { |v| @options[:workers] = v }
|
|
30
|
+
o.on("-r", "--require PATH", String, "Require a file before starting") { |v| @options[:require] = v }
|
|
31
|
+
o.on("-h", "--help") { puts o; exit 0 }
|
|
32
|
+
o.on("-v", "--version") { puts "nats_worker #{NatsWorker::VERSION}"; exit 0 }
|
|
33
|
+
end.parse!(@argv)
|
|
34
|
+
|
|
35
|
+
env_workers = ENV["WORKERS"]
|
|
36
|
+
@options[:workers] ||= env_workers.split(",").map(&:strip).reject(&:empty?) if env_workers && !env_workers.empty?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def boot_app
|
|
40
|
+
if @options[:require]
|
|
41
|
+
require File.expand_path(@options[:require])
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
env_path = File.expand_path("config/environment.rb")
|
|
46
|
+
if File.exist?(env_path)
|
|
47
|
+
ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
|
|
48
|
+
require env_path
|
|
49
|
+
Rails.application.eager_load! if defined?(Rails) && Rails.respond_to?(:application)
|
|
50
|
+
else
|
|
51
|
+
require "nats_worker"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def resolve_workers
|
|
56
|
+
if @options[:workers]
|
|
57
|
+
@options[:workers].map { |name| constantize(name) }
|
|
58
|
+
else
|
|
59
|
+
NatsWorker.workers
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def constantize(name)
|
|
64
|
+
Object.const_get(name)
|
|
65
|
+
rescue NameError => e
|
|
66
|
+
raise NatsWorker::Error, "Unknown worker class #{name.inspect}: #{e.message}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "logger"
|
|
2
|
+
|
|
3
|
+
module NatsWorker
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :servers, :nats_options, :logger,
|
|
6
|
+
:default_fetch_size, :default_fetch_timeout,
|
|
7
|
+
:default_threads, :default_ack_wait,
|
|
8
|
+
:shutdown_timeout
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@servers = ENV.fetch("NATS_URL", "nats://127.0.0.1:4222").split(",")
|
|
12
|
+
@nats_options = {}
|
|
13
|
+
@logger = Logger.new($stdout)
|
|
14
|
+
@logger.level = Logger::INFO
|
|
15
|
+
@default_fetch_size = 10
|
|
16
|
+
@default_fetch_timeout = 5
|
|
17
|
+
@default_threads = 1
|
|
18
|
+
@default_ack_wait = 30
|
|
19
|
+
@shutdown_timeout = 25
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require "rails/railtie"
|
|
2
|
+
|
|
3
|
+
module NatsWorker
|
|
4
|
+
class Railtie < ::Rails::Railtie
|
|
5
|
+
railtie_name :nats_worker
|
|
6
|
+
|
|
7
|
+
rake_tasks do
|
|
8
|
+
load File.expand_path("tasks.rake", __dir__)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Make sure app/workers is autoloaded by Rails (it usually is via the
|
|
12
|
+
# default app autoload paths, but we add it explicitly to be safe).
|
|
13
|
+
initializer "nats_worker.autoload" do |app|
|
|
14
|
+
workers_path = app.root.join("app", "workers")
|
|
15
|
+
app.config.autoload_paths << workers_path.to_s if workers_path.exist?
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
require "nats/client"
|
|
2
|
+
|
|
3
|
+
module NatsWorker
|
|
4
|
+
# Runs one or more worker classes: opens a NATS connection, creates
|
|
5
|
+
# JetStream pull subscriptions and dispatches incoming messages to
|
|
6
|
+
# each worker's #work method.
|
|
7
|
+
class Runner
|
|
8
|
+
def initialize(worker_classes, config: NatsWorker.configuration)
|
|
9
|
+
@worker_classes = Array(worker_classes)
|
|
10
|
+
@config = config
|
|
11
|
+
@logger = config.logger
|
|
12
|
+
@threads = []
|
|
13
|
+
@stopping = false
|
|
14
|
+
@nats = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
raise NatsWorker::Error, "no workers to run" if @worker_classes.empty?
|
|
19
|
+
|
|
20
|
+
install_signal_handlers
|
|
21
|
+
connect!
|
|
22
|
+
|
|
23
|
+
@worker_classes.each { |klass| start_worker(klass) }
|
|
24
|
+
|
|
25
|
+
@logger.info("[nats_worker] running #{@worker_classes.size} worker(s); pid=#{Process.pid}")
|
|
26
|
+
wait_for_shutdown
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def stop
|
|
30
|
+
return if @stopping
|
|
31
|
+
|
|
32
|
+
@stopping = true
|
|
33
|
+
@logger.info("[nats_worker] shutting down...")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def install_signal_handlers
|
|
39
|
+
%w[INT TERM].each do |sig|
|
|
40
|
+
trap(sig) { stop }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def connect!
|
|
45
|
+
opts = { servers: @config.servers }.merge(@config.nats_options || {})
|
|
46
|
+
@nats = NATS.connect(**opts)
|
|
47
|
+
@logger.info("[nats_worker] connected to NATS: #{@config.servers.join(',')}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def start_worker(klass)
|
|
51
|
+
sub_cfg = klass.nats_subscription
|
|
52
|
+
threads = sub_cfg[:threads] || @config.default_threads
|
|
53
|
+
instance = klass.new
|
|
54
|
+
|
|
55
|
+
threads.times do |i|
|
|
56
|
+
@threads << Thread.new do
|
|
57
|
+
Thread.current.name = "#{klass.name}##{i}" rescue nil
|
|
58
|
+
run_pull_loop(instance, klass, sub_cfg)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def run_pull_loop(instance, klass, sub_cfg)
|
|
64
|
+
js = @nats.jetstream
|
|
65
|
+
sub = subscribe(js, sub_cfg)
|
|
66
|
+
|
|
67
|
+
fetch_size = sub_cfg[:fetch] || @config.default_fetch_size
|
|
68
|
+
fetch_timeout = sub_cfg[:fetch_timeout] || @config.default_fetch_timeout
|
|
69
|
+
|
|
70
|
+
until @stopping
|
|
71
|
+
begin
|
|
72
|
+
msgs = sub.fetch(fetch_size, timeout: fetch_timeout)
|
|
73
|
+
rescue NATS::IO::Timeout
|
|
74
|
+
next
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
@logger.error("[#{klass.name}] fetch error: #{e.class}: #{e.message}")
|
|
77
|
+
sleep 1
|
|
78
|
+
next
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
msgs.each do |msg|
|
|
82
|
+
break if @stopping
|
|
83
|
+
dispatch(instance, klass, msg)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
@logger.error("[#{klass.name}] fatal: #{e.class}: #{e.message}\n#{e.backtrace.first(10).join("\n")}")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def subscribe(js, sub_cfg)
|
|
91
|
+
subject = sub_cfg[:subject] || ">"
|
|
92
|
+
ensure_consumer(js, sub_cfg, subject)
|
|
93
|
+
js.pull_subscribe(subject, sub_cfg[:durable], stream: sub_cfg[:stream])
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def ensure_consumer(js, sub_cfg, subject)
|
|
97
|
+
js.consumer_info(sub_cfg[:stream], sub_cfg[:durable])
|
|
98
|
+
rescue NATS::JetStream::Error::NotFound
|
|
99
|
+
ack_wait = sub_cfg[:ack_wait] || @config.default_ack_wait
|
|
100
|
+
cfg = {
|
|
101
|
+
durable_name: sub_cfg[:durable],
|
|
102
|
+
ack_policy: "explicit",
|
|
103
|
+
ack_wait: ack_wait * 1_000_000_000,
|
|
104
|
+
filter_subject: subject
|
|
105
|
+
}.merge(sub_cfg[:consumer_config] || {})
|
|
106
|
+
js.add_consumer(sub_cfg[:stream], NATS::JetStream::API::ConsumerConfig.new(**cfg))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def dispatch(instance, klass, msg)
|
|
110
|
+
instance.work(msg)
|
|
111
|
+
safe_ack(msg)
|
|
112
|
+
rescue StandardError => e
|
|
113
|
+
@logger.error("[#{klass.name}] work error: #{e.class}: #{e.message}")
|
|
114
|
+
begin
|
|
115
|
+
msg.nak
|
|
116
|
+
rescue StandardError => nak_err
|
|
117
|
+
@logger.error("[#{klass.name}] nak failed: #{nak_err.class}: #{nak_err.message}")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def safe_ack(msg)
|
|
122
|
+
msg.ack
|
|
123
|
+
rescue NATS::JetStream::Error::MsgAlreadyAckd
|
|
124
|
+
# User already acked manually — fine.
|
|
125
|
+
rescue StandardError => e
|
|
126
|
+
@logger.warn("[nats_worker] ack failed: #{e.class}: #{e.message}")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def wait_for_shutdown
|
|
130
|
+
sleep 0.5 until @stopping
|
|
131
|
+
deadline = Time.now + @config.shutdown_timeout
|
|
132
|
+
@threads.each do |t|
|
|
133
|
+
remaining = deadline - Time.now
|
|
134
|
+
t.join(remaining.positive? ? remaining : 0)
|
|
135
|
+
end
|
|
136
|
+
@threads.select(&:alive?).each(&:kill)
|
|
137
|
+
@nats&.drain
|
|
138
|
+
rescue StandardError => e
|
|
139
|
+
@logger.error("[nats_worker] shutdown error: #{e.class}: #{e.message}")
|
|
140
|
+
ensure
|
|
141
|
+
@nats&.close rescue nil
|
|
142
|
+
@logger.info("[nats_worker] stopped")
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require "nats_worker/cli"
|
|
2
|
+
|
|
3
|
+
namespace :nats_worker do
|
|
4
|
+
desc "Run NATS JetStream workers (use WORKERS=Foo,Bar to limit)"
|
|
5
|
+
task work: :environment do
|
|
6
|
+
Rails.application.eager_load! if defined?(Rails)
|
|
7
|
+
argv = []
|
|
8
|
+
argv += ["--workers", ENV["WORKERS"]] if ENV["WORKERS"] && !ENV["WORKERS"].empty?
|
|
9
|
+
NatsWorker::CLI.start(argv)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module NatsWorker
|
|
2
|
+
# Mix in to make a class a NATS JetStream worker.
|
|
3
|
+
#
|
|
4
|
+
# Example:
|
|
5
|
+
# class OrderWorker
|
|
6
|
+
# include NatsWorker::Worker
|
|
7
|
+
#
|
|
8
|
+
# from_stream "ORDERS",
|
|
9
|
+
# subject: "orders.>",
|
|
10
|
+
# durable: "order_worker",
|
|
11
|
+
# threads: 2,
|
|
12
|
+
# fetch: 10,
|
|
13
|
+
# ack_wait: 30
|
|
14
|
+
#
|
|
15
|
+
# def work(msg)
|
|
16
|
+
# payload = JSON.parse(msg.data)
|
|
17
|
+
# # ... do something ...
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
module Worker
|
|
21
|
+
def self.included(base)
|
|
22
|
+
base.extend(ClassMethods)
|
|
23
|
+
NatsWorker.register(base)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The framework auto-acks on success and nacks on exception, but workers
|
|
27
|
+
# may call these explicitly if they want finer control.
|
|
28
|
+
def ack!(msg); msg.ack; end
|
|
29
|
+
def nack!(msg); msg.nak; end
|
|
30
|
+
def term!(msg); msg.term; end
|
|
31
|
+
|
|
32
|
+
def logger
|
|
33
|
+
NatsWorker.logger
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module ClassMethods
|
|
37
|
+
# Subscription configuration for this worker.
|
|
38
|
+
#
|
|
39
|
+
# @param stream [String] JetStream stream name (required)
|
|
40
|
+
# @param subject [String] subject filter (defaults to ">" inside the stream)
|
|
41
|
+
# @param durable [String] durable consumer name (defaults to class name underscored)
|
|
42
|
+
# @param threads [Integer] number of pull-loop threads
|
|
43
|
+
# @param fetch [Integer] batch size for each pull
|
|
44
|
+
# @param fetch_timeout [Integer] seconds to wait for a batch
|
|
45
|
+
# @param ack_wait [Integer] consumer ack_wait in seconds
|
|
46
|
+
# @param consumer_config [Hash] extra fields merged into the consumer config
|
|
47
|
+
def from_stream(stream, subject: nil, durable: nil, threads: nil,
|
|
48
|
+
fetch: nil, fetch_timeout: nil, ack_wait: nil,
|
|
49
|
+
consumer_config: {})
|
|
50
|
+
@nats_subscription = {
|
|
51
|
+
stream: stream,
|
|
52
|
+
subject: subject,
|
|
53
|
+
durable: durable || default_durable_name,
|
|
54
|
+
threads: threads,
|
|
55
|
+
fetch: fetch,
|
|
56
|
+
fetch_timeout: fetch_timeout,
|
|
57
|
+
ack_wait: ack_wait,
|
|
58
|
+
consumer_config: consumer_config
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def nats_subscription
|
|
63
|
+
@nats_subscription or raise NatsWorker::Error,
|
|
64
|
+
"#{name} did not declare from_stream(...)"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def default_durable_name
|
|
70
|
+
n = name.to_s.gsub("::", "/")
|
|
71
|
+
n.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
72
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
73
|
+
.tr("/", "_")
|
|
74
|
+
.downcase
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/nats_worker.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require "logger"
|
|
2
|
+
|
|
3
|
+
require "nats_worker/version"
|
|
4
|
+
require "nats_worker/configuration"
|
|
5
|
+
require "nats_worker/worker"
|
|
6
|
+
require "nats_worker/runner"
|
|
7
|
+
|
|
8
|
+
module NatsWorker
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def configure
|
|
13
|
+
yield configuration
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def configuration
|
|
17
|
+
@configuration ||= Configuration.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def logger
|
|
21
|
+
configuration.logger
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Registry of worker classes that included NatsWorker::Worker.
|
|
25
|
+
def workers
|
|
26
|
+
@workers ||= []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def register(worker_class)
|
|
30
|
+
workers << worker_class unless workers.include?(worker_class)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
require "nats_worker/railtie" if defined?(Rails::Railtie)
|
data/nats_worker.gemspec
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Gem::Specification.new do |s|
|
|
2
|
+
s.name = "nats_worker"
|
|
3
|
+
s.version = "0.1.0"
|
|
4
|
+
s.summary = "Minimal worker framework for NATS JetStream"
|
|
5
|
+
s.description = "Drop-in workers for Rails (app/workers) that consume NATS JetStream " \
|
|
6
|
+
"streams via pull-based subscriptions, using nats-pure."
|
|
7
|
+
s.authors = ["nats_worker contributors"]
|
|
8
|
+
s.license = "MIT"
|
|
9
|
+
s.required_ruby_version = ">= 2.7"
|
|
10
|
+
|
|
11
|
+
s.files = Dir["lib/**/*", "bin/*", "README.md", "LICENSE", "nats_worker.gemspec"]
|
|
12
|
+
s.bindir = "bin"
|
|
13
|
+
s.executables = ["nats_worker"]
|
|
14
|
+
s.require_paths = ["lib"]
|
|
15
|
+
|
|
16
|
+
s.add_dependency "nats-pure", ">= 2.3"
|
|
17
|
+
s.add_dependency "concurrent-ruby", ">= 1.1"
|
|
18
|
+
|
|
19
|
+
s.add_development_dependency "rspec", "~> 3.12"
|
|
20
|
+
s.add_development_dependency "rake", "~> 13.0"
|
|
21
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: nats_worker
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- nats_worker contributors
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-04 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: nats-pure
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.3'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.3'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: concurrent-ruby
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.1'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.1'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.12'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.12'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rake
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '13.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '13.0'
|
|
69
|
+
description: Drop-in workers for Rails (app/workers) that consume NATS JetStream streams
|
|
70
|
+
via pull-based subscriptions, using nats-pure.
|
|
71
|
+
email:
|
|
72
|
+
executables:
|
|
73
|
+
- nats_worker
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- LICENSE
|
|
78
|
+
- README.md
|
|
79
|
+
- bin/nats_worker
|
|
80
|
+
- lib/nats_worker.rb
|
|
81
|
+
- lib/nats_worker/cli.rb
|
|
82
|
+
- lib/nats_worker/configuration.rb
|
|
83
|
+
- lib/nats_worker/railtie.rb
|
|
84
|
+
- lib/nats_worker/runner.rb
|
|
85
|
+
- lib/nats_worker/tasks.rake
|
|
86
|
+
- lib/nats_worker/version.rb
|
|
87
|
+
- lib/nats_worker/worker.rb
|
|
88
|
+
- nats_worker.gemspec
|
|
89
|
+
homepage:
|
|
90
|
+
licenses:
|
|
91
|
+
- MIT
|
|
92
|
+
metadata: {}
|
|
93
|
+
post_install_message:
|
|
94
|
+
rdoc_options: []
|
|
95
|
+
require_paths:
|
|
96
|
+
- lib
|
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '2.7'
|
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
- - ">="
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '0'
|
|
107
|
+
requirements: []
|
|
108
|
+
rubygems_version: 3.5.22
|
|
109
|
+
signing_key:
|
|
110
|
+
specification_version: 4
|
|
111
|
+
summary: Minimal worker framework for NATS JetStream
|
|
112
|
+
test_files: []
|