async-tools 0.2.1 → 0.2.4

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: 37e9af668ebad3f0c2356cc2a428b1bedf8b432e239099dd1b961f16d0b01036
4
- data.tar.gz: 201bc5e60a2b19e5a1b1498c14c0e792e96bd21a688b3605f443e6f98a2b6f29
3
+ metadata.gz: ec1be570dca6a72219f3a39a96a7df91da1783ac2eeff110797f924a3fea3f5e
4
+ data.tar.gz: f16ce65bc17d3652f6e89eb44a95c9f07ba9fd6852328d317d0a023bdaff5138
5
5
  SHA512:
6
- metadata.gz: f92056eef4bc4dc65210c682b8637fc6e39088b1213cca0f4b9bf54a06a5b736ce841c351bbaf211bac3777ccda8a7220ee2a926d822e54758a83e87b5c856f8
7
- data.tar.gz: e463ee25e793a281c1130ede835c427f03c47f15bd51d708f28396533652ef6be14e7ecd0b08b577ec2dadf73e8ec90522bc986a6911b64ea1f049c3c5cd264e
6
+ metadata.gz: 13f5d055571394818ed8f3e9e06abd39bcb778a060b489fff53331f572a512089767967b0ba16697a8b024c242cfe55c709ccf5be954e4425e33d58cc7b5c512
7
+ data.tar.gz: 1f79f53e28dccd8355416e9f51ee25cd65df6d8253375853122173caef6cc5173cb5923fab212399fd71d160276c2898ffeae79e32df94ae48cf43465b5f2d36
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- async-tools (0.2.1)
4
+ async-tools (0.2.4)
5
5
  async (~> 2.3)
6
6
  zeitwerk (~> 2.6)
7
7
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Async::App::Component
4
+ def self.included(base)
5
+ base.extend(Async::App::Injector)
6
+ base.inject(:bus)
7
+
8
+ base.include(Async::Logger)
9
+
10
+ strict = Dry.Types::Strict
11
+
12
+ string_like = (strict::String | strict::Symbol).constructor(&:to_s)
13
+ kv = strict::Hash.map(string_like, strict::String)
14
+
15
+ base.const_set(:T, Module.new do
16
+ include Dry.Types
17
+ const_set(:StringLike, string_like)
18
+ const_set(:KV, kv)
19
+ end)
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Async::App::Injector
4
+ def inject(name)
5
+ define_method(name) do
6
+ $__ASYNC_APP.container[name] # rubocop:disable Style/GlobalVars
7
+ end
8
+ private name
9
+ end
10
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Async::App::Metrics::RubyRuntimeMonitor
4
+ include Async::Logger
5
+
6
+ INTERVAL = 2
7
+
8
+ def run
9
+ Async::Timer.new(INTERVAL, run_on_start: true, on_error: ->(e) { warn(e) }) do
10
+ fibers = ObjectSpace.each_object(Fiber)
11
+ threads = ObjectSpace.each_object(Thread)
12
+ ractors = ObjectSpace.each_object(Ractor)
13
+
14
+ yield({
15
+ ruby_fibers: { value: fibers.count },
16
+ ruby_fibers_active: { value: fibers.count(&:alive?) },
17
+ ruby_threads: { value: threads.count },
18
+ ruby_threads_active: { value: threads.count(&:alive?) },
19
+ ruby_ractors: { value: ractors.count },
20
+ ruby_memory: { value: GetProcessMem.new.bytes.to_s("F"), suffix: "bytes" }
21
+ })
22
+ end
23
+ info { "Started" }
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Async::App::Metrics::Serializer
4
+ def initialize(prefix:)
5
+ @prefix = prefix
6
+ end
7
+
8
+ def serialize(metrics)
9
+ metrics.flat_map { metric_line(_1) }
10
+ .compact
11
+ .join("\n")
12
+ .then { "#{_1}\n" }
13
+ end
14
+
15
+ def metric_name(value) = "#{@prefix}_#{value[:name]}_#{value[:suffix]}"
16
+
17
+ def metric_labels(value) = value[:labels].map { |tag, tag_value| "#{tag}=#{tag_value.to_s.inspect}" }.join(",")
18
+
19
+ def metric_line(value)
20
+ labels = metric_labels(value)
21
+
22
+ "#{metric_name(value)}{#{labels}} #{value[:value]}" if value.key?(:value)
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Async::App::Metrics::Server
4
+ include Async::Logger
5
+
6
+ PATHS = ["/metrics", "/metrics/"].freeze
7
+
8
+ def initialize(prefix:, port: 8080)
9
+ @prefix = prefix
10
+ @port = port
11
+ end
12
+
13
+ def run
14
+ Async::App::Metrics::RubyRuntimeMonitor.new.run { update_metrics(_1) }
15
+
16
+ endpoint = Async::HTTP::Endpoint.parse("http://0.0.0.0:#{@port}")
17
+ Async { Async::HTTP::Server.new(self, endpoint).run }
18
+ info { "Started on #{endpoint.url}" }
19
+ end
20
+
21
+ def call(request)
22
+ return Protocol::HTTP::Response[404, {}, ["Not found"]] unless PATHS.include?(request.path)
23
+
24
+ Protocol::HTTP::Response[200, {}, serializer.serialize(metrics_store)]
25
+ end
26
+
27
+ def update_metrics(metrics) = metrics.each { metrics_store.set(_1, **_2) }
28
+
29
+ private
30
+
31
+ def metrics_store = @metrics_store ||= Async::App::Metrics::Store.new
32
+ def serializer = @serializer ||= Async::App::Metrics::Serializer.new(prefix: @prefix)
33
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Async::App::Metrics::Store
4
+ include Enumerable
5
+
6
+ def set(name, value:, suffix: "total", **labels)
7
+ key = [name, labels]
8
+ counters[key] ||= { name:, labels:, suffix:, value: }
9
+ counters[key].merge!(value:)
10
+ end
11
+
12
+ def each(&) = counters.values.each(&)
13
+
14
+ private
15
+
16
+ def counters = @counters ||= {}
17
+ end
data/lib/async/app.rb CHANGED
@@ -1,52 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Async::App
4
- # rubocop:disable Style/GlobalVars
5
-
6
- module Injector
7
- def inject(name)
8
- define_method(name) do
9
- $__ASYNC_APP.container[name]
10
- end
11
- private name
12
- end
13
- end
14
-
15
- module Component
16
- def self.included(base)
17
- base.extend(Injector)
18
- base.inject(:bus)
4
+ extend Async::App::Injector
19
5
 
