cosmonats 0.5.1 → 0.7.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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +21 -169
  3. data/README.md +102 -18
  4. data/lib/cosmo/api/counter.rb +14 -11
  5. data/lib/cosmo/api/cron/entry.rb +23 -3
  6. data/lib/cosmo/api/cron.rb +22 -26
  7. data/lib/cosmo/api/job.rb +30 -0
  8. data/lib/cosmo/api/kv.rb +31 -11
  9. data/lib/cosmo/api/registry.rb +68 -0
  10. data/lib/cosmo/api/stats/busy.rb +63 -0
  11. data/lib/cosmo/api/stats/processes.rb +37 -0
  12. data/lib/cosmo/api/stats/totals.rb +35 -0
  13. data/lib/cosmo/api/stats.rb +5 -4
  14. data/lib/cosmo/api/stream.rb +1 -1
  15. data/lib/cosmo/api.rb +3 -2
  16. data/lib/cosmo/batch.rb +9 -2
  17. data/lib/cosmo/cli.rb +27 -7
  18. data/lib/cosmo/client.rb +69 -6
  19. data/lib/cosmo/config.rb +6 -2
  20. data/lib/cosmo/engine.rb +103 -4
  21. data/lib/cosmo/heartbeat.rb +129 -0
  22. data/lib/cosmo/http_server/handler.rb +44 -0
  23. data/lib/cosmo/http_server/health.rb +39 -0
  24. data/lib/cosmo/http_server/ping.rb +23 -0
  25. data/lib/cosmo/http_server.rb +62 -0
  26. data/lib/cosmo/job/failure.rb +52 -0
  27. data/lib/cosmo/job/processor.rb +87 -25
  28. data/lib/cosmo/job/stream_filter.rb +54 -0
  29. data/lib/cosmo/job.rb +2 -0
  30. data/lib/cosmo/logger.rb +21 -3
  31. data/lib/cosmo/processor.rb +46 -16
  32. data/lib/cosmo/sentry/job_processor_middleware.rb +1 -2
  33. data/lib/cosmo/stream/message.rb +2 -1
  34. data/lib/cosmo/stream/processor.rb +5 -0
  35. data/lib/cosmo/utils/json.rb +1 -1
  36. data/lib/cosmo/utils/overrides.rb +199 -8
  37. data/lib/cosmo/utils/thread_pool.rb +5 -0
  38. data/lib/cosmo/version.rb +1 -1
  39. data/lib/cosmo/web/assets/app.css +21 -0
  40. data/lib/cosmo/web/controllers/jobs.rb +28 -4
  41. data/lib/cosmo/web/helpers/application.rb +10 -1
  42. data/lib/cosmo/web/views/batches/index.erb +1 -1
  43. data/lib/cosmo/web/views/jobs/_busy.erb +70 -4
  44. data/lib/cosmo/web/views/jobs/_dead.erb +29 -16
  45. data/lib/cosmo/web/views/jobs/_enqueued.erb +10 -0
  46. data/lib/cosmo/web/views/jobs/_processes.erb +131 -0
  47. data/lib/cosmo/web/views/jobs/_scheduled.erb +1 -1
  48. data/lib/cosmo/web/views/jobs/busy.erb +1 -1
  49. data/lib/cosmo/web/views/jobs/dead.erb +2 -2
  50. data/lib/cosmo/web/views/jobs/enqueued.erb +1 -1
  51. data/lib/cosmo/web/views/jobs/index.erb +7 -1
  52. data/lib/cosmo/web/views/jobs/scheduled.erb +1 -1
  53. data/lib/cosmo/web/views/streams/index.erb +1 -1
  54. data/lib/cosmo/web.rb +2 -0
  55. data/lib/cosmo.rb +13 -0
  56. data/sig/cosmo/api/counter.rbs +2 -4
  57. data/sig/cosmo/api/cron/entry.rbs +2 -0
  58. data/sig/cosmo/api/cron.rbs +4 -2
  59. data/sig/cosmo/api/job.rbs +12 -0
  60. data/sig/cosmo/api/kv.rbs +7 -1
  61. data/sig/cosmo/api/registry.rbs +30 -0
  62. data/sig/cosmo/api/stats/busy.rbs +31 -0
  63. data/sig/cosmo/api/stats/processes.rbs +16 -0
  64. data/sig/cosmo/api/stats/totals.rbs +17 -0
  65. data/sig/cosmo/cli.rbs +4 -0
  66. data/sig/cosmo/client.rbs +17 -2
  67. data/sig/cosmo/engine.rbs +29 -0
  68. data/sig/cosmo/heartbeat.rbs +36 -0
  69. data/sig/cosmo/http_server/handler.rbs +17 -0
  70. data/sig/cosmo/http_server/health.rbs +21 -0
  71. data/sig/cosmo/http_server/ping.rbs +14 -0
  72. data/sig/cosmo/http_server.rbs +22 -0
  73. data/sig/cosmo/job/failure.rbs +28 -0
  74. data/sig/cosmo/job/processor.rbs +17 -1
  75. data/sig/cosmo/job/stream_filter.rbs +25 -0
  76. data/sig/cosmo/logger.rbs +10 -0
  77. data/sig/cosmo/processor.rbs +12 -4
  78. data/sig/cosmo/stream/message.rbs +3 -1
  79. data/sig/cosmo/stream/processor.rbs +2 -0
  80. data/sig/cosmo/utils/thread_pool.rbs +3 -0
  81. data/sig/cosmo.rbs +4 -0
  82. metadata +33 -20
  83. data/lib/cosmo/api/busy.rb +0 -66
  84. data/sig/cosmo/api/busy.rbs +0 -35
