cosmonats 0.2.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b63356c69b61ea32b4791b519830003e4f42330d39947a332cfbfdb20ed91c7d
4
- data.tar.gz: 6cc0401b58b06038dcc4dc7e4bea9ff695662fa1b251b89fd681a66e893387c7
3
+ metadata.gz: f850638b66506538b131180c03b64a13e31cd86f924503703812da49b6ba3d20
4
+ data.tar.gz: 0dfdab08193600f3bca2c32558ffb016a38657f9488789770eea5e60612281e9
5
5
  SHA512:
6
- metadata.gz: 5c7ef63abf649154cb9a5e5662e29297cd4b171810f819b16401f08651a5ee3a8c3111421ed3873ea4c12477d05cd6329e18f67feda91edadf7605eadf2a4b91
7
- data.tar.gz: d1bb560e31a2c0fb6c321d52599a543a372f184e136f29778a44451648eaa7beed1405d83d1fe996f87b8ee029432d8f395c3fad348a7eaacb73de958ad31573
6
+ metadata.gz: 56f46ac8a8c1409b1583033ad8b5cf9e513652fae2643b07344aa8f8bf1cca80f948463af92e2a989ec9679cc830ac05a6b873b1f0c9b5e10a1e6ef5e8399ad0
7
+ data.tar.gz: '086ae5c5b26471417e54c37d2f782c0c103340953e9a27ca8c02131478122f01ede30f2f8ab5a0b29c81ce7b4ea07a5c8de8e7f51957e4d9d5dfd5d670a0ff08'
data/README.md CHANGED
@@ -95,62 +95,69 @@ gem "cosmonats"
95
95
 