20
- base.include(Async::Logger)
21
-
22
- strict = Dry.Types::Strict
23
-
24
- string_like = (strict::String | strict::Symbol).constructor(&:to_s)
25
- kv = strict::Hash.map(string_like, strict::String)
26
-
27
- base.const_set(:T, Module.new do
28
- include Dry.Types
29
- const_set(:StringLike, string_like)
30
- const_set(:KV, kv)
31
- end)
32
- end
33
- end
34
-
35
- extend Injector
36
6
  include Async::Logger
37
7
 
38
8
  inject :bus
39
9
 
10
+ # rubocop:disable Style/GlobalVars
40
11
  def initialize
41
12
  raise "only one instance of #{self.class} is allowed" if $__ASYNC_APP
42
13
 
43
14
  $__ASYNC_APP = self
44
-
45
- container.register(:bus, Async::Bus.new(:__async_app))
15
+ @task = Async::Task.current
46
16
 
47
17
  set_traps!
48
- @task = Async::Task.current
49
- container_config.each { container.register(_1, _2) }
18
+ {
19
+ bus: Async::Bus.new(app_name),
20
+ **container_config
21
+ }.each { container.register(_1, _2) }
22
+
23
+ start_metrics_server!
50
24
  run!
51
25
  info { "Started" }
52
26
  rescue StandardError => e
@@ -59,6 +33,7 @@ class Async::App
59
33
  def container = @container ||= Dry::Container.new
60
34
  def run! = nil
61
35
  def container_config = {}
36
+ def app_name = :async_app
62
37
 
63
38
  def stop
64
39
  @task&.stop
@@ -82,4 +57,10 @@ class Async::App
82
57
  fatal { "Forced exit" }
83
58
  exit(1)
84
59
  end
