async-tools 0.2.1 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
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