@@ -5,27 +5,62 @@ require "timeout"
5
5
  module Cosmo
6
6
  module Job
7
7
  class Processor < ::Cosmo::Processor
8
+ # Headers the scheduler consumes rather than forwards. The +X-+ ones address the dispatch
9
+ # itself, and NATS sets +Nats-Scheduler+/+Nats-Schedule-Next+ on a message it fires and
10
+ # rejects a publish that carries them back.
11
+ DISPATCH_HEADERS = %w[X-Stream X-Subject X-Execute-At Nats-Expected-Stream
12
+ Nats-Scheduler Nats-Schedule-Next].freeze
13
+
14
+ # @raise [UnknownJobStreamError] when --streams/--stream or COSMO_JOBS_STREAMS names an unconfigured stream
15
+ def self.validate_options!(options)
16
+ StreamFilter.from(options[:streams]).validate!
17
+ end
18
+
19
+ # @return [Array<String>] job streams pulled from, plus +scheduled+ when dispatching scheduled jobs
20
+ def subscriptions
21
+ names = @consumers.map { |(_, config, _)| config[:stream].to_s }
22
+ names << StreamFilter::SCHEDULED.to_s if scheduler? && scheduled_config
23
+ names
24
+ end
25
+
8
26
  private
9
27
 
10
28
  def setup
29
+ filter = StreamFilter.from(@options[:streams]).validate!
30
+
11
31
  # Initialize singletons before starting to process messages
12
- API::Busy.instance
13
- API::Counter.instance
32
+ API::Stats::Busy.instance
33
+ API::Stats::Totals.instance
14
34
  Limit.instance
15
35
 
16
36
  jobs_config = Config.dig(:consumers, :jobs)
17
37
  jobs_config&.each do |stream_name, config|
18
- next if stream_name == :scheduled # scheduled jobs are handled in schedule_loop
38
+ next if stream_name == StreamFilter::SCHEDULED # scheduled jobs are handled in schedule_loop
39
+ next unless filter.include?(stream_name)
19
40
 
20
41
  @consumers << subscribe(stream_name, config)
21
42
  end
43
+
44
+ log_subscriptions
45
+ end
46
+
47
+ def log_subscriptions
48
+ return if @consumers.empty?
49
+
50
+ names = @consumers.map { |(_, config, _)| config[:consumer] }
51
+ names << consumer_name(StreamFilter::SCHEDULED) if scheduler? && scheduled_config
52
+ Logger.info "subscribed: #{names.join(", ")}#{" (scheduler off)" unless scheduler?}"
53
+ end
54
+
55
+ def scheduled_config
56
+ Config.dig(:consumers, :jobs, StreamFilter::SCHEDULED)
22
57
  end