60
+
61
+ def start_metrics_server!
62
+ Metrics::Server.new(prefix: app_name).tap(&:run).tap do |server|
63
+ bus.subscribe("metrics.updated") { server.update_metrics(_1) }
64
+ end
65
+ end
85
66
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Async::Cache
4
+ Item = Struct.new("Item", :task, :value, :created_at, :duration) do
5
+ def expired? = created_at && Time.now - created_at >= duration
6
+ end
7
+
8
+ def cache(id, duration:, parent: Async::Task.current)
9
+ cleanup!
10
+ find_or_create(id, duration:) do |item|
11
+ parent.async do |task|
12
+ item.task = task
13
+ item.value = yield(id) if block_given?
14
+ item.created_at = Time.now
15
+ end.wait
16
+ end.value
17
+ end
18
+
19
+ def cleanup! = storage.delete_if { _2.expired? }
20
+ def count = storage.count
21
+
22
+ private
23
+
24
+ def find_or_create(id, duration:)
25
+ storage[id].tap do |item|
26
+ item.duration = duration
27
+ item.task&.wait
28
+ return item if item.created_at
29
+
30
+ yield(item)
31
+ end
32
+ end
33
+
34
+ def storage = @storage ||= Hash.new { _1[_2] = Item.new }
35
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # inspired by https://github.com/negativecode/vines/blob/master/lib/vines/token_bucket.rb
4
+ class Async::Throttler
5
+ def initialize(capacity, rate, parent: Async::Task.current)
6
+ raise ArgumentError, "capacity must be > 0" unless capacity.positive?
7
+ raise ArgumentError, "rate must be > 0" unless rate.positive?
8
+
9
+ @capacity = capacity
10
+ @tokens = capacity
11
+ @rate = rate
12
+ @parent = parent
13
+
14
+ @timestamp = Time.new
15
+ end
16
+
17
+ def wait(timeout: 0)
18
+ with_timeout(timeout) do
19
+ while @tokens < 1
20
+ fill!
21
+ sleep(1.0 / @rate)
22
+ end
23
+ end
24
+
25
+ @tokens -= 1
26
+ end
27
+
28
+ def async(parent: @parent, timeout: 0, &)
29
+ wait(timeout:)
30
+ parent.async(&)
31
+ end
32
+
33
+ private
34
+
35
+ def with_timeout(timeout, &)
36
+ return yield if timeout.zero?
37
+
38
+ Fiber.scheduler.with_timeout(timeout, &)
39
+ end
40
+
41
+ def fill!
42
+ return if @tokens >= @capacity
43
+
44
+ now = Time.new
45
+ @tokens += @rate * (now - @timestamp)
46
+ @tokens = @capacity if @tokens > @capacity
47
+ @timestamp = now
48
+ end
49
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Async # rubocop:disable Style/ClassAndModuleChildren
4
4
  module Tools
5
- VERSION = "0.2.1"
5
+ VERSION = "0.2.4"
6
6
  end
7
7
  end
data/lib/async/tools.rb CHANGED
@@ -22,9 +22,11 @@ module Async
22
22
  end
23
23
  end
24
24
 
25
- def self.map(collection, **params, &)
26
- WorkerPool.with(queue_limit: collection.count, **params) do |pool|
27
- pool.schedule_all(collection, &).map(&:wait)
25
+ def self.map(collection, concurrency: nil, parent: Async::Task.current, &)
26
+ Async::Semaphore.new(concurrency || collection.count, parent:).then do |s|
27
+ collection.map do |item|
28
+ s.async { yield(item) }
29
+ end.map(&:wait)
28
30
  end
29
31
  end
30
32
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gleb Sinyavskiy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-03-03 00:00:00.000000000 Z
11
+ date: 2023-03-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -61,11 +61,19 @@ files:
61
61
  - bin/console
62
62
  - bin/setup
63
63
  - lib/async/app.rb
64
+ - lib/async/app/component.rb
65
+ - lib/async/app/injector.rb
66
+ - lib/async/app/metrics/ruby_runtime_monitor.rb
67
+ - lib/async/app/metrics/serializer.rb
68
+ - lib/async/app/metrics/server.rb
69
+ - lib/async/app/metrics/store.rb
64
70
  - lib/async/bus.rb
71
+ - lib/async/cache.rb
65
72
  - lib/async/channel.rb
66
73
  - lib/async/logger.rb
67
74
  - lib/async/q.rb
68
75
  - lib/async/result_notification.rb
76
+ - lib/async/throttler.rb
69
77
  - lib/async/timer.rb
70
78
  - lib/async/tools.rb
71
79
  - lib/async/tools/version.rb