async-tools 0.1.10 → 0.2.2
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/.github/workflows/main.yml +3 -1
- data/.rubocop.yml +9 -0
- data/Gemfile.lock +1 -1
- data/Rakefile +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 +66 -0
- data/lib/async/bus.rb +42 -7
- data/lib/async/logger.rb +11 -0
- data/lib/async/tools/version.rb +1 -1
- data/lib/async/tools.rb +1 -0
- metadata +10 -3
- data/lib/async/bus/bus.rb +0 -95
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b3b08f9b120bebf744df3e7c0da6e2cb7c8d4c5acb01458c25f8be8ff7588fa
|
4
|
+
data.tar.gz: 7c02d0bfce7a3bcb21b8ac8b2f4a8aec4bfad5a12233141b7d1d73b139c08684
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 496680113c44a54745da6846bca478f426454eaa34c125de569205c9f5fe3bc82ec6fe53589f8c86c0b1222361ec949c7938baa64b2be0246a15da280ca8f30a
|
7
|
+
data.tar.gz: 14d6d23403ede1040d74e68590ad0572e1c01b402ff6d7d0ff25dfb11c6f0f7a187b894c5681772e7828c88a8ba5f2e95751a3bce58ba9d9dc2aa14c31078e67
|
data/.github/workflows/main.yml
CHANGED
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
data/Rakefile
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
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Async::App
|
4
|
+
extend Async::App::Injector
|
5
|
+
|
6
|
+
include Async::Logger
|
7
|
+
|
8
|
+
inject :bus
|
9
|
+
|
10
|
+
# rubocop:disable Style/GlobalVars
|
11
|
+
def initialize
|
12
|
+
raise "only one instance of #{self.class} is allowed" if $__ASYNC_APP
|
13
|
+
|
14
|
+
$__ASYNC_APP = self
|
15
|
+
@task = Async::Task.current
|
16
|
+
|
17
|
+
set_traps!
|
18
|
+
{
|
19
|
+
bus: Async::Bus.new(app_name),
|
20
|
+
**container_config
|
21
|
+
}.each { container.register(_1, _2) }
|
22
|
+
|
23
|
+
start_metrics_server!
|
24
|
+
run!
|
25
|
+
info { "Started" }
|
26
|
+
rescue StandardError => e
|
27
|
+
fatal { e }
|
28
|
+
stop
|
29
|
+
exit(1)
|
30
|
+
end
|
31
|
+
# rubocop:enable Style/GlobalVars
|
32
|
+
|
33
|
+
def container = @container ||= Dry::Container.new
|
34
|
+
def run! = nil
|
35
|
+
def container_config = {}
|
36
|
+
def app_name = :async_app
|
37
|
+
|
38
|
+
def stop
|
39
|
+
@task&.stop
|
40
|
+
info { "Stopped" }
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def set_traps!
|
46
|
+
trap("INT") do
|
47
|
+
force_exit! if @stopping
|
48
|
+
@stopping = true
|
49
|
+
warn { "Interrupted, stopping. Press ^C once more to force exit." }
|
50
|
+
stop
|
51
|
+
end
|
52
|
+
|
53
|
+
trap("TERM") { stop }
|
54
|
+
end
|
55
|
+
|
56
|
+
def force_exit!
|
57
|
+
fatal { "Forced exit" }
|
58
|
+
exit(1)
|
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
|
66
|
+
end
|
data/lib/async/bus.rb
CHANGED
@@ -1,12 +1,47 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
3
|
+
class Async::Bus
|
4
|
+
include Async::Logger
|
5
|
+
# dry-events is not a dependency of async-tools on purpose.
|
6
|
+
# add it to your bundle yourself
|
7
|
+
|
8
|
+
# Semantics:
|
9
|
+
# - Lazily registeres events
|
10
|
+
# - Synchronous by default
|
11
|
+
# - Catches exceptions in subscribers, logs them
|
12
|
+
def initialize(name)
|
13
|
+
@name = name
|
14
|
+
@w = Class.new.include(Dry::Events::Publisher[name]).new
|
15
|
+
end
|
16
|
+
|
17
|
+
# BLOCKING unless subscribers run in tasks
|
18
|
+
def publish(name, *args, **params)
|
19
|
+
@w.register_event(name)
|
20
|
+
@w.publish(name, payload: (args.first || params))
|
21
|
+
rescue StandardError => e
|
22
|
+
log_error(name, e)
|
23
|
+
end
|
24
|
+
|
25
|
+
# NON-BLOCKING
|
26
|
+
def subscribe(name)
|
27
|
+
@w.register_event(name)
|
28
|
+
@w.subscribe(name) { yield(_1[:payload]) }
|
29
|
+
end
|
30
|
+
|
31
|
+
# NON-BLOCKING, runs subscriber in a task
|
32
|
+
def async_subscribe(name, parent: Async::Task.current)
|
33
|
+
subscribe(name) do |event|
|
34
|
+
parent.async do
|
35
|
+
yield(event)
|
36
|
+
rescue StandardError => e
|
37
|
+
log_error(name, e)
|
38
|
+
end
|
10
39
|
end
|
11
40
|
end
|
41
|
+
|
42
|
+
def convert(from_event, to_event) = subscribe(from_event) { publish(to_event, **yield(_1)) }
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def log_error(name, e) = warn("Subscriber for #{name.inspect} failed with exception.", e)
|
12
47
|
end
|
data/lib/async/logger.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Async::Logger
|
4
|
+
[:debug, :info, :warn, :error, :fatal].each do |name|
|
5
|
+
define_method(name) do |*args, &block|
|
6
|
+
info = respond_to?(:logger_info, true) ? logger_info : nil
|
7
|
+
|
8
|
+
Console.logger.public_send(name, self, info, *args, &block)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
data/lib/async/tools/version.rb
CHANGED
data/lib/async/tools.rb
CHANGED
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.
|
4
|
+
version: 0.2.2
|
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-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: async
|
@@ -60,9 +60,16 @@ files:
|
|
60
60
|
- async-tools.gemspec
|
61
61
|
- bin/console
|
62
62
|
- bin/setup
|
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
|
63
70
|
- lib/async/bus.rb
|
64
|
-
- lib/async/bus/bus.rb
|
65
71
|
- lib/async/channel.rb
|
72
|
+
- lib/async/logger.rb
|
66
73
|
- lib/async/q.rb
|
67
74
|
- lib/async/result_notification.rb
|
68
75
|
- lib/async/timer.rb
|
data/lib/async/bus/bus.rb
DELETED
@@ -1,95 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Async::Bus::Bus
|
4
|
-
attr_reader :name
|
5
|
-
|
6
|
-
include Console
|
7
|
-
|
8
|
-
def initialize(name: :default, limit: 10, parent: Async::Task.current)
|
9
|
-
@name = name
|
10
|
-
@limit = limit
|
11
|
-
@parent = parent
|
12
|
-
|
13
|
-
@closed = false
|
14
|
-
|
15
|
-
@subscribers = Hash.new { |hash, key| hash[key] = [] }
|
16
|
-
end
|
17
|
-
|
18
|
-
# Blocks if any of output channels is full!
|
19
|
-
def publish(nameable_or_event, **payload)
|
20
|
-
check_if_open!
|
21
|
-
name, payload = normalize(nameable_or_event, payload)
|
22
|
-
|
23
|
-
subs = @subscribers[name]
|
24
|
-
return if subs.empty?
|
25
|
-
|
26
|
-
subs.each do |chan|
|
27
|
-
publishing_blocked if chan.full?
|
28
|
-
chan << [name, payload]
|
29
|
-
end
|
30
|
-
Async::Task.current.yield
|
31
|
-
end
|
32
|
-
|
33
|
-
# Blocks!
|
34
|
-
def subscribe(nameable, callable = nil, &block)
|
35
|
-
check_if_open!
|
36
|
-
callable ||= block
|
37
|
-
unless callable.respond_to?(:call)
|
38
|
-
raise ArgumentError, "callable or block must be provided. callable must respond to :call"
|
39
|
-
end
|
40
|
-
|
41
|
-
event_name = normalize(nameable).first
|
42
|
-
|
43
|
-
chan = Async::Channel.new(@limit)
|
44
|
-
@subscribers[event_name] << chan
|
45
|
-
serve(chan, event_name, callable)
|
46
|
-
end
|
47
|
-
|
48
|
-
def async_subscribe(*, **, &) = @parent.async { subscribe(*, **, &) }
|
49
|
-
def on_event(&block) = @on_event_callback = block
|
50
|
-
|
51
|
-
def close
|
52
|
-
return if @closed
|
53
|
-
|
54
|
-
@closed = true
|
55
|
-
|
56
|
-
@subscribers.values.flatten.each(&:close)
|
57
|
-
@subscribers.clear
|
58
|
-
end
|
59
|
-
|
60
|
-
private
|
61
|
-
|
62
|
-
def normalize(nameable, payload = nil)
|
63
|
-
return [nameable, payload] if nameable.is_a?(Symbol)
|
64
|
-
return [nameable.to_sym, payload] if nameable.is_a?(String)
|
65
|
-
return [nameable.event_name.to_sym, nameable] if nameable.respond_to?(:event_name)
|
66
|
-
|
67
|
-
n = nameable[:event_name] || nameable["event_name"]
|
68
|
-
return [n.to_sym, nameable] if n
|
69
|
-
|
70
|
-
raise ArgumentError, "cannot infer event name from #{nameable.inspect}"
|
71
|
-
end
|
72
|
-
|
73
|
-
def serve(chan, event_name, callable)
|
74
|
-
stopped = false
|
75
|
-
unsub = lambda {
|
76
|
-
chan.close
|
77
|
-
stopped = true
|
78
|
-
@subscribers[event_name].delete(chan)
|
79
|
-
}
|
80
|
-
|
81
|
-
chan.each do |name, payload|
|
82
|
-
@on_event_callback&.call(wrapper)
|
83
|
-
callable.call(payload, unsub:, meta: { bus: self })
|
84
|
-
break if stopped
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def publishing_blocked
|
89
|
-
logger.warn(self) { "One of the subscribers is slow, blocking publishing. Event name: #{name}" }
|
90
|
-
end
|
91
|
-
|
92
|
-
def check_if_open!
|
93
|
-
raise "Bus is closed" if @closed
|
94
|
-
end
|
95
|
-
end
|