23
58
 
24
59
  def schedule_loop # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize
25
- config = Config.dig(:consumers, :jobs, :scheduled)
60
+ config = scheduled_config
26
61
  return unless config
27
62
 
28
- subscription, = subscribe(:scheduled, config)
63
+ subscription, = subscribe(StreamFilter::SCHEDULED, config)
29
64
  while running?
30
65
  break unless running?
31
66
 
@@ -33,13 +68,18 @@ module Cosmo
33
68
  timeout = ENV.fetch("COSMO_JOBS_SCHEDULER_FETCH_TIMEOUT", 5).to_f
34
69
  messages = fetch(subscription, batch_size: 100, timeout:)
35
70
  messages&.each do |message|
36
- headers = message.header.except("X-Stream", "X-Subject", "X-Execute-At", "Nats-Expected-Stream")
71
+ headers = message.header.except(*DISPATCH_HEADERS)
37
72
  stream, subject, execute_at = message.header.values_at("X-Stream", "X-Subject", "X-Execute-At")
38
73
  headers["Nats-Expected-Stream"] = stream
74
+ scheduler = message.header["Nats-Scheduler"]
75
+ if scheduler
76
+ headers["X-Scheduled-By"] = scheduler
77
+ headers["X-Enqueued-At"] = message.metadata.timestamp.to_f.to_s
78
+ end
39
79
  execute_at = execute_at.to_i
40
80
 
41
81
  if now >= execute_at
42
- client.publish(subject, message.data, headers: headers)
82
+ client.publish(subject, message.data, header: headers)
43
83
  message.ack
44
84
  else
45
85
  message.nak(delay: Config.to_ns(execute_at - now))
@@ -60,16 +100,11 @@ module Cosmo
60
100
  message = messages.first
61
101
  Logger.debug "received messages #{messages.inspect}"
62
102
  data = Utils::Json.parse(message.data)
63
- unless data
64
- Logger.error ArgumentError.new("malformed payload")
65
- move_message(message)
66
- return
67
- end
103
+ return reject_message(message, ArgumentError.new("malformed payload")) unless data
68
104
 
69
105
  worker_class = Utils::String.safe_constantize(data[:class])
70
106
  unless worker_class
71
- Logger.error ArgumentError.new("#{data[:class]} class not found")
72
- move_message(message, data)
107
+ reject_message(message, ArgumentError.new("#{data[:class]} class not found"), data)
73
108
  notify_batch(data, success: false)
74
109
  return
75
110
  end
@@ -115,9 +150,9 @@ module Cosmo
115
150
  def build_worker(worker_class, data, message)
116
151
  worker_class.new.tap do |worker|
117
152
  worker.jid = data[:jid]
118
- worker.enqueued_at = message.metadata.timestamp
153
+ worker.enqueued_at = enqueued_at(message)
119
154
  worker.attempt = message.metadata.num_delivered
120
- worker.scheduled_by = message.header&.dig("Nats-Scheduler")
155
+ worker.scheduled_by = scheduled_by(message)
121
156
  worker.batch_id = data[:batch_id]
122
157
  end
123
158
  end
@@ -154,7 +189,7 @@ module Cosmo
154
189
  end
155
190
 
156
191
  warn_capped(message, data, capped_at) if capped_at
157
- data[:dead] ? move_message(message, data) : drop_message(message, data)
192
+ data[:dead] ? move_message(message, data, exception) : drop_message(message, data)
158
193
  notify_batch(data, success: false)
159
194
  true
160
195
  end
@@ -207,7 +242,7 @@ module Cosmo
207
242
  config = config.dup
208
243
  config[:batch_size] = 1
209
244
  config[:stream] = stream_name
210
- config[:consumer] = "consumer-#{stream_name}"
245
+ config[:consumer] = consumer_name(stream_name)
211
246
  subscription = client.subscribe(config[:subject], config[:consumer], config.except(:subject, :priority, :stream, :batch_size, :consumer))
