cosmonats 0.1.4 → 0.3.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 +4 -4
- data/README.md +129 -67
- data/lib/cosmo/api/busy.rb +66 -0
- data/lib/cosmo/api/counter.rb +70 -0
- data/lib/cosmo/api/job.rb +46 -0
- data/lib/cosmo/api/kv.rb +63 -0
- data/lib/cosmo/api/stats.rb +44 -0
- data/lib/cosmo/api/stream.rb +123 -0
- data/lib/cosmo/api.rb +11 -0
- data/lib/cosmo/cli.rb +8 -5
- data/lib/cosmo/client.rb +58 -3
- data/lib/cosmo/config.rb +13 -38
- data/lib/cosmo/engine.rb +1 -1
- data/lib/cosmo/job/processor.rb +66 -57
- data/lib/cosmo/job.rb +1 -1
- data/lib/cosmo/logger.rb +8 -1
- data/lib/cosmo/processor.rb +110 -2
- data/lib/cosmo/stream/processor.rb +23 -59
- data/lib/cosmo/stream.rb +2 -2
- data/lib/cosmo/utils/hash.rb +3 -27
- data/lib/cosmo/utils/overrides.rb +15 -0
- data/lib/cosmo/utils/ttl_cache.rb +44 -0
- data/lib/cosmo/utils/warnings.rb +17 -0
- data/lib/cosmo/utils.rb +15 -0
- data/lib/cosmo/version.rb +1 -1
- data/lib/cosmo/web/assets/app.css +477 -0
- data/lib/cosmo/web/assets/htmx.2.0.8.min.js.gz +0 -0
- data/lib/cosmo/web/context.rb +28 -0
- data/lib/cosmo/web/controllers/actions.rb +16 -0
- data/lib/cosmo/web/controllers/application.rb +43 -0
- data/lib/cosmo/web/controllers/jobs.rb +97 -0
- data/lib/cosmo/web/controllers/streams.rb +70 -0
- data/lib/cosmo/web/helpers/application.rb +87 -0
- data/lib/cosmo/web/renderer.rb +58 -0
- data/lib/cosmo/web/views/actions/index.erb +7 -0
- data/lib/cosmo/web/views/jobs/_busy.erb +50 -0
- data/lib/cosmo/web/views/jobs/_dead.erb +65 -0
- data/lib/cosmo/web/views/jobs/_enqueued.erb +60 -0
- data/lib/cosmo/web/views/jobs/_scheduled.erb +49 -0
- data/lib/cosmo/web/views/jobs/_stats.erb +69 -0
- data/lib/cosmo/web/views/jobs/busy.erb +16 -0
- data/lib/cosmo/web/views/jobs/dead.erb +17 -0
- data/lib/cosmo/web/views/jobs/enqueued.erb +16 -0
- data/lib/cosmo/web/views/jobs/index.erb +12 -0
- data/lib/cosmo/web/views/jobs/scheduled.erb +17 -0
- data/lib/cosmo/web/views/layout.erb +33 -0
- data/lib/cosmo/web/views/streams/_info.erb +92 -0
- data/lib/cosmo/web/views/streams/_pause_banner.erb +17 -0
- data/lib/cosmo/web/views/streams/_stream_row.erb +42 -0
- data/lib/cosmo/web/views/streams/_table.erb +25 -0
- data/lib/cosmo/web/views/streams/index.erb +11 -0
- data/lib/cosmo/web/views/streams/info.erb +11 -0
- data/lib/cosmo/web.rb +68 -0
- data/lib/cosmo.rb +2 -7
- data/sig/cosmo/api/busy.rbs +35 -0
- data/sig/cosmo/api/counter.rbs +34 -0
- data/sig/cosmo/api/job.rbs +31 -0
- data/sig/cosmo/api/kv.rbs +30 -0
- data/sig/cosmo/api/stats.rbs +21 -0
- data/sig/cosmo/api/stream.rbs +50 -0
- data/sig/cosmo/client.rbs +21 -3
- data/sig/cosmo/config.rbs +3 -15
- data/sig/cosmo/job/processor.rbs +16 -8
- data/sig/cosmo/processor.rbs +26 -0
- data/sig/cosmo/stream/processor.rbs +4 -10
- data/sig/cosmo/utils/hash.rbs +0 -8
- data/sig/cosmo/utils/ttl_cache.rbs +20 -0
- metadata +62 -3
- data/lib/cosmo/defaults.yml +0 -69
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cosmo/api/job"
|
|
4
|
+
|
|
5
|
+
module Cosmo
|
|
6
|
+
module API
|
|
7
|
+
class Stream
|
|
8
|
+
LIMIT = 20
|
|
9
|
+
|
|
10
|
+
include Enumerable
|
|
11
|
+
|
|
12
|
+
def self.all
|
|
13
|
+
client.list_streams.map { new(_1.dig("config", "name")) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.jobs
|
|
17
|
+
client.list_streams.select { _1.dig("config", "metadata", "_cosmo.type") == "jobs" }
|
|
18
|
+
.reject { %w[scheduled dead].include?(_1.dig("config", "name")) }
|
|
19
|
+
.map { new(_1.dig("config", "name")) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.client
|
|
23
|
+
@client ||= Client.instance
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attr_reader :name
|
|
27
|
+
|
|
28
|
+
def initialize(name)
|
|
29
|
+
@name = name
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def info
|
|
33
|
+
info = client.stream_info(name)
|
|
34
|
+
{ state: info.state, config: info.config }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def total
|
|
38
|
+
info[:state].messages.to_i
|
|
39
|
+
rescue StandardError
|
|
40
|
+
0
|
|
41
|
+
end
|
|
42
|
+
alias size total
|
|
43
|
+
|
|
44
|
+
def retries
|
|
45
|
+
client.list_consumers(name).sum { _1["num_redelivered"].to_i }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def each
|
|
49
|
+
return if total.zero?
|
|
50
|
+
|
|
51
|
+
state = info[:state]
|
|
52
|
+
current = @offset || state.first_seq.to_i
|
|
53
|
+
last = state.last_seq.to_i
|
|
54
|
+
|
|
55
|
+
loop do
|
|
56
|
+
break if current > last
|
|
57
|
+
|
|
58
|
+
job = message(current)
|
|
59
|
+
break unless job
|
|
60
|
+
|
|
61
|
+
yield job
|
|
62
|
+
current += 1
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def offset(value)
|
|
67
|
+
@offset = value.to_i
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def messages(page: nil, limit: nil)
|
|
72
|
+
jobs = []
|
|
73
|
+
limit = (limit || LIMIT).to_i
|
|
74
|
+
state = info[:state]
|
|
75
|
+
start = state.first_seq.to_i
|
|
76
|
+
start += (page.to_i - 1) * limit if page
|
|
77
|
+
|
|
78
|
+
offset(start).each do |message|
|
|
79
|
+
jobs << message
|
|
80
|
+
break if jobs.size >= limit
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
jobs
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def message(seq)
|
|
87
|
+
Job.new(name, client.get_message(name, seq: seq, direct: true))
|
|
88
|
+
rescue NATS::JetStream::Error::NotFound
|
|
89
|
+
# nop, acked/nacked
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def retry(seq)
|
|
93
|
+
job = message(seq)
|
|
94
|
+
return unless job
|
|
95
|
+
|
|
96
|
+
client.publish(job.x_subject, job.message.data)
|
|
97
|
+
delete(seq)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def delete(seq)
|
|
101
|
+
client.delete_message(name, seq)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def pause!
|
|
105
|
+
client.pause_stream(name)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def unpause!
|
|
109
|
+
client.unpause_stream(name)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def paused?
|
|
113
|
+
client.stream_paused?(name)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def client
|
|
119
|
+
self.class.client
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
data/lib/cosmo/api.rb
ADDED
data/lib/cosmo/cli.rb
CHANGED
|
@@ -102,13 +102,16 @@ module Cosmo
|
|
|
102
102
|
load_config(flags)
|
|
103
103
|
boot_application
|
|
104
104
|
|
|
105
|
-
Config[:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
Config[:setup]&.each_value do |configs|
|
|
106
|
+
configs.each do |name, config|
|
|
107
|
+
Client.instance.stream_info(name)
|
|
108
|
+
rescue NATS::JetStream::Error::NotFound
|
|
109
|
+
meta = { metadata: { "_cosmo.type" => "jobs" } }
|
|
110
|
+
Client.instance.create_stream(name, config.merge(meta))
|
|
111
|
+
end
|
|
109
112
|
end
|
|
110
113
|
|
|
111
|
-
puts "Cosmo streams were created
|
|
114
|
+
puts "Cosmo streams were created successfully"
|
|
112
115
|
exit(0)
|
|
113
116
|
end
|
|
114
117
|
|
data/lib/cosmo/client.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "nats/client"
|
|
4
|
+
require "cosmo/utils/overrides"
|
|
4
5
|
|
|
5
6
|
module Cosmo
|
|
6
7
|
class Client
|
|
@@ -37,16 +38,70 @@ module Cosmo
|
|
|
37
38
|
js.delete_stream(name, params)
|
|
38
39
|
end
|
|
39
40
|
|
|
41
|
+
def update_stream(name, config)
|
|
42
|
+
js.update_stream(name: name, **config)
|
|
43
|
+
end
|
|
44
|
+
|
|
40
45
|
def list_streams
|
|
41
46
|
response = nc.request("$JS.API.STREAM.LIST", "")
|
|
42
47
|
data = Utils::Json.parse(response.data, symbolize_names: false)
|
|
43
48
|
return [] if data.nil? || data["streams"].nil?
|
|
44
49
|
|
|
45
|
-
data["streams"]
|
|
50
|
+
data["streams"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def pause_stream(name)
|
|
54
|
+
config = stream_info(name).config.to_h
|
|
55
|
+
config[:metadata] ||= {}
|
|
56
|
+
config[:metadata][:"_cosmo.paused"] = "true"
|
|
57
|
+
update_stream(name, config)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def unpause_stream(name)
|
|
61
|
+
config = stream_info(name).config.to_h
|
|
62
|
+
config[:metadata] ||= {}
|
|
63
|
+
config[:metadata].delete(:"_cosmo.paused")
|
|
64
|
+
update_stream(name, config)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def stream_paused?(name)
|
|
68
|
+
stream_info(name).config.metadata&.[](:"_cosmo.paused") == "true"
|
|
69
|
+
rescue NATS::IO::Timeout
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def list_consumers(stream_name)
|
|
74
|
+
response = nc.request("$JS.API.CONSUMER.LIST.#{stream_name}", "")
|
|
75
|
+
data = Utils::Json.parse(response.data, default: {}, symbolize_names: false)
|
|
76
|
+
Array(data["consumers"])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def consumer_info(stream_name, consumer_name)
|
|
80
|
+
js.consumer_info(stream_name, consumer_name)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def get_message(name, **options)
|
|
84
|
+
js.get_msg(name, **options)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def delete_message(name, seq)
|
|
88
|
+
response = nc.request("$JS.API.STREAM.MSG.DELETE.#{name}", JSON.dump({ seq: seq }))
|
|
89
|
+
Utils::Json.parse(response.data, symbolize_names: false)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def purge(stream_name, subject)
|
|
93
|
+
payload = subject ? Utils::Json.dump({ filter: subject }) : ""
|
|
94
|
+
response = @nc.request("$JS.API.STREAM.PURGE.#{stream_name}", payload)
|
|
95
|
+
result = Utils::Json.parse(response.data, default: {}, symbolize_names: false)
|
|
96
|
+
raise NATS::JetStream::Error, result.dig("error", "description") if result["error"]
|
|
97
|
+
|
|
98
|
+
result["purged"] # number of messages purged
|
|
46
99
|
end
|
|
47
100
|
|
|
48
|
-
def
|
|
49
|
-
js.
|
|
101
|
+
def kv(name, **options)
|
|
102
|
+
js.key_value(name)
|
|
103
|
+
rescue NATS::KeyValue::BucketNotFoundError
|
|
104
|
+
js.create_key_value({ bucket: name }.merge(options))
|
|
50
105
|
end
|
|
51
106
|
|
|
52
107
|
def close
|
data/lib/cosmo/config.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "yaml"
|
|
|
4
4
|
require "forwardable"
|
|
5
5
|
|
|
6
6
|
module Cosmo
|
|
7
|
-
class Config
|
|
7
|
+
class Config < ::Hash
|
|
8
8
|
NANO = 1_000_000_000
|
|
9
9
|
DEFAULT_PATH = "config/cosmo.yml"
|
|
10
10
|
|
|
@@ -18,7 +18,7 @@ module Cosmo
|
|
|
18
18
|
YAML.load_file(path, aliases: true).tap { normalize!(_1) }
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def self.normalize!(config) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
21
|
+
def self.normalize!(config) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
22
22
|
Utils::Hash.symbolize_keys!(config)
|
|
23
23
|
|
|
24
24
|
config[:consumers]&.each_key do |name|
|
|
@@ -30,11 +30,13 @@ module Cosmo
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
config[:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
config[:setup]&.each_key do |type|
|
|
34
|
+
config[:setup][type]&.each_key do |name|
|
|
35
|
+
c = config[:setup][type][name]
|
|
36
|
+
c[:max_age] = c[:max_age].to_i * NANO if c[:max_age]
|
|
37
|
+
c[:duplicate_window] = c[:duplicate_window].to_i * NANO if c[:duplicate_window]
|
|
38
|
+
c[:subjects] = c[:subjects].map { |s| format(s, name: name) } if c[:subjects]
|
|
39
|
+
end
|
|
38
40
|
end
|
|
39
41
|
end
|
|
40
42
|
|
|
@@ -57,45 +59,18 @@ module Cosmo
|
|
|
57
59
|
@instance ||= new
|
|
58
60
|
end
|
|
59
61
|
|
|
60
|
-
def self.
|
|
61
|
-
@
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def initialize
|
|
65
|
-
@config = nil
|
|
66
|
-
@system = {}
|
|
67
|
-
@defaults = self.class.parse_file(File.expand_path("defaults.yml", __dir__))
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def [](key)
|
|
71
|
-
dig(key)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def fetch(key, default = nil)
|
|
75
|
-
return @config.fetch(key, default) if @config && Utils::Hash.keys?(@config, key)
|
|
76
|
-
|
|
77
|
-
@defaults.fetch(key, default)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def dig(*keys)
|
|
81
|
-
return @config&.dig(*keys) if @config && Utils::Hash.keys?(@config, *keys)
|
|
82
|
-
|
|
83
|
-
@defaults.dig(*keys)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def to_h
|
|
87
|
-
Utils::Hash.merge(@defaults, @config)
|
|
62
|
+
def self.internal
|
|
63
|
+
@internal ||= {}
|
|
88
64
|
end
|
|
89
65
|
|
|
90
66
|
def set(...)
|
|
91
|
-
|
|
92
|
-
Utils::Hash.set(@config, ...)
|
|
67
|
+
Utils::Hash.set(self, ...)
|
|
93
68
|
end
|
|
94
69
|
|
|
95
70
|
def load(path = nil)
|
|
96
71
|
return unless path
|
|
97
72
|
|
|
98
|
-
|
|
73
|
+
replace(self.class.parse_file(path))
|
|
99
74
|
end
|
|
100
75
|
end
|
|
101
76
|
end
|
data/lib/cosmo/engine.rb
CHANGED
|
@@ -25,7 +25,7 @@ module Cosmo
|
|
|
25
25
|
|
|
26
26
|
def run(type, options)
|
|
27
27
|
handler = Utils::Signal.trap(:INT, :TERM)
|
|
28
|
-
Logger.info "Starting processing, hit Ctrl-C to stop"
|
|
28
|
+
Logger.info "Starting processing, hit Ctrl-C to stop [concurrency=#{@concurrency}]"
|
|
29
29
|
|
|
30
30
|
processor_classes = type && PROCESSORS.key?(type.to_sym) ? [PROCESSORS[type.to_sym]] : PROCESSORS.values
|
|
31
31
|
@processors = processor_classes.map { _1.run(@pool, @running, options) }
|
data/lib/cosmo/job/processor.rb
CHANGED
|
@@ -3,63 +3,27 @@
|
|
|
3
3
|
module Cosmo
|
|
4
4
|
module Job
|
|
5
5
|
class Processor < ::Cosmo::Processor
|
|
6
|
-
def initialize(pool, running, options)
|
|
7
|
-
super
|
|
8
|
-
@weights = []
|
|
9
|
-
end
|
|
10
|
-
|
|
11
6
|
private
|
|
12
7
|
|
|
13
|
-
def run_loop
|
|
14
|
-
Thread.new { work_loop }
|
|
15
|
-
Thread.new { schedule_loop }
|
|
16
|
-
end
|
|
17
|
-
|
|
18
8
|
def setup
|
|
19
9
|
jobs_config = Config.dig(:consumers, :jobs)
|
|
20
10
|
jobs_config&.each do |stream_name, config|
|
|
21
|
-
|
|
22
|
-
subject = config.delete(:subject)
|
|
23
|
-
priority = config.delete(:priority)
|
|
24
|
-
@weights += ([stream_name] * priority.to_i) if priority
|
|
25
|
-
subscription = client.subscribe(subject, consumer_name, config)
|
|
26
|
-
@consumers << [subscription, stream_name]
|
|
27
|
-
end
|
|
28
|
-
end
|
|
11
|
+
next if stream_name == :scheduled # scheduled jobs are handled in schedule_loop
|
|
29
12
|
|
|
30
|
-
|
|
31
|
-
shutdown = false
|
|
32
|
-
|
|
33
|
-
while running?
|
|
34
|
-
break if shutdown
|
|
35
|
-
|
|
36
|
-
@weights.shuffle.each do |stream_name|
|
|
37
|
-
break unless running?
|
|
38
|
-
|
|
39
|
-
begin
|
|
40
|
-
timeout = ENV.fetch("COSMO_JOBS_FETCH_TIMEOUT", 0.1).to_f
|
|
41
|
-
@pool.post do
|
|
42
|
-
subscription = @consumers.find { |(_, sn)| sn == stream_name }&.first
|
|
43
|
-
messages = fetch(subscription, batch_size: 1, timeout:)
|
|
44
|
-
process(messages) if messages&.any?
|
|
45
|
-
end
|
|
46
|
-
rescue Concurrent::RejectedExecutionError
|
|
47
|
-
shutdown = true
|
|
48
|
-
break # pool doesn't accept new jobs, we are shutting down
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
break unless running?
|
|
52
|
-
end
|
|
13
|
+
@consumers << subscribe(stream_name, config)
|
|
53
14
|
end
|
|
54
15
|
end
|
|
55
16
|
|
|
56
17
|
def schedule_loop # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
18
|
+
config = Config.dig(:consumers, :jobs, :scheduled)
|
|
19
|
+
return unless config
|
|
20
|
+
|
|
21
|
+
subscription, = subscribe(:scheduled, config)
|
|
57
22
|
while running?
|
|
58
23
|
break unless running?
|
|
59
24
|
|
|
60
25
|
now = Time.now.to_i
|
|
61
26
|
timeout = ENV.fetch("COSMO_JOBS_SCHEDULER_FETCH_TIMEOUT", 5).to_f
|
|
62
|
-
subscription = @consumers.find { |(_, sn)| sn == :scheduled }&.first
|
|
63
27
|
messages = fetch(subscription, batch_size: 100, timeout:)
|
|
64
28
|
messages&.each do |message|
|
|
65
29
|
headers = message.header.except("X-Stream", "X-Subject", "X-Execute-At", "Nats-Expected-Stream")
|
|
@@ -80,22 +44,24 @@ module Cosmo
|
|
|
80
44
|
end
|
|
81
45
|
end
|
|
82
46
|
|
|
83
|
-
def process(messages) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
47
|
+
def process(messages, _) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
84
48
|
message = messages.first
|
|
85
49
|
Logger.debug "received messages #{messages.inspect}"
|
|
86
50
|
data = Utils::Json.parse(message.data)
|
|
87
51
|
unless data
|
|
88
|
-
Logger.
|
|
52
|
+
Logger.error ArgumentError.new("malformed payload")
|
|
53
|
+
move_message(message)
|
|
89
54
|
return
|
|
90
55
|
end
|
|
91
56
|
|
|
92
57
|
worker_class = Utils::String.safe_constantize(data[:class])
|
|
93
58
|
unless worker_class
|
|
94
|
-
Logger.
|
|
59
|
+
Logger.error ArgumentError.new("#{data[:class]} class not found")
|
|
60
|
+
move_message(message, data)
|
|
95
61
|
return
|
|
96
62
|
end
|
|
97
63
|
|
|
98
|
-
|
|
64
|
+
with_stats(message) do
|
|
99
65
|
sw = stopwatch
|
|
100
66
|
Logger.with(jid: data[:jid])
|
|
101
67
|
Logger.info "start"
|
|
@@ -104,10 +70,12 @@ module Cosmo
|
|
|
104
70
|
instance.perform(*data[:args])
|
|
105
71
|
message.ack
|
|
106
72
|
Logger.with(elapsed: sw.elapsed_seconds) { Logger.info "done" }
|
|
73
|
+
true
|
|
107
74
|
rescue StandardError => e
|
|
108
75
|
Logger.debug e
|
|
109
76
|
Logger.with(elapsed: sw.elapsed_seconds) { Logger.info "fail" }
|
|
110
|
-
handle_failure(message, data)
|
|
77
|
+
dropped = handle_failure(message, data)
|
|
78
|
+
false if dropped
|
|
111
79
|
rescue Exception # rubocop:disable Lint/RescueException
|
|
112
80
|
Logger.with(elapsed: sw.elapsed_seconds) { Logger.info "fail" }
|
|
113
81
|
raise
|
|
@@ -117,24 +85,65 @@ module Cosmo
|
|
|
117
85
|
Logger.debug "processed message #{message.inspect}"
|
|
118
86
|
end
|
|
119
87
|
|
|
120
|
-
def handle_failure(message, data) # rubocop:disable
|
|
88
|
+
def handle_failure(message, data) # rubocop:disable Naming/PredicateMethod
|
|
121
89
|
current_attempt = message.metadata.num_delivered
|
|
122
90
|
max_retries = data[:retry].to_i + 1
|
|
123
91
|
|
|
124
92
|
if current_attempt < max_retries
|
|
125
|
-
# NATS will auto-retry based on
|
|
93
|
+
# NATS will auto-retry with delay (exponential backoff based on current attempt).
|
|
94
|
+
# When max_deliver is reached, NATS stops redelivering the message and marks it as "max deliveries exceeded".
|
|
95
|
+
# The message is effectively abandoned by NATS — it stays in the stream (consuming a slot) but will never be delivered again to that consumer.
|
|
126
96
|
delay_ns = ((current_attempt**4) + 15) * 1_000_000_000
|
|
127
97
|
message.nak(delay: delay_ns)
|
|
128
|
-
return
|
|
98
|
+
return false
|
|
129
99
|
end
|
|
130
100
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
101
|
+
data[:dead] ? move_message(message, data) : drop_message(message, data)
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def subscribe(stream_name, config)
|
|
106
|
+
config = config.dup
|
|
107
|
+
config[:batch_size] = 1
|
|
108
|
+
config[:stream] = stream_name
|
|
109
|
+
consumer_name = "consumer-#{stream_name}"
|
|
110
|
+
subscription = client.subscribe(config[:subject], consumer_name, config.except(:subject, :priority, :stream, :batch_size))
|
|
111
|
+
[subscription, config, nil]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def drop_message(message, data)
|
|
115
|
+
message.term
|
|
116
|
+
Logger.debug "job dropped #{data[:jid]}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def move_message(message, data = nil)
|
|
120
|
+
klass = data ? Utils::String.underscore(data[:class]) : "default"
|
|
121
|
+
headers = { "X-Stream" => message.metadata.stream, "X-Subject" => message.subject }
|
|
122
|
+
Client.instance.publish("jobs.dead.#{klass}", message.data, header: headers)
|
|
123
|
+
message.ack
|
|
124
|
+
Logger.debug "job moved #{data&.dig(:jid)} to DLQ"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def scheduler?
|
|
128
|
+
true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def consumers
|
|
132
|
+
@weights ||= @consumers.filter_map { |(_, c, _)| [c[:stream]] * [c[:priority].to_i, 1].max }.flatten
|
|
133
|
+
@weights.shuffle.map { |s| @consumers.find { |(_, c, _)| c[:stream] == s } }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def fetch_subjects(config)
|
|
137
|
+
config[:subject]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def fetch_timeout(_config)
|
|
141
|
+
ENV.fetch("COSMO_JOBS_FETCH_TIMEOUT", 0.1).to_f
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def with_stats(message, &block)
|
|
145
|
+
API::Busy.instance.with(message) do
|
|
146
|
+
API::Counter.instance.with(&block)
|
|
138
147
|
end
|
|
139
148
|
end
|
|
140
149
|
end
|
data/lib/cosmo/job.rb
CHANGED
data/lib/cosmo/logger.rb
CHANGED
|
@@ -60,7 +60,14 @@ module Cosmo
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def self.instance
|
|
63
|
-
@instance ||= ::Logger.new($stdout).tap
|
|
63
|
+
@instance ||= ::Logger.new($stdout).tap do |logger|
|
|
64
|
+
logger.formatter = SimpleFormatter.new
|
|
65
|
+
logger.level = ::Logger::Severity.coerce(ENV.fetch("COSMO_LOG_LEVEL", "info"))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.instance=(logger)
|
|
70
|
+
@instance = logger
|
|
64
71
|
end
|
|
65
72
|
end
|
|
66
73
|
end
|