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 +4 -4
- data/Gemfile.lock +1 -1
- data/lib/async/app/component.rb +21 -0
- data/lib/async/app/injector.rb +10 -0
- data/lib/async/app/metrics/ruby_runtime_monitor.rb +25 -0
- data/lib/async/app/metrics/serializer.rb +24 -0
- data/lib/async/app/metrics/server.rb +33 -0
- data/lib/async/app/metrics/store.rb +17 -0
- data/lib/async/app.rb +16 -35
- data/lib/async/cache.rb +35 -0
- data/lib/async/throttler.rb +49 -0
- data/lib/async/tools/version.rb +1 -1
- data/lib/async/tools.rb +5 -3
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec1be570dca6a72219f3a39a96a7df91da1783ac2eeff110797f924a3fea3f5e
|
4
|
+
data.tar.gz: f16ce65bc17d3652f6e89eb44a95c9f07ba9fd6852328d317d0a023bdaff5138
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 13f5d055571394818ed8f3e9e06abd39bcb778a060b489fff53331f572a512089767967b0ba16697a8b024c242cfe55c709ccf5be954e4425e33d58cc7b5c512
|
7
|
+
data.tar.gz: 1f79f53e28dccd8355416e9f51ee25cd65df6d8253375853122173caef6cc5173cb5923fab212399fd71d160276c2898ffeae79e32df94ae48cf43465b5f2d36
|
data/Gemfile.lock
CHANGED
@@ -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,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
|
-
|
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
|
-
|
49
|
-
|
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
|
data/lib/async/cache.rb
ADDED
@@ -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
|
data/lib/async/tools/version.rb
CHANGED
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,
|
26
|
-
|
27
|
-
|
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.
|
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-
|
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
|