212
247
  [subscription, config, nil]
213
248
  end
@@ -217,16 +252,44 @@ module Cosmo
217
252
  Logger.debug "job dropped #{data[:jid]}"
218
253
  end
219
254
 
220
- def move_message(message, data = nil)
255
+ # Logs why a message can't be processed at all and parks it in the DLQ.
256
+ def reject_message(message, error, data = nil)
257
+ Logger.error error
258
+ move_message(message, data, error)
259
+ end
260
+
261
+ def move_message(message, data = nil, exception = nil)
221
262
  klass = data ? Utils::String.underscore(data[:class]) : "default"
222
263
  headers = { "X-Stream" => message.metadata.stream, "X-Subject" => message.subject }
264
+ headers.merge!(Failure.headers(exception)) if exception
223
265
  Client.instance.publish("jobs.dead.#{klass}", message.data, header: headers)
224
266
  message.ack
225
267
  Logger.debug "job moved #{data&.dig(:jid)} to DLQ"
226
268
  end
227
269
 
228
270
  def scheduler?
229
- true
271
+ @options.fetch(:scheduler, true)
272
+ end
273
+
274
+ # +Nats-Scheduler+ on a message NATS fired directly into a job stream, and +X-Scheduled-By+
275
+ # on one the scheduler dispatched onward, which may not carry the reserved header.
276
+ def scheduled_by(message)
277
+ header = message.header or return
278
+
279
+ header["X-Scheduled-By"] || header["Nats-Scheduler"]
280
+ end
281
+
282
+ # A cron message is enqueued when NATS fires it, not when the scheduler dispatches it on, so
283
+ # the firing forwards its own timestamp in +X-Enqueued-At+ to survive the re-publish.
284
+ def enqueued_at(message)
285
+ forwarded = message.header&.dig("X-Enqueued-At")
286
+
287
+ forwarded ? Time.at(forwarded.to_f) : message.metadata.timestamp
288
+ end
289
+
290
+ # Durable and per stream, so every process pulls from the same consumer and shares the work.
291
+ def consumer_name(stream_name)
292
+ "consumer-#{stream_name}"
230
293
  end
231
294
 
232
295
  def consumers
@@ -243,8 +306,8 @@ module Cosmo
243
306
  end
244
307
 
245
308
  def with_stats(message, &block)
246
- API::Busy.instance.with(message) do
247
- API::Counter.instance.with(&block)
309
+ API::Stats::Busy.instance.with(message) do
310
+ API::Stats::Totals.instance.with(&block)
248
311
  end
249
312
  end
250
313
 
@@ -253,15 +316,14 @@ module Cosmo
253
316
  # @param message [NATS::Msg]
254
317
  # @param duration [Float, nil]
255
318
  #
256
- # rubocop:disable Lint/UnusedMethodArgument
319
+ # rubocop:disable-next Lint/UnusedMethodArgument
257
320
  def perform_job(job_instance, data:, message:, duration: nil)
258
321
  if duration
259
- Timeout.timeout(duration) { job_instance.perform(*data[:args]) }
322
+ Timeout.timeout(duration, Timeout::Error, "execution expired after the #{duration}s duration limit") { job_instance.perform(*data[:args]) }
260
323
  else
261
324
  job_instance.perform(*data[:args])
262
325
  end
263
326
  end
264
- # rubocop:enable Lint/UnusedMethodArgument
265
327
  end
266
328
  end
