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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e998dd042f7fc4c496862c3ca2feec5f370ac86c803dcc61c55d970f6bae28a
4
- data.tar.gz: fcd146663d144d9ccf9738c58594faabda6b98cf754523b1145b65016715eac7
3
+ metadata.gz: 8b3b08f9b120bebf744df3e7c0da6e2cb7c8d4c5acb01458c25f8be8ff7588fa
4
+ data.tar.gz: 7c02d0bfce7a3bcb21b8ac8b2f4a8aec4bfad5a12233141b7d1d73b139c08684
5
5
  SHA512:
6
- metadata.gz: 6315fe469b3e53cdfb87cd569e4833eb34669ab961d3a16fd1c435b5490dd52087ebfe55b8fea41ea997759918d695ab1f999effecd43cf73419cbd5a60d3a47
7
- data.tar.gz: 720846506b9f600528c7c1ada372741abd42bd2588ab2c4f9e0ec53ea7e19877b08b9bac37b103345092e9fcf67a493c2714149c06e5211f7257c609657385f2
6
+ metadata.gz: 496680113c44a54745da6846bca478f426454eaa34c125de569205c9f5fe3bc82ec6fe53589f8c86c0b1222361ec949c7938baa64b2be0246a15da280ca8f30a
7
+ data.tar.gz: 14d6d23403ede1040d74e68590ad0572e1c01b402ff6d7d0ff25dfb11c6f0f7a187b894c5681772e7828c88a8ba5f2e95751a3bce58ba9d9dc2aa14c31078e67
@@ -1,6 +1,8 @@
1
1
  name: Ruby
2
2
 
3
- on: [push]
3
+ on:
4
+ - push
5
+ - workflow_dispatch
4
6
 
5
7
  jobs:
6
8
  lint:
data/.rubocop.yml CHANGED
@@ -51,3 +51,12 @@ Metrics/MethodLength:
51
51
 
52
52
  Lint/UnusedBlockArgument:
53
53
  Enabled: false
54
+
55
+ Style/SymbolArray:
56
+ EnforcedStyle: brackets
57
+
58
+ Style/WordArray:
59
+ EnforcedStyle: brackets
60
+
61
+ Style/NumberedParametersLimit:
62
+ Max: 2
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- async-tools (0.1.10)
4
+ async-tools (0.2.2)
5
5
  async (~> 2.3)
6
6
  zeitwerk (~> 2.6)
7
7
 
data/Rakefile CHANGED
@@ -9,4 +9,4 @@ require "rubocop/rake_task"
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
12
- task default: %i[spec rubocop]
12
+ task default: [:spec, :rubocop]
@@ -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 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
- # Threading is not supported!
4
- module Async::Bus
5
- class << self
6
- def get(name = :default)
7
- @buses ||= {}
8
- @buses[name] ||= Bus.new(name:)
9
- @buses[name]
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
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Async # rubocop:disable Style/ClassAndModuleChildren
4
4
  module Tools
5
- VERSION = "0.1.10"
5
+ VERSION = "0.2.2"
6
6
  end
7
7
  end
data/lib/async/tools.rb CHANGED
@@ -12,6 +12,7 @@ loader = Zeitwerk::Loader.new
12
12
  loader.tag = File.basename(__FILE__, ".rb")
13
13
  loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
14
14
  loader.push_dir(File.expand_path("..", __dir__.to_s))
15
+
15
16
  loader.setup
16
17
 
17
18
  module Async
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.1.10
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-02 00:00:00.000000000 Z
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