96
96
  **Requirements:** Ruby 3.1.0+, NATS Server ([installation guide](https://docs.nats.io/running-a-nats-service/introduction/installation))
97
97
 
98
+ Add these lines to config/routes.rb:
99
+ ```ruby
100
+ require "cosmo/web"
101
+
102
+ Rails.application.routes.draw do
103
+ mount Cosmo::Web => "/cosmo" # access web UI at http://localhost:3000/cosmo
104
+ ...
105
+ end
106
+ ```
107
+
98
108
 
99
109
  ## 🚀 Quick Start
100
110
 
101
- ### 1. Create a Job
111
+ ### 1. Create `config/cosmo.yml` and run `bundle exec cosmo -S` to create streams in NATS:
112
+
113
+ ```yaml
114
+ concurrency: 5
115
+ max_retries: 3
116
+
117
+ consumers:
118
+ jobs:
119
+ default:
120
+ ack_policy: explicit
121
+ max_deliver: 10
122
+ max_ack_pending: 10
123
+ ack_wait: 15
124
+ subject: jobs.%{name}.>
125
+
126
+ setup:
127
+ jobs:
128
+ default:
129
+ storage: file
130
+ retention: workqueue
131
+ subjects: ["jobs.%{name}.>"]
132
+ allow_direct: true
133
+ ```
134
+
135
+ ### 2. Create a Job in app/workers
102
136
 
103
137
  ```ruby
104
138
  class SendEmailJob
105
139
  include Cosmo::Job
106
140
 
107
- # configure job options (optional)
108
141
  options stream: :default, retry: 3, dead: true
109
142
 
110
143
  def perform(user_id, email_type)
111
- user = User.find(user_id)
112
- UserMailer.send(email_type, user).deliver_now
144
+ # Pretend to send email to user: UserMailer.send(email_type, user_id).deliver_now
145
+ sleep 0.5 # Simulate work
146
+ puts "#{user_id}, #{email_type}"
113
147
  end
114
148
  end
115
149
  ```
116
150
 
117
- ### 2. Enqueue Jobs
151
+ ### 3. Enqueue Jobs
118
152
 
119
153
  ```ruby
120
- SendEmailJob.perform_async(123, 'welcome') # Immediately
121
- SendEmailJob.perform_in(1.hour, 123, 'reminder') # Delayed
122
- SendEmailJob.perform_at(1.day.from_now, 123, 'test') # Scheduled
154
+ 10.times { |i| SendEmailJob.perform_async(i, "welcome") }
123
155
  ```
124
156
 
125
- ### 3. Configure (config/cosmo.yml)
126
-
127
- ```yaml
128
- concurrency: 10
129
- max_retries: 3
130
-
131
- consumers:
132
- jobs:
133
- default:
134
- ack_policy: explicit
135
- max_deliver: 3
136
- max_ack_pending: 3
137
- ack_wait: 60
138
-
139
- streams:
140
- default:
141
- storage: file
142
- retention: workqueue
143
- subjects: ["jobs.default.>"]
144
- ```
145
-
146
- ### 4. Setup & Run
157
+ ### 4. Run
147
158
 
148
159
  ```bash
149
- # Setup streams
150
- cosmo -C config/cosmo.yml --setup
151
-
152
- # Start processing
153
- cosmo -C config/cosmo.yml -c 10 -r ./app/jobs jobs
160
+ bundle exec cosmo jobs
154
161
  ```
155
162
 
156
163
 
@@ -182,7 +189,7 @@ end
182
189
  # Usage
183
190
  ReportJob.perform_async(42) # Enqueue now
184
191
  ReportJob.perform_in(30.minutes, 42) # Delayed
185
- ReportJob.perform_at(Time.parse('2026-01-25 10:00'), 42) # Scheduled
192
+ ReportJob.perform_at(Time.parse("2026-01-25 10:00"), 42) # Scheduled
186
193
  ```
187
194
 
188
195
  ### Streams
@@ -221,8 +228,8 @@ end
221
228
 
222
229
  # Publishing
223
230
  ClicksProcessor.publish(
224
- { user_id: 123, page: '/home' },
225
- subject: 'events.clicks.homepage'
231
+ { user_id: 123, page: "/home" },
232
+ subject: "events.clicks.homepage"
226
233
  )
227
234
 
228
235
  # Message acknowledgment strategies
@@ -235,32 +242,86 @@ message.term # Permanent failure, no retry
235
242
 
236
243
  **File-based (config/cosmo.yml):**
237
244
  ```yaml
238
- timeout: 25 # Shutdown timeout in seconds
239
- concurrency: 10 # Number of worker threads
240
- max_retries: 3 # Default max retries
245
+ timeout: 25 # Shutdown timeout in seconds
246
+ concurrency: &concurrency 1 # Number of worker threads
247
+ max_retries: &max_retries 3 # Default max retries
248
+
249
+ stream_config: &stream_config
250
+ storage: file # storage type (file or memory)
251
+ retention: workqueue # retention policy (limits, interest, workqueue)
252
+ duplicate_window: 120 # time window for duplicate message detection in seconds
253
+ discard: old # discard new messages when stream is full (discard new or old)
254
+ allow_direct: true # allow direct messages to stream, required for web UI
255
+ subjects:
256
+ - jobs.%{name}.> # subject pattern for stream, %{name} will be replaced with stream name
257
+
258
+ consumer_config: &consumer_config
259
+ ack_policy: explicit # ack policy (explicit, none, all), each individual message must be acknowledged
260
+ max_deliver: 10 # maximum number of times a message will be delivered before it's considered failed
261
+ max_ack_pending: 20 # maximum number of messages with pending ack for this consumer
262
+ ack_wait: 60 # time in seconds to wait for an ack before redelivering the message
263
+ subject: jobs.%{name}.> # subject pattern for consumer, %{name} will be replaced with stream name
241
264
 
242
265
  consumers:
243
- streams:
244
- - class: MyStream
245
- batch_size: 50
246
- consumer:
247
- ack_policy: explicit
248
- max_deliver: 3
249
- subjects: ["events.>"]
266
+ jobs:
267
+ critical:
268
+ <<: *consumer_config
269
+ priority: 50
270
+ high:
271
+ <<: *consumer_config
272
+ priority: 30
273
+ default:
274
+ <<: *consumer_config
275
+ priority: 15
276
+ low:
277
+ <<: *consumer_config
278
+ priority: 5
279
+ scheduled:
280
+ <<: *consumer_config
281
+ max_deliver: 1
282
+ max_ack_pending: 100
283
+ ack_wait: 10
250
284
 
251
285
  setup:
252
- streams:
253
- my_stream:
254
- storage: file # or memory
255
- retention: workqueue # or limits
256
- max_age: 86400 # 1d in seconds
257
- subjects: ["events.>"]
286
+ jobs:
287
+ critical:
288
+ <<: *stream_config
289
+ description: Very critical priority jobs
290
+ high:
291
+ <<: *stream_config
292
+ description: Higher priority jobs
293
+ default:
294
+ <<: *stream_config
295
+ description: Default priority jobs
296
+ low:
297
+ <<: *stream_config
298
+ description: Lower priority jobs
299
+ scheduled:
300
+ <<: *stream_config
301
+ description: Scheduled jobs
302
+ dead:
303
+ <<: *stream_config
304
+ retention: limits
305
+ max_msgs: 10000
306
+ max_age: 604800 # 7d
307
+ description: Broken jobs (DLQ)
308
+
309
+ development:
310
+ verbose: false
311
+ concurrency: *concurrency
312
+
313
+ staging:
314
+ verbose: true
315
+ concurrency: 3
316
+
317
+ production:
318
+ concurrency: 3
258
319
  ```
259
320
 
260
321
  **Programmatic:**
261
322
  ```ruby
262
323
  Cosmo::Config.set(:concurrency, 20)
263
- Cosmo::Config.set(:setup, :streams, :custom, { storage: 'file', subjects: ['custom.>'] })
324
+ Cosmo::Config.set(:setup, :streams, :custom, { storage: "file", subjects: ["custom.>"] })
264
325
  ```
265
326
 
266
327
  **Environment variables:**
@@ -328,10 +389,10 @@ end
328
389
  **Testing:**
329
390
  ```ruby
330
391
  # Synchronous execution
331
- SendEmailJob.perform_sync(123, 'test')
392
+ SendEmailJob.perform_sync(123, "test")
332
393
 
333
394
  # Test job creation
334
- jid = SendEmailJob.perform_async(123, 'welcome')
395
+ jid = SendEmailJob.perform_async(123, "welcome")
335
396
  assert_kind_of String, jid
336
397
  ```
337
398
 
@@ -438,7 +499,7 @@ sudo systemctl status cosmo
438
499
  **Stream Metrics:**
439
500
  ```ruby
440
501
  client = Cosmo::Client.instance
441
- info = client.stream_info('default')
502
+ info = client.stream_info("default")
442
503
 
443
504
  info.state.messages # Total messages
444
505
  info.state.bytes # Total bytes
@@ -465,8 +526,8 @@ class EmailJob
465
526
  end
466
527
  end
467
528
 
468
- EmailJob.perform_async(123, 'welcome')
469
- EmailJob.perform_in(1.day, 123, 'followup')
529
+ EmailJob.perform_async(123, "welcome")
530
+ EmailJob.perform_in(1.day, 123, "followup")
470
531
  ```
471
532
 
472
533
  **Image Processing Pipeline:**
@@ -475,12 +536,12 @@ class ImageProcessor
475
536
  include Cosmo::Stream
476
537
  options(
477
538
  stream: :images,
478
- consumer: { subjects: ['images.uploaded.>'] }
539
+ consumer: { subjects: ["images.uploaded.>"] }
479
540
  )
480
541
 
481
542
  def process_one
482
- processed = ImageService.process(message.data['url'])
483
- publish(processed, subject: 'images.processed.optimized')
543
+ processed = ImageService.process(message.data["url"])
544
+ publish(processed, subject: "images.processed.optimized")
484
545
  message.ack
485
546
  rescue => e
486
547
  logger.error "Processing failed: #{e.message}"
@@ -488,18 +549,18 @@ class ImageProcessor
488
549
  end
489
550
  end
490
551
 
491
- ImageProcessor.publish({ url: 'https://example.com/image.jpg' }, subject: 'images.uploaded.user')
552
+ ImageProcessor.publish({ url: "https://example.com/image.jpg" }, subject: "images.uploaded.user")
492
553
  ```
493
554
 
494
555
  **Real-Time Analytics:**
495
556
  ```ruby
496
557
  class AnalyticsAggregator
497
558
  include Cosmo::Stream
498
- options batch_size: 1000, consumer: { subjects: ['events.*.>'] }
559
+ options batch_size: 1000, consumer: { subjects: ["events.*.>"] }
499
560
 
500
561
  def process(messages)
501
562
  events = messages.map(&:data)
502
- aggregates = events.group_by { |e| e['type'] }.transform_values(&:count)
563
+ aggregates = events.group_by { |e| e["type"] }.transform_values(&:count)
503
564
  Analytics.bulk_insert(aggregates)
504
565
  messages.each(&:ack)
505
566
  end
data/lib/cosmo/api/kv.rb CHANGED
@@ -48,7 +48,7 @@ module Cosmo
48
48
 
49
49
  def count
50
50
  keys.size
51
- rescue NATS::KeyValue::NoKeysFoundError
51
+ rescue NATS::KeyValue::NoKeysFoundError, NATS::JetStream::Error::NotFound
52
52
  0
53
53
  end
54
54
  alias size count
@@ -10,12 +10,13 @@ module Cosmo
10
10
  include Enumerable
11
11
 
12
12
  def self.all
13
- client.list_streams.filter_map { new(_1) }
13
+ client.list_streams.map { new(_1.dig("config", "name")) }
14
14
  end
15
15
 
16
16
  def self.jobs
17
- names = Config[:setup][:jobs].keys - %i[scheduled dead]
18
- all.select { names.include?(_1.name.to_sym) }
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")) }
19
20
  end
20
21
 
21
22
  def self.client
@@ -41,7 +42,7 @@ module Cosmo
41
42
  alias size total
42
43
 
43
44
  def retries
44
- client.list_consumers(name).sum { it["num_redelivered"].to_i }
45
+ client.list_consumers(name).sum { _1["num_redelivered"].to_i }
45
46
  end
46
47
 
47
48
  def each
@@ -100,6 +101,18 @@ module Cosmo
100
101
  client.delete_message(name, seq)
101
102
  end
102
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
+
103
116
  private
104
117
 
105
118
  def client
data/lib/cosmo/cli.rb CHANGED
@@ -106,11 +106,12 @@ module Cosmo
106
106
  configs.each do |name, config|
107
107
  Client.instance.stream_info(name)
108
108
  rescue NATS::JetStream::Error::NotFound
109
- Client.instance.create_stream(name, config)
109
+ meta = { metadata: { "_cosmo.type" => "jobs" } }
110
+ Client.instance.create_stream(name, config.merge(meta))
110
111
  end
111
112
  end
112
113
 
113
- puts "Cosmo streams were created/updated"
114
+ puts "Cosmo streams were created successfully"
114
115
  exit(0)
115
116
  end
116
117
 
data/lib/cosmo/client.rb CHANGED
@@ -38,18 +38,42 @@ module Cosmo
38
38
  js.delete_stream(name, params)
39
39
  end
40
40
 
41
+ def update_stream(name, config)
42
+ js.update_stream(name: name, **config)
43
+ end
44
+
41
45
  def list_streams
42
46
  response = nc.request("$JS.API.STREAM.LIST", "")
43
47
  data = Utils::Json.parse(response.data, symbolize_names: false)
44
48
  return [] if data.nil? || data["streams"].nil?
45
49
 
46
- data["streams"].filter_map { _1.dig("config", "name") }
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
47
71
  end
48
72
 
49
73
  def list_consumers(stream_name)
50
74
  response = nc.request("$JS.API.CONSUMER.LIST.#{stream_name}", "")
51
- data = Utils::Json.parse(response.data, symbolize_names: false)
52
- data["consumers"]
75
+ data = Utils::Json.parse(response.data, default: {}, symbolize_names: false)
76
+ Array(data["consumers"])
53
77
  end
54
78
 
55
79
  def consumer_info(stream_name, consumer_name)
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
 
@@ -59,45 +59,18 @@ module Cosmo
59
59
  @instance ||= new
60
60
  end
61
61
 
62
- def self.system
63
- @system ||= {}
64
- end
65
-
66
- def initialize
67
- @config = nil
68
- @system = {}
69
- @defaults = self.class.parse_file(File.expand_path("defaults.yml", __dir__))
70
- end
71
-
72
- def [](key)
73
- dig(key)
74
- end
75
-
76
- def fetch(key, default = nil)
77
- return @config.fetch(key, default) if @config && Utils::Hash.keys?(@config, key)
78
-
79
- @defaults.fetch(key, default)
80
- end
81
-
82
- def dig(*keys)
83
- return @config&.dig(*keys) if @config && Utils::Hash.keys?(@config, *keys)
84
-
85
- @defaults.dig(*keys)
86
- end
87
-
88
- def to_h
89
- Utils::Hash.merge(@defaults, @config)
62
+ def self.internal
63
+ @internal ||= {}
90
64
  end
91
65
 
92
66
  def set(...)
93
- @config ||= {}
94
- Utils::Hash.set(@config, ...)
67
+ Utils::Hash.set(self, ...)
95
68
  end
96
69
 
97
70
  def load(path = nil)
98
71
  return unless path
99
72
 
100
- @config = self.class.parse_file(path)
73
+ replace(self.class.parse_file(path))
101
74
  end
102
75
  end
103
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) }
@@ -2,74 +2,28 @@
2
2
 
3
3
  module Cosmo
4
4
  module Job
5
- class Processor < ::Cosmo::Processor # rubocop:disable Metrics/ClassLength
6
- def initialize(pool, running, options)
7
- super
8
- @weights = []
9
- end
10
-
11
- def stop(timeout = Config[:timeout])
12
- @running.make_false
13
- @pool.shutdown
14
- @consumers.each { |(s, _)| s.unsubscribe rescue nil }
15
- @pool.wait_for_termination(timeout)
16
- [@work_thread, @schedule_thread].compact.each { _1.join(timeout) || _1.kill }
17
- @consumers.clear
18
- end
19
-
5
+ class Processor < ::Cosmo::Processor
20
6
  private
21
7
 
22
- def run_loop
23
- @work_thread = Thread.new { work_loop }
24
- @schedule_thread = Thread.new { schedule_loop }
25
- end
26
-
27
8
  def setup
28
9
  jobs_config = Config.dig(:consumers, :jobs)
29
10
  jobs_config&.each do |stream_name, config|
30
- config = config.dup
31
- consumer_name = "consumer-#{stream_name}"
32
- subject = config.delete(:subject)
33
- priority = config.delete(:priority)
34
- @weights += ([stream_name] * priority.to_i) if priority
35
- subscription = client.subscribe(subject, consumer_name, config)
36
- @consumers << [subscription, stream_name]
37
- end
38
- end
39
-
40
- def work_loop # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize
41
- shutdown = false
11
+ next if stream_name == :scheduled # scheduled jobs are handled in schedule_loop
42
12
 
43
- while running?
44
- break if shutdown
45
-
46
- @weights.shuffle.each do |stream_name|
47
- break unless running?
48
-
49
- begin
50
- timeout = ENV.fetch("COSMO_JOBS_FETCH_TIMEOUT", 0.1).to_f
51
- @pool.post do
52
- subscription = @consumers.find { |(_, sn)| sn == stream_name }&.first
53
- messages = lock(stream_name) { fetch(subscription, batch_size: 1, timeout:) }
54
- process(messages) if messages&.any?
55
- end
56
- rescue Concurrent::RejectedExecutionError
57
- shutdown = true
58
- break # pool doesn't accept new jobs, we are shutting down
59
- end
60
-
61
- break unless running?
62
- end
13
+ @consumers << subscribe(stream_name, config)
63
14
  end
64
15
  end
65
16
 
66
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)
67
22
  while running?
68
23
  break unless running?
69
24
 
70
25
  now = Time.now.to_i
71
26
  timeout = ENV.fetch("COSMO_JOBS_SCHEDULER_FETCH_TIMEOUT", 5).to_f
72
- subscription = @consumers.find { |(_, sn)| sn == :scheduled }&.first
73
27
  messages = fetch(subscription, batch_size: 100, timeout:)
74
28
  messages&.each do |message|
75
29
  headers = message.header.except("X-Stream", "X-Subject", "X-Execute-At", "Nats-Expected-Stream")
@@ -90,7 +44,7 @@ module Cosmo
90
44
  end
91
45
  end
92
46
 
93
- def process(messages) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
47
+ def process(messages, _) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
94
48
  message = messages.first
95
49
  Logger.debug "received messages #{messages.inspect}"
96
50
  data = Utils::Json.parse(message.data)
@@ -148,6 +102,15 @@ module Cosmo
148
102
  true
149
103
  end
150
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
+
151
114
  def drop_message(message, data)
152
115
  message.term
153
116
  Logger.debug "job dropped #{data[:jid]}"
@@ -161,16 +124,28 @@ module Cosmo
161
124
  Logger.debug "job moved #{data&.dig(:jid)} to DLQ"
162
125
  end
163
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
+
164
144
  def with_stats(message, &block)
165
145
  API::Busy.instance.with(message) do
166
146
  API::Counter.instance.with(&block)
167
147
  end
168
148
  end
169
-
170
- def lock(stream_name, &)
171
- @mutexes ||= Hash.new { |h, k| h[k] = Mutex.new }
172
- @mutexes[stream_name].synchronize(&)
173
- end
174
149
  end
175
150
  end
176
151
  end
data/lib/cosmo/logger.rb CHANGED
@@ -60,7 +60,10 @@ module Cosmo
60
60
  end
61
61
 
62
62
  def self.instance
63
- @instance ||= ::Logger.new($stdout).tap { _1.formatter = SimpleFormatter.new }
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
64
67
  end
65
68
 
66
69
  def self.instance=(logger)