267
329
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cosmo
4
+ module Job
5
+ # Which job streams a worker subscribes to: every configured stream, or the subset named by
6
+ # +--streams+/+--stream+, falling back to the +COSMO_JOBS_STREAMS+ environment variable.
7
+ #
8
+ # The scheduled stream is a service stream. Naming it selects nothing, it runs unless the worker
9
+ # was started with +--no-scheduler+.
10
+ class StreamFilter
11
+ SCHEDULED = :scheduled
12
+ ENV_NAME = "COSMO_JOBS_STREAMS"
13
+
14
+ # @param names [Array<String>, nil] stream names from the command line
15
+ # @return [StreamFilter]
16
+ def self.from(names)
17
+ new(names || ENV[ENV_NAME]&.split(","))
18
+ end
19
+
20
+ # @param names [Array<String>, nil]
21
+ def initialize(names)
22
+ @names = Array(names).map(&:strip).reject(&:empty?)
23
+ end
24
+
25
+ # @return [Boolean] whether every configured stream is selected
26
+ def all?
27
+ @names.empty?
28
+ end
29
+
30
+ # @param stream_name [String, Symbol]
31
+ # @return [Boolean] whether the stream is selected
32
+ def include?(stream_name)
33
+ all? || @names.include?(stream_name.to_s)
34
+ end
35
+
36
+ # @return [StreamFilter] self, so it can be chained onto {.from}
37
+ # @raise [UnknownJobStreamError] when a name is not configured
38
+ def validate!
39
+ return self if all?
40
+
41
+ unknown = @names - configured - [SCHEDULED.to_s]
42
+ raise UnknownJobStreamError.new(unknown, configured) if unknown.any?
43
+
44
+ self
45
+ end
46
+
47
+ private
48
+
49
+ def configured
50
+ @configured ||= Config.dig(:consumers, :jobs)&.keys.to_a.map(&:to_s) - [SCHEDULED.to_s]
51
+ end
52
+ end
53
+ end
54
+ end
data/lib/cosmo/job.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "cosmo/job/data"
4
+ require "cosmo/job/failure"
5
+ require "cosmo/job/stream_filter"
4
6
  require "cosmo/job/limit"
5
7
  require "cosmo/job/processor"
6
8
 
data/lib/cosmo/logger.rb CHANGED
@@ -5,6 +5,8 @@ require "forwardable"
5
5
 
6
6
  module Cosmo
7
7
  module Logger
8
+ TRACE = -1 # Below DEBUG; opt in with COSMO_LOG_LEVEL=trace for high-frequency polling/loop noise
9
+
8
10
  module Context
9
11
  KEY = :cosmo_context
10
12
 
@@ -45,10 +47,21 @@ module Cosmo
45
47
  end
46
48
  end
47
49
 
50
+ # Adds TRACE (below DEBUG) on top of stdlib's fixed DEBUG..UNKNOWN severities.
51
+ class Instance < ::Logger
52
+ def trace(progname = nil, &)
53
+ add(TRACE, nil, progname, &)
54
+ end
55
+
56
+ def format_severity(severity)
57
+ severity == TRACE ? "TRACE" : super
58
+ end
59
+ end
60
+
48
61
  class << self
49
62
  extend Forwardable
50
63
 
51
- delegate %i[info error debug warn fatal] => :instance
64
+ delegate %i[info error debug warn fatal trace] => :instance
52
65
  end
53
66
 
54
67
  def self.with(...)
@@ -60,12 +73,17 @@ module Cosmo
60
73
  end
61
74
 
62
75
  def self.instance
63
- @instance ||= ::Logger.new($stdout).tap do |logger|
76
+ @instance ||= Instance.new($stdout).tap do |logger|
64
77
  logger.formatter = SimpleFormatter.new
65
- logger.level = ::Logger::Severity.coerce(ENV.fetch("COSMO_LOG_LEVEL", "info"))
78
+ logger.level = coerce_level(ENV.fetch("COSMO_LOG_LEVEL", "info"))
66
79
  end
67
80
  end
68
81
 
82
+ def self.coerce_level(level)
83
+ level.to_s.downcase == "trace" ? TRACE : ::Logger::Severity.coerce(level)
84
+ end
85
+ private_class_method :coerce_level
86
+
69
87
  def self.instance=(logger)
70
88
  @instance = logger
71
89
  end
@@ -5,20 +5,29 @@ module Cosmo
5
5
  STREAM_PAUSED_RECHECK_TTL = "5s" # How long a stream's paused state is cached before re-checking (override via COSMO_STREAM_PAUSED_RECHECK_TTL)
6
6
  STREAMS_PAUSED_IDLE_SLEEP = "1s" # How long to sleep when every stream is paused, preventing a tight CPU spin (override via COSMO_STREAMS_PAUSED_IDLE_SLEEP)
