async-tools 0.1.10 → 0.2.2

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: 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