anway 0.1.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/LICENSE +22 -0
- data/README.md +146 -0
- data/exe/anway +6 -0
- data/lib/active_job/queue_adapters/anway_adapter.rb +65 -0
- data/lib/anway/cli.rb +176 -0
- data/lib/anway/client.rb +151 -0
- data/lib/anway/configuration.rb +32 -0
- data/lib/anway/connection.rb +195 -0
- data/lib/anway/errors.rb +34 -0
- data/lib/anway/executor.rb +29 -0
- data/lib/anway/job.rb +81 -0
- data/lib/anway/pool.rb +85 -0
- data/lib/anway/queues.rb +21 -0
- data/lib/anway/railtie.rb +23 -0
- data/lib/anway/runner.rb +259 -0
- data/lib/anway/testing.rb +48 -0
- data/lib/anway/version.rb +5 -0
- data/lib/anway/worker.rb +127 -0
- data/lib/anway.rb +59 -0
- metadata +71 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 334855b00a1b57308782f76328e3d72d2339169713022fe5cc5421961a4acfc9
|
|
4
|
+
data.tar.gz: d63ddc5085f28acc3d00174f77632f3f2a95a6c827b4d901629496ccfc0251a0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 56e3d4d168da46531849c13c713e85483d624336fd30f42eace765adbbce119c8c97e09072375e5373410750e5cded72a315eda6d6b917bb137ab57a887aa09a
|
|
7
|
+
data.tar.gz: a97fb1df5b38f972d9165fbc9eee86045abec79b5ec543684cb008dbee60f1ec0c222e7a39d7ea99ffdf3b72c80687f1747f79c5f9ebf70d76c9b0be04486bf2
|
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thadeu
|
|
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.
|
|
22
|
+
|
data/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# anway-ruby
|
|
2
|
+
|
|
3
|
+
Ruby and Rails client and worker for [Anway](https://github.com/thadeu/anway). Ruby 3.4 or later. Rails 8.1 for the Rails integration.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
gem 'anway'
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The server address comes from `ANWAY_URL`, for example `tcp://:password@localhost:7419`. Use `tcp+tls://` for TLS.
|
|
10
|
+
|
|
11
|
+
## Worker
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
class WelcomeJob
|
|
15
|
+
include Anway::Worker
|
|
16
|
+
|
|
17
|
+
job_type 'email.send'
|
|
18
|
+
job_options queue: 'mailers', retry: 5
|
|
19
|
+
|
|
20
|
+
def perform(address)
|
|
21
|
+
# ...
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
WelcomeJob.perform_async('a@b.com')
|
|
26
|
+
WelcomeJob.perform_in(300, 'a@b.com')
|
|
27
|
+
WelcomeJob.set(queue: 'low').perform_async('a@b.com')
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- `job_type` is optional. The default is the class name. Use a name when a worker in another language runs the job.
|
|
31
|
+
- `job_options` accepts `queue`, `retry`, `reserve_for`, `backtrace`, and `custom`, such as `custom: { unique_for: 600 }`.
|
|
32
|
+
- Args must be JSON types with string hash keys. Symbols, times, and objects raise `ArgumentError`.
|
|
33
|
+
- Delivery is at least once, so jobs must be idempotent.
|
|
34
|
+
|
|
35
|
+
To send a job that a worker in any language runs:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
Anway.push('image.resize', [42], queue: 'media')
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Rails
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
config.active_job.queue_adapter = :anway
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- Existing ActiveJob classes work. Each one runs in the Rails reloader.
|
|
48
|
+
- In a database transaction, a push waits for the commit, and a rollback cancels it. This also applies to `perform_async`.
|
|
49
|
+
- A job with `retry_on` keeps ActiveJob attempts, and its final error goes to the dead set.
|
|
50
|
+
- A job with no `retry_on` uses server retries.
|
|
51
|
+
|
|
52
|
+
Tasks in `recurring.yml` and jobs that wait in Solid Queue do not move automatically. Move recurring tasks to the server cron, and drain the old queue first.
|
|
53
|
+
|
|
54
|
+
## Process
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
bundle exec anway -q critical,mailers,default -c 10
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
| Flag | Default | Purpose |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `-q`, `--queues` | Queues of the job classes, and `default` | Queues in order, as `name` or `name:weight` |
|
|
63
|
+
| `-c`, `--concurrency` | `10` | Job threads |
|
|
64
|
+
| `-t`, `--timeout` | `25` | Seconds for jobs to finish at a stop |
|
|
65
|
+
| `-r`, `--require` | `config/environment.rb` | File that loads the jobs. Required outside Rails. |
|
|
66
|
+
| `-C`, `--config` | `config/anway.yml` | YAML config file |
|
|
67
|
+
| `-e`, `--environment` | `RAILS_ENV`, `APP_ENV` | Environment section of the YAML file |
|
|
68
|
+
| `-l`, `--label` | None | Worker label |
|
|
69
|
+
| `-v`, `--verbose` | Off | Debug logs |
|
|
70
|
+
| `-V`, `--version` | | Print the version |
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
concurrency: <%= ENV.fetch('WORKER_THREADS', 10) %>
|
|
74
|
+
timeout: 25
|
|
75
|
+
queues:
|
|
76
|
+
- [critical, 4]
|
|
77
|
+
- [mailers, 2]
|
|
78
|
+
- [default, 1]
|
|
79
|
+
|
|
80
|
+
production:
|
|
81
|
+
concurrency: 20
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Flags have priority over the environment section, and the environment section has priority over top-level keys. A list without weights uses strict order.
|
|
85
|
+
|
|
86
|
+
| Signal | Effect |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `TERM`, `INT` | Stop |
|
|
89
|
+
| `TSTP` | Quiet: stop fetching and stay alive |
|
|
90
|
+
| `TTIN` | Write thread data to the log |
|
|
91
|
+
|
|
92
|
+
Exit codes: `0` for a normal stop, `1` for a boot failure, `2` for a usage error.
|
|
93
|
+
|
|
94
|
+
At a stop, jobs get `--timeout` seconds to finish. Then `Anway::Shutdown` is raised in them, and they get 5 seconds to return. The process then fails jobs that still run and stops. Set the container stop timeout above 30 seconds.
|
|
95
|
+
|
|
96
|
+
Keep the database pool at the concurrency value or more. A job queue with no worker has no error, and its jobs wait.
|
|
97
|
+
|
|
98
|
+
## Tests in your app
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
require 'anway/testing'
|
|
102
|
+
|
|
103
|
+
Anway::Testing.fake!
|
|
104
|
+
WelcomeJob.perform_async('a@b.com')
|
|
105
|
+
Anway::Testing.jobs.size
|
|
106
|
+
Anway::Testing.drain
|
|
107
|
+
|
|
108
|
+
Anway::Testing.inline!
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Client
|
|
112
|
+
|
|
113
|
+
`Anway::Client` has the operations of [anway-go](https://github.com/thadeu/anway-go), in snake_case. It is safe for threads and for forks.
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
client = Anway::Client.new('tcp://localhost:7419')
|
|
117
|
+
client.push(Anway::Job.new('email.send', ['a@b.com']))
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The error kinds are `Anway::NotUnique`, `Anway::Closed`, `Anway::ServerError`, and `Anway::ConnectionError`. A failed call does not resend.
|
|
121
|
+
|
|
122
|
+
## Configuration
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
Anway.configure do |config|
|
|
126
|
+
config.pool_size = 8
|
|
127
|
+
config.timeout = 10
|
|
128
|
+
config.tls = { ca_file: 'ca.pem' }
|
|
129
|
+
config.use MyMiddleware.new
|
|
130
|
+
config.on_error { |error, job| report(error, job) }
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Middleware has `call(job)` and yields. The first one added is the outermost.
|
|
135
|
+
|
|
136
|
+
## Tests
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
SERVER_BIN=/path/to/anway bundle exec rspec
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The specs map to the [conformance scenarios](https://github.com/thadeu/anway/blob/main/conformance/scenarios.md).
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT
|
data/exe/anway
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_job'
|
|
4
|
+
require 'anway'
|
|
5
|
+
|
|
6
|
+
module Anway
|
|
7
|
+
# Runs serialized ActiveJob payloads.
|
|
8
|
+
class ActiveJobWrapper
|
|
9
|
+
include Anway::Worker
|
|
10
|
+
|
|
11
|
+
job_type 'ActiveJob'
|
|
12
|
+
|
|
13
|
+
def perform(data)
|
|
14
|
+
ActiveJob::Base.execute(data)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Records the use of retry_on, because rescue_handlers also holds rescue_from.
|
|
19
|
+
module ActiveJobRetries
|
|
20
|
+
def retry_on(...)
|
|
21
|
+
self.anway_retry_on = true
|
|
22
|
+
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
ActiveSupport.on_load(:active_job) do
|
|
29
|
+
class_attribute :anway_retry_on, default: false
|
|
30
|
+
singleton_class.prepend(Anway::ActiveJobRetries)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module ActiveJob
|
|
34
|
+
module QueueAdapters
|
|
35
|
+
class AnwayAdapter < AbstractAdapter
|
|
36
|
+
BACKTRACE = 30
|
|
37
|
+
DEAD_SET = -1
|
|
38
|
+
|
|
39
|
+
def enqueue(job)
|
|
40
|
+
push(job, {})
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def enqueue_at(job, timestamp)
|
|
44
|
+
push(job, { at: Time.at(timestamp) })
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Each push has a new id. A retry_on attempt keeps its job_id while the
|
|
50
|
+
# first attempt is still reserved, and the server rejects a repeated id.
|
|
51
|
+
def push(job, options)
|
|
52
|
+
options = options.merge(queue: job.queue_name, backtrace: BACKTRACE)
|
|
53
|
+
|
|
54
|
+
# retry_on owns the attempts, so its final error goes to the dead set.
|
|
55
|
+
options[:retry] = DEAD_SET if job.class.anway_retry_on
|
|
56
|
+
|
|
57
|
+
pushed = Anway::Job.new('ActiveJob', [job.serialize.as_json], **options)
|
|
58
|
+
|
|
59
|
+
job.provider_job_id = pushed.jid
|
|
60
|
+
|
|
61
|
+
Anway.enqueue(pushed)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/anway/cli.rb
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'optparse'
|
|
5
|
+
require 'yaml'
|
|
6
|
+
|
|
7
|
+
# The Railtie registers only when Rails loads before the gem.
|
|
8
|
+
begin
|
|
9
|
+
require 'rails'
|
|
10
|
+
rescue LoadError
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require 'anway'
|
|
15
|
+
|
|
16
|
+
module Anway
|
|
17
|
+
# bundle exec anway -q critical,mailers:2,default -c 10
|
|
18
|
+
class CLI
|
|
19
|
+
EXIT_OK = 0
|
|
20
|
+
EXIT_BOOT = 1
|
|
21
|
+
EXIT_USAGE = 2
|
|
22
|
+
DEFAULTS = { concurrency: 10, timeout: 25, labels: [] }.freeze
|
|
23
|
+
DEFAULT_CONFIG = 'config/anway.yml'
|
|
24
|
+
RAILS_ENTRY = 'config/environment.rb'
|
|
25
|
+
|
|
26
|
+
def initialize(argv)
|
|
27
|
+
$stdout.sync = true
|
|
28
|
+
|
|
29
|
+
@argv = argv.dup
|
|
30
|
+
@flags = {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def run
|
|
34
|
+
return EXIT_OK if parse == :done
|
|
35
|
+
|
|
36
|
+
boot
|
|
37
|
+
start
|
|
38
|
+
EXIT_OK
|
|
39
|
+
rescue OptionParser::ParseError, ArgumentError => e
|
|
40
|
+
warn("anway: #{e.message}")
|
|
41
|
+
|
|
42
|
+
EXIT_USAGE
|
|
43
|
+
rescue Error, ScriptError, StandardError => e
|
|
44
|
+
warn("anway: #{e.class}: #{e.message}")
|
|
45
|
+
|
|
46
|
+
EXIT_BOOT
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def parse
|
|
52
|
+
parser.parse!(@argv)
|
|
53
|
+
|
|
54
|
+
raise ArgumentError, "unexpected argument: #{@argv.first}" unless @argv.empty?
|
|
55
|
+
|
|
56
|
+
@flags[:done] ? :done : nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parser
|
|
60
|
+
OptionParser.new do |options|
|
|
61
|
+
options.banner = 'Usage: anway [options]'
|
|
62
|
+
|
|
63
|
+
options.on('-q', '--queues LIST', 'Queues in order, as name or name:weight') do |list|
|
|
64
|
+
@flags[:queues] = list.split(',').map { |entry| entry.split(':') }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
options.on('-c', '--concurrency N', Integer, 'Number of job threads') { |n| @flags[:concurrency] = n }
|
|
68
|
+
options.on('-t', '--timeout N', Float, 'Seconds for jobs to finish at stop') { |n| @flags[:timeout] = n }
|
|
69
|
+
options.on('-r', '--require PATH', 'File that loads the jobs') { |path| @flags[:require] = path }
|
|
70
|
+
options.on('-C', '--config PATH', 'YAML config file') { |path| @flags[:config] = path }
|
|
71
|
+
options.on('-e', '--environment NAME', 'Environment name') { |name| @flags[:environment] = name }
|
|
72
|
+
options.on('-l', '--label NAME', 'Worker label') { |name| (@flags[:labels] ||= []) << name }
|
|
73
|
+
options.on('-v', '--verbose', 'Debug logs') { @flags[:verbose] = true }
|
|
74
|
+
|
|
75
|
+
options.on('-V', '--version', 'Print the version') do
|
|
76
|
+
puts "anway #{VERSION}"
|
|
77
|
+
|
|
78
|
+
@flags[:done] = true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
options.on('-h', '--help', 'Print this help') do
|
|
82
|
+
puts options
|
|
83
|
+
|
|
84
|
+
@flags[:done] = true
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def boot
|
|
90
|
+
environment = @flags[:environment] || ENV['RAILS_ENV'] || ENV['APP_ENV'] || 'development'
|
|
91
|
+
|
|
92
|
+
ENV['RAILS_ENV'] ||= environment
|
|
93
|
+
ENV['APP_ENV'] ||= environment
|
|
94
|
+
|
|
95
|
+
@options = DEFAULTS.merge(file_options(environment)).merge(@flags)
|
|
96
|
+
|
|
97
|
+
load_app
|
|
98
|
+
|
|
99
|
+
Anway.config.logger.level = Logger::DEBUG if @options[:verbose]
|
|
100
|
+
|
|
101
|
+
check_database_pool
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def file_options(environment)
|
|
105
|
+
path = @flags[:config] || DEFAULT_CONFIG
|
|
106
|
+
|
|
107
|
+
unless File.exist?(path)
|
|
108
|
+
raise ArgumentError, "config file not found: #{path}" if @flags[:config]
|
|
109
|
+
|
|
110
|
+
return {}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
content = YAML.safe_load(ERB.new(File.read(path)).result, aliases: true) || {}
|
|
114
|
+
section = content.fetch(environment, {})
|
|
115
|
+
common = content.reject { |_key, value| value.is_a?(Hash) }
|
|
116
|
+
|
|
117
|
+
common.merge(section).transform_keys(&:to_sym)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def load_app
|
|
121
|
+
path = @options[:require]
|
|
122
|
+
path ||= RAILS_ENTRY if File.exist?(RAILS_ENTRY)
|
|
123
|
+
|
|
124
|
+
raise ArgumentError, 'use -r to load the jobs outside Rails' unless path
|
|
125
|
+
|
|
126
|
+
require File.expand_path(path)
|
|
127
|
+
|
|
128
|
+
Rails.application.eager_load! if defined?(Rails) && Rails.application
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def check_database_pool
|
|
132
|
+
return unless defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
|
|
133
|
+
|
|
134
|
+
size = ActiveRecord::Base.connection_pool.size
|
|
135
|
+
|
|
136
|
+
return if size >= @options[:concurrency]
|
|
137
|
+
|
|
138
|
+
Anway.config.logger.warn("database pool #{size} is smaller than concurrency #{@options[:concurrency]}")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def start
|
|
142
|
+
runner = Runner.new(
|
|
143
|
+
queues: queues,
|
|
144
|
+
concurrency: @options[:concurrency],
|
|
145
|
+
timeout: @options[:timeout],
|
|
146
|
+
labels: @options[:labels]
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
trap_signals(runner)
|
|
150
|
+
runner.run
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def trap_signals(runner)
|
|
154
|
+
%w[TERM INT].each { |name| Signal.trap(name) { runner.stop } }
|
|
155
|
+
|
|
156
|
+
Signal.trap('TSTP') { runner.quiet }
|
|
157
|
+
Signal.trap('TTIN') { runner.dump }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Without a list, the worker uses the queues of its job classes.
|
|
161
|
+
def queues
|
|
162
|
+
return @options[:queues] if @options[:queues]
|
|
163
|
+
|
|
164
|
+
names = Worker.classes.map { |klass| klass.job_options[:queue]&.to_s }
|
|
165
|
+
names.concat(active_job_queues)
|
|
166
|
+
|
|
167
|
+
[*names.compact, 'default'].uniq
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def active_job_queues
|
|
171
|
+
return [] unless defined?(ActiveJob::Base)
|
|
172
|
+
|
|
173
|
+
ActiveJob::Base.descendants.map(&:queue_name).grep(String)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
data/lib/anway/client.rb
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module Anway
|
|
7
|
+
# Same operations as the anway-go client. It is safe for concurrent use.
|
|
8
|
+
class Client
|
|
9
|
+
DEFAULT_URL = 'tcp://127.0.0.1:7419'
|
|
10
|
+
DEFAULT_PORT = 7419
|
|
11
|
+
FETCH_WAIT = 2
|
|
12
|
+
|
|
13
|
+
Endpoint = Data.define(:host, :port, :password, :tls)
|
|
14
|
+
|
|
15
|
+
# A blank URL uses ANWAY_URL, then the local default.
|
|
16
|
+
def initialize(url = nil, pool_size: 8, timeout: 10, tls: nil, wid: nil, labels: [])
|
|
17
|
+
@endpoint = parse_url(url || ENV.fetch('ANWAY_URL', nil) || DEFAULT_URL)
|
|
18
|
+
@timeout = timeout
|
|
19
|
+
@wid = wid
|
|
20
|
+
|
|
21
|
+
@pool = Pool.new(pool_size) do
|
|
22
|
+
Connection.new(@endpoint, timeout: timeout, tls: tls, wid: wid, labels: labels)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def close
|
|
27
|
+
@pool.close
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Raises NotUnique when a unique lock blocks the job.
|
|
31
|
+
def push(job)
|
|
32
|
+
call('PUSH', job.to_h)
|
|
33
|
+
|
|
34
|
+
job.jid
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns the next job, or nil when no job arrives in about 2 seconds.
|
|
38
|
+
def fetch(*queues)
|
|
39
|
+
reply = call('FETCH', queues.join(' '), wait: FETCH_WAIT)
|
|
40
|
+
|
|
41
|
+
reply && Job.from_hash(JSON.parse(reply))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def ack(jid)
|
|
45
|
+
call('ACK', { jid: jid })
|
|
46
|
+
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def fail(jid, errtype:, message:, backtrace: [])
|
|
51
|
+
call('FAIL', { jid: jid, errtype: errtype, message: message, backtrace: backtrace })
|
|
52
|
+
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Returns nil, "quiet", or "terminate".
|
|
57
|
+
def beat(current_state = nil)
|
|
58
|
+
reply = call('BEAT', { wid: @wid, current_state: current_state }.compact)
|
|
59
|
+
|
|
60
|
+
reply == 'OK' ? nil : JSON.parse(reply)['state']
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def info
|
|
64
|
+
JSON.parse(call('INFO'))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def queue_sizes
|
|
68
|
+
info.dig('faktory', 'queues') || {}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def pause_queues(*queues)
|
|
72
|
+
queue_command('PAUSE', queues)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def resume_queues(*queues)
|
|
76
|
+
queue_command('RESUME', queues)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def remove_queues(*queues)
|
|
80
|
+
queue_command('REMOVE', queues)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The command is "kill", "discard", "requeue", or "clear".
|
|
84
|
+
# The target is "retries", "scheduled", or "dead".
|
|
85
|
+
def mutate(command, target, jobtype: nil, jids: nil, regexp: nil)
|
|
86
|
+
filter = { jobtype: jobtype, jids: jids, regexp: regexp }.compact
|
|
87
|
+
|
|
88
|
+
call('MUTATE', { cmd: command, target: target, filter: filter })
|
|
89
|
+
|
|
90
|
+
true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def batch_new(success: nil, complete: nil, parent_bid: nil, description: nil)
|
|
94
|
+
definition = {
|
|
95
|
+
success: success&.to_h, complete: complete&.to_h, parent_bid: parent_bid, description: description
|
|
96
|
+
}.compact
|
|
97
|
+
|
|
98
|
+
call('BATCH', "NEW #{JSON.generate(definition)}")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def batch_open(bid)
|
|
102
|
+
call('BATCH', "OPEN #{bid}")
|
|
103
|
+
|
|
104
|
+
true
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def batch_commit(bid)
|
|
108
|
+
call('BATCH', "COMMIT #{bid}")
|
|
109
|
+
|
|
110
|
+
true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def batch_status(bid)
|
|
114
|
+
JSON.parse(call('BATCH', "STATUS #{bid}"))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def track_get(jid)
|
|
118
|
+
JSON.parse(call('TRACK', "GET #{jid}"))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def track_set(jid, percent: nil, desc: nil, reserve_until: nil)
|
|
122
|
+
update = { jid: jid, percent: percent, desc: desc, reserve_until: reserve_until&.to_time&.utc&.iso8601(9) }
|
|
123
|
+
|
|
124
|
+
call('TRACK', "SET #{JSON.generate(update.compact)}")
|
|
125
|
+
|
|
126
|
+
true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def queue_command(action, queues)
|
|
132
|
+
call('QUEUE', "#{action} #{queues.join(' ')}")
|
|
133
|
+
|
|
134
|
+
true
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def call(verb, argument = nil, wait: 0)
|
|
138
|
+
@pool.with(timeout: @timeout) { |connection| connection.command(verb, argument, wait: wait) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def parse_url(url)
|
|
142
|
+
uri = URI.parse(url)
|
|
143
|
+
|
|
144
|
+
raise ArgumentError, "unsupported scheme: #{uri.scheme}" unless %w[tcp tcp+tls].include?(uri.scheme)
|
|
145
|
+
|
|
146
|
+
password = uri.password && URI.decode_www_form_component(uri.password)
|
|
147
|
+
|
|
148
|
+
Endpoint.new(host: uri.hostname, port: uri.port || DEFAULT_PORT, password: password, tls: uri.scheme == 'tcp+tls')
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
5
|
+
module Anway
|
|
6
|
+
class Configuration
|
|
7
|
+
attr_accessor :url, :pool_size, :timeout, :tls, :logger, :push_wrapper, :reloader
|
|
8
|
+
attr_reader :middleware, :error_handlers
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@url = nil
|
|
12
|
+
@pool_size = 8
|
|
13
|
+
@timeout = 10
|
|
14
|
+
@tls = nil
|
|
15
|
+
@logger = Logger.new($stdout, level: Logger::INFO)
|
|
16
|
+
@push_wrapper = ->(&block) { block.call }
|
|
17
|
+
@reloader = ->(&block) { block.call }
|
|
18
|
+
@middleware = []
|
|
19
|
+
@error_handlers = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The first middleware added is the outermost. An object has `call(job)`
|
|
23
|
+
# and yields to run the next one.
|
|
24
|
+
def use(middleware)
|
|
25
|
+
@middleware << middleware
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def on_error(&block)
|
|
29
|
+
@error_handlers << block
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|