7
7
  STREAM_EMPTY_BACKOFF_MAX = "5s" # Max sleep between empty fetches (override via COSMO_STREAM_EMPTY_BACKOFF_MAX)
8
+ QUIET_SLEEP = "5s" # How long to sleep between checks while in quiet mode (override via COSMO_QUIET_IDLE_SLEEP)
8
9
 
9
10
  def self.run(...)
10
11
  new(...).tap(&:run)
11
12
  end
12
13
 
14
+ # Checks the command-line options this processor understands, before anything is booted or connected.
15
+ #
16
+ # @param options [Hash]
17
+ # @return [void]
18
+ def self.validate_options!(_options); end
19
+
13
20
  attr_reader :consumers
14
21
 
15
- def initialize(pool, running, options)
22
+ def initialize(pool, running, options, quiet:)
16
23
  @pool = pool
17
24
  @running = running
25
+ @quiet = quiet
18
26
  @options = options
19
27
  @threads = []
20
28
  @consumers = []
21
29
  @cache = Utils::TTLCache.new
30
+ @busy = Concurrent::AtomicFixnum.new
22
31
  end
23
32
 
24
33
  def run
@@ -29,6 +38,13 @@ module Cosmo
29
38
  run_loop
30
39
  end
31
40
 
41
+ # Threads processing fetched messages right now; threads waiting on a fetch don't count.
42
+ #
43
+ # @return [Integer]
44
+ def busy
45
+ @busy.value
46
+ end
47
+
32
48
  def stop(timeout = Config[:timeout])
33
49
  @running.make_false
34
50
  @pool.shutdown
@@ -52,6 +68,11 @@ module Cosmo
52
68
  while running?
53
69
  break if shutdown
54
70
 
71
+ if quiet?
72
+ sleep(Utils::Duration.parse(ENV.fetch("COSMO_QUIET_IDLE_SLEEP", QUIET_SLEEP)))
73
+ next
74
+ end
75
+
55
76
  all_empty = true # every stream is empty
56
77
  all_paused = true # every stream is paused
57
78
  consumers.each do |(subscription, config, processor)| # rubocop:disable Metrics/BlockLength
@@ -60,14 +81,14 @@ module Cosmo
60
81
  stream_name = config[:stream].to_s
61
82
  ttl = Utils::Duration.parse(ENV.fetch("COSMO_STREAM_PAUSED_RECHECK_TTL", STREAM_PAUSED_RECHECK_TTL))
62
83
  if @cache.fetch("#{stream_name}:paused", ttl:) { API::Stream.new(stream_name).paused? }
63
- Logger.debug "stream #{stream_name} is paused, skipping fetch"
84
+ Logger.trace "stream #{stream_name} is paused, skipping fetch"
64
85
  next
65
86
  end
66
87
  all_paused = false
67
88
 
68
89
  _, skip_t = consumer_state[stream_name]
69
90
  if skip_t && Time.now < skip_t
70
- Logger.debug "stream #{stream_name} is empty, backing off"
91
+ Logger.trace "stream #{stream_name} is empty, backing off"
71
92
  next
72
93
  end
73
94
  all_empty = false
@@ -79,16 +100,19 @@ module Cosmo
79
100
  next if skip_t && Time.now < skip_t
80
101
 
81
102
  timeout = fetch_timeout(config)
82
- Logger.debug "fetching #{fetch_subjects(config).inspect}, timeout=#{timeout}"
83
- messages = lock(stream_name) { fetch(subscription, batch_size: config[:batch_size], timeout:) }
84
- Logger.debug "fetched (#{messages&.size.to_i}) messages"
103
+ Logger.trace "fetching #{fetch_subjects(config).inspect}, timeout=#{timeout}"
104
+ started_at = Time.now
105
+ messages = fetch(subscription, batch_size: config[:batch_size], timeout:)
106
+ Logger.trace "fetched (#{messages&.size.to_i}) messages"
85
107
  if messages&.any?
86
108
  consumer_state.delete(stream_name)
87
- process(messages, processor)
109
+ track_busy { process(messages, processor) }
88
110
  else
