autoscale-agent 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fc5fb7bd79bb436a7f36d9c95b3f27f8a1437cc269da86c275cc00b434784ed2
4
+ data.tar.gz: 69057429ec9c74443d3b23a8fbe5ef2fd7b716fe414393d4e59eef5944b533d2
5
+ SHA512:
6
+ metadata.gz: 5379183c0664bb43b528c11d229f607328a1ce99a88865f4da0d4e955751c083bed3b57884bc85b915ed902c877199c853cd112214661dcd58e6a6e86596f2a7
7
+ data.tar.gz: 493b64a3a84714e14b840a24ccb0a3197c6f6d8adf17ca4802a49cd3692ec3448216a49047df321eba5b2e9b7e9148b3702378d1290817f53be55d2ae1ccd15f
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2023-03-03
4
+
5
+ - Initial release
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2023 Michael R. van Rooijen, Autoscale.app
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Ruby Agent (Autoscale.app)
2
+
3
+ Provides [Autoscale.app] with the necessary metrics for autoscaling web and worker processes.
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your `Gemfile`:
8
+
9
+ bundle add autoscale-agent --version "~> 0"
10
+
11
+ ## Usage
12
+
13
+ This gem may be used as a stand-alone agent, or as [Rack] middleware that integrates with any Rack-based web frameworks, including [Rails], [Sinatra] and [Hanami]).
14
+
15
+ Installation instructions are provided during the autoscaler setup process on [Autoscale.app].
16
+
17
+ ## Related Packages
18
+
19
+ The following gems are currently available.
20
+
21
+ #### Queues (Worker Metric Functions)
22
+
23
+ | Worker Library | Repository |
24
+ |----------------|---------------------------------------------------------|
25
+ | Sidekiq | https://github.com/autoscale-app/ruby-queue-sidekiq |
26
+ | Delayed Job | https://github.com/autoscale-app/ruby-queue-delayed-job |
27
+ | Good Job | https://github.com/autoscale-app/ruby-queue-good_job |
28
+
29
+ Let us know if your preferred worker library isn't available and we'll see if we can add support.
30
+
31
+ ## Development
32
+
33
+ Prepare environment:
34
+
35
+ bin/setup
36
+
37
+ See Rake for relevant tasks:
38
+
39
+ bin/rake -T
40
+
41
+ ## Contributing
42
+
43
+ Bug reports and pull requests are welcome on GitHub at https://github.com/autoscale-app/ruby-agent
44
+
45
+ [Autoscale.app]: https://autoscale.app
46
+ [Agent]: https://github.com/autoscale-app/ruby-agent
47
+ [Rack]: https://github.com/rack/rack
48
+ [Rails]: https://rubyonrails.org
49
+ [Sinatra]: https://sinatrarb.com
50
+ [Hanami]: https://hanamirb.org
@@ -0,0 +1,80 @@
1
+ module Autoscale
2
+ module Agent
3
+ class Configuration
4
+ class BlockMissingError < StandardError; end
5
+
6
+ class PlatformMissingError < StandardError; end
7
+
8
+ class InvalidPlatformError < StandardError; end
9
+
10
+ class << self
11
+ attr_writer :run
12
+
13
+ def run?
14
+ !defined?(@run) || @run == true
15
+ end
16
+ end
17
+
18
+ def initialize(&block)
19
+ instance_eval(&block)
20
+
21
+ if Configuration.run?
22
+ web_dispatchers.run
23
+ worker_dispatchers.run
24
+ end
25
+ end
26
+
27
+ def platform(value = nil)
28
+ if value
29
+ @platform = validate_platform(value)
30
+ else
31
+ @platform || raise(PlatformMissingError)
32
+ end
33
+ end
34
+
35
+ def web_dispatchers
36
+ @web_dispatchers ||= WebDispatchers.new
37
+ end
38
+
39
+ def worker_dispatchers
40
+ @worker_dispatchers ||= WorkerDispatchers.new
41
+ end
42
+
43
+ def worker_servers
44
+ @worker_servers ||= WorkerServers.new
45
+ end
46
+
47
+ def dispatch(token, &block)
48
+ if block
49
+ dispatch_worker(token, &block)
50
+ else
51
+ dispatch_web(token)
52
+ end
53
+ end
54
+
55
+ def serve(token, &block)
56
+ raise BlockMissingError, "missing block" unless block
57
+ worker_servers << WorkerServer.new(token, &block)
58
+ end
59
+
60
+ private
61
+
62
+ def dispatch_web(token)
63
+ web_dispatchers.queue_time = WebDispatcher.new(token)
64
+ end
65
+
66
+ def dispatch_worker(token, &block)
67
+ worker_dispatchers << WorkerDispatcher.new(token, &block)
68
+ end
69
+
70
+ def validate_platform(value)
71
+ case value
72
+ when :render
73
+ value
74
+ else
75
+ raise InvalidPlatformError, "currently the only valid option is :render"
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autoscale
4
+ module Agent
5
+ class Middleware
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ return serve(env) if env["PATH_INFO"] == "/autoscale"
12
+ record_queue_time(env)
13
+ @app.call(env)
14
+ end
15
+
16
+ private
17
+
18
+ def serve(env)
19
+ tokens = (env["HTTP_AUTOSCALE_METRIC_TOKENS"] || "").split(",")
20
+ server = Autoscale::Agent.configuration.worker_servers.find(tokens)
21
+ return [404, {}, ["Not Found"]] unless server
22
+ headers = {
23
+ "content-type" => "application/json",
24
+ "cache-control" => "must-revalidate, private, max-age=0"
25
+ }
26
+ [200, headers, [MultiJson.dump(server.serve)]]
27
+ end
28
+
29
+ def record_queue_time(env)
30
+ return unless request_start_header(env)
31
+ return unless (dispatcher = Autoscale::Agent.configuration.web_dispatchers.queue_time)
32
+ current_time = (Time.now.to_f * 1000).to_i
33
+ request_start_time = to_ms(request_start_header(env))
34
+ elapsed_ms = current_time - request_start_time
35
+ elapsed = (elapsed_ms < 0) ? 0 : elapsed_ms
36
+ dispatcher.add(elapsed)
37
+ end
38
+
39
+ def request_start_header(env)
40
+ (env["HTTP_X_REQUEST_START"] || env["HTTP_X_QUEUE_START"]).to_i
41
+ end
42
+
43
+ def to_ms(start)
44
+ case Autoscale::Agent.configuration.platform
45
+ when :render
46
+ (start / 1000).to_i
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,9 @@
1
+ module Autoscale
2
+ module Agent
3
+ class Railtie < ::Rails::Railtie
4
+ initializer "autoscale.add_middleware" do |app|
5
+ app.config.middleware.insert 0, Autoscale::Agent::Middleware
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+ module Autoscale
2
+ module Agent
3
+ module Request
4
+ module_function
5
+
6
+ def dispatch(body, token:)
7
+ url = ENV["AUTOSCALE_METRICS_URL"] || "https://metrics.autoscale.app"
8
+
9
+ headers = {
10
+ "User-Agent" => "Autoscale Agent (Ruby)",
11
+ "Content-Type" => "application/json",
12
+ "Autoscale-Metric-Token" => token
13
+ }
14
+
15
+ post(url, body, headers)
16
+ end
17
+
18
+ def post(url, body, headers = {})
19
+ uri = URI.parse(url)
20
+ http = Net::HTTP.new(uri.host, uri.port)
21
+ http.use_ssl = uri.port == 443
22
+ http.open_timeout = 5
23
+ http.read_timeout = 5
24
+ request = Net::HTTP::Post.new(uri.request_uri)
25
+ headers.each { |key, value| request[key] = value }
26
+ request.body = body
27
+ http.request(request)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ module Autoscale
2
+ module Agent
3
+ VERSION = "0.1.0".freeze
4
+ end
5
+ end
@@ -0,0 +1,64 @@
1
+ module Autoscale
2
+ module Agent
3
+ class WebDispatcher
4
+ TTL = 30
5
+
6
+ attr_reader :token
7
+
8
+ def initialize(token)
9
+ @id = token.each_char.take(7).join
10
+ @token = token
11
+ @buffer = {}
12
+ @mutex = Mutex.new
13
+ end
14
+
15
+ def add(value, timestamp: Time.now.to_i)
16
+ @mutex.synchronize do
17
+ @buffer[timestamp] ||= 0
18
+ @buffer[timestamp] = value if value > @buffer[timestamp]
19
+ end
20
+ end
21
+
22
+ def prune
23
+ @mutex.synchronize do
24
+ max_age = Time.now.to_i - TTL
25
+ @buffer.delete_if { |timestamp, _| timestamp < max_age }
26
+ end
27
+ end
28
+
29
+ def dispatch
30
+ return unless (payload = build_payload)
31
+
32
+ body = MultiJson.dump(payload)
33
+ response = Request.dispatch(body, token: token)
34
+
35
+ unless response.is_a?(Net::HTTPOK)
36
+ revert_payload(payload)
37
+ error "Failed to dispatch (#{response.code}) #{response.body}"
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def build_payload
44
+ @mutex.synchronize do
45
+ now = Time.now.to_i
46
+ keys = @buffer.each_key.select { |key| key < now }
47
+ payload = @buffer.slice(*keys)
48
+ keys.each { |key| @buffer.delete(key) }
49
+ payload if payload.any?
50
+ end
51
+ end
52
+
53
+ def revert_payload(payload)
54
+ payload.each do |timestamp, value|
55
+ add(value, timestamp: timestamp)
56
+ end
57
+ end
58
+
59
+ def error(msg)
60
+ puts "Autoscale::Agent/WebDispatcher[#{@id}][ERROR]: #{msg}"
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,47 @@
1
+ module Autoscale
2
+ module Agent
3
+ class WebDispatchers
4
+ class AlreadySetError < StandardError
5
+ end
6
+
7
+ include Enumerable
8
+
9
+ DISPATCH_INTERVAL = 15
10
+
11
+ attr_reader :queue_time
12
+
13
+ def initialize
14
+ @dispatchers = []
15
+ end
16
+
17
+ def queue_time=(dispatcher)
18
+ raise AlreadySetError if defined?(@queue_time)
19
+ @dispatchers << (@queue_time = dispatcher)
20
+ end
21
+
22
+ def each(&block)
23
+ @dispatchers.each(&block)
24
+ end
25
+
26
+ def prune
27
+ each(&:prune)
28
+ end
29
+
30
+ def dispatch
31
+ each(&:dispatch)
32
+ rescue => err
33
+ puts "Autoscale::Agent/WebDispatcher: #{err}\n#{err.backtrace.join("\n")}"
34
+ end
35
+
36
+ def run
37
+ Thread.new do
38
+ loop do
39
+ prune
40
+ dispatch
41
+ sleep DISPATCH_INTERVAL
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,32 @@
1
+ module Autoscale
2
+ module Agent
3
+ class WorkerDispatcher
4
+ attr_reader :token
5
+
6
+ def initialize(token, &block)
7
+ @id = token.each_char.take(7).join
8
+ @token = token
9
+ @block = block
10
+ end
11
+
12
+ def dispatch
13
+ if (value = @block.call)
14
+ body = MultiJson.dump(Time.now.to_i => value)
15
+ response = Request.dispatch(body, token: @token)
16
+
17
+ unless response.is_a?(Net::HTTPOK)
18
+ error "Failed to dispatch (#{response.code}) #{response.body}"
19
+ end
20
+ else
21
+ error "Failed to calculate worker information (nil)"
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def error(msg)
28
+ puts "Autoscale::Agent/WorkerDispatcher[#{@id}][ERROR]: #{msg}"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,38 @@
1
+ module Autoscale
2
+ module Agent
3
+ class WorkerDispatchers
4
+ include Enumerable
5
+
6
+ DISPATCH_INTERVAL = 15
7
+
8
+ def initialize
9
+ @dispatchers = []
10
+ end
11
+
12
+ def each(&block)
13
+ @dispatchers.each(&block)
14
+ end
15
+
16
+ def <<(dispatcher)
17
+ @dispatchers << dispatcher
18
+ end
19
+
20
+ def dispatch
21
+ each do |dispatcher|
22
+ dispatcher.dispatch
23
+ rescue => err
24
+ puts "Autoscale::Agent/WorkerDispatcher: #{err}\n#{err.backtrace.join("\n")}"
25
+ end
26
+ end
27
+
28
+ def run
29
+ Thread.new do
30
+ loop do
31
+ dispatch
32
+ sleep DISPATCH_INTERVAL
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,16 @@
1
+ module Autoscale
2
+ module Agent
3
+ class WorkerServer
4
+ attr_reader :token
5
+
6
+ def initialize(token, &block)
7
+ @token = token
8
+ @block = block
9
+ end
10
+
11
+ def serve
12
+ @block.call
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,23 @@
1
+ module Autoscale
2
+ module Agent
3
+ class WorkerServers
4
+ include Enumerable
5
+
6
+ def initialize
7
+ @servers = []
8
+ end
9
+
10
+ def each(&block)
11
+ @servers.each(&block)
12
+ end
13
+
14
+ def <<(server)
15
+ @servers << server
16
+ end
17
+
18
+ def find(tokens)
19
+ @servers.find { |server| tokens.include?(server.token) }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ require "uri"
2
+ require "net/https"
3
+ require "multi_json"
4
+
5
+ require_relative "agent/configuration"
6
+ require_relative "agent/middleware"
7
+ require_relative "agent/request"
8
+ require_relative "agent/version"
9
+ require_relative "agent/web_dispatcher"
10
+ require_relative "agent/web_dispatchers"
11
+ require_relative "agent/worker_dispatcher"
12
+ require_relative "agent/worker_dispatchers"
13
+ require_relative "agent/worker_server"
14
+ require_relative "agent/worker_servers"
15
+
16
+ module Autoscale
17
+ module Agent
18
+ module_function
19
+
20
+ def configure(&block)
21
+ @configuration = Configuration.new(&block)
22
+ end
23
+
24
+ def configuration
25
+ @configuration
26
+ end
27
+ end
28
+ end
29
+
30
+ require_relative "agent/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1 @@
1
+ require_relative "autoscale/agent"
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: autoscale-agent
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael R. van Rooijen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-03-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: multi_json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1'
27
+ description:
28
+ email:
29
+ - support@autoscale.app
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - CHANGELOG.md
35
+ - LICENSE
36
+ - README.md
37
+ - lib/autoscale-agent.rb
38
+ - lib/autoscale/agent.rb
39
+ - lib/autoscale/agent/configuration.rb
40
+ - lib/autoscale/agent/middleware.rb
41
+ - lib/autoscale/agent/railtie.rb
42
+ - lib/autoscale/agent/request.rb
43
+ - lib/autoscale/agent/version.rb
44
+ - lib/autoscale/agent/web_dispatcher.rb
45
+ - lib/autoscale/agent/web_dispatchers.rb
46
+ - lib/autoscale/agent/worker_dispatcher.rb
47
+ - lib/autoscale/agent/worker_dispatchers.rb
48
+ - lib/autoscale/agent/worker_server.rb
49
+ - lib/autoscale/agent/worker_servers.rb
50
+ homepage: https://autoscale.app
51
+ licenses:
52
+ - MIT
53
+ metadata:
54
+ homepage_uri: https://autoscale.app
55
+ documentation_uri: https://rubydoc.info/gems/autoscale-agent
56
+ source_code_uri: https://github.com/autoscale-app/ruby-agent
57
+ changelog_uri: https://github.com/autoscale-app/ruby-agent/blob/master/CHANGELOG.md
58
+ bug_tracker_uri: https://github.com/autoscale-app/ruby-agent/issues
59
+ rubygems_mfa_required: 'true'
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.7.0
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.4.6
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: Provides Autoscale.app with the necessary metrics for autoscaling web and
79
+ worker processes
80
+ test_files: []