89
111
  max_backoff = Utils::Duration.parse(ENV.fetch("COSMO_STREAM_EMPTY_BACKOFF_MAX", STREAM_EMPTY_BACKOFF_MAX))
90
- consumer_state.compute(stream_name) do |current|
91
- count = (current&.first || 0) + 1
112
+ consumer_state.compute(stream_name) do |(count, wake_at)|
113
+ next [count, wake_at] if wake_at && wake_at > started_at
114
+
115
+ count = count.to_i + 1
92
116
  backoff = [timeout * (2**(count - 1)), max_backoff].min
93
117
  [count, Time.now + backoff]
94
118
  end
@@ -104,14 +128,14 @@ module Cosmo
104
128
 
105
129
  if all_paused
106
130
  period = Utils::Duration.parse(ENV.fetch("COSMO_STREAMS_PAUSED_IDLE_SLEEP", STREAMS_PAUSED_IDLE_SLEEP))
107
- Logger.debug "all streams paused, sleep=#{period}"
131
+ Logger.trace "all streams paused, sleep=#{period}"
108
132
  sleep(period)
109
133
  elsif all_empty
110
134
  next_wake = consumer_state.values.filter_map { |_, t| t }.min
111
135
  next unless next_wake # entry was deleted concurrently (messages arrived), re-loop immediately
112
136
 
113
137
  remaining = [next_wake - Time.now, 0.01].max
114
- Logger.debug "all streams empty, sleep=#{remaining}"
138
+ Logger.trace "all streams empty, sleep=#{remaining}"
115
139
  sleep(remaining)
116
140
  end
117
141
  end
@@ -129,10 +153,21 @@ module Cosmo
129
153
  raise NotImplementedError
130
154
  end
131
155
 
156
+ def track_busy
157
+ @busy.increment
158
+ yield
159
+ ensure
160
+ @busy.decrement
161
+ end
162
+
132
163
  def running?
133
164
  @running.true?
134
165
  end
135
166
 
167
+ def quiet?
168
+ @quiet.true?
169
+ end
170
+
136
171
  def scheduler?
137
172
  false
138
173
  end
@@ -158,11 +193,6 @@ module Cosmo
158
193
  Utils::Stopwatch.new
159
194
  end
160
195
 
161
- def lock(stream_name, &)
162
- @locks ||= Hash.new { |h, k| h[k] = Mutex.new }
163
- @locks[stream_name].synchronize(&)
164
- end
165
-
166
196
  def consumer_state
167
197
  @consumer_state ||= Concurrent::Map.new
168
198
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
3
+ # rubocop:disable-next Metrics/MethodLength, Metrics/AbcSize
4
4
  module Cosmo
5
5
  module Sentry
6
6
  module JobProcessorMiddleware
@@ -63,4 +63,3 @@ module Cosmo
63
63
  end
64
64
  end
65
65
  end
66
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
@@ -7,8 +7,9 @@ module Cosmo
7
7
  class Message
8
8
  extend Forwardable
9
9
 
10
- delegate %i[subject reply header metadata ack nack term in_progress] => :@msg
10
+ delegate %i[subject reply header metadata ack nak term in_progress] => :@msg
11
11
  delegate %i[timestamp num_delivered num_pending] => :metadata
12
+ alias nack nak
12
13
 
13
14
  def initialize(msg, serializer: nil)
14
15
  @msg = msg
@@ -3,6 +3,11 @@
3
3
  module Cosmo
4
4
  module Stream
5
5
  class Processor < ::Cosmo::Processor
6
+ # @return [Array<String>] names of the stream classes being processed
7
+ def subscriptions
8
+ @consumers.map { |(_, config, _)| config[:class].name }
9
+ end
10
+
6
11
  private
7
12
 
8
13
  def setup
@@ -8,7 +8,7 @@ module Cosmo
8
8
  module_function
9
9
 
10
10
  def parse(value, default: nil, symbolize_names: true, **options)
11
- JSON.parse(value, options.merge(symbolize_names:))
11
+ JSON.parse(value, **options, symbolize_names:)
12
12
  rescue TypeError, JSON::ParserError
13
13
  default
14
14
  end