aikido-zen 0.1.0.alpha4-arm64-darwin → 0.1.1-arm64-darwin
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.simplecov +19 -0
- data/CHANGELOG.md +16 -0
- data/README.md +136 -23
- data/Rakefile +4 -0
- data/benchmarks/README.md +27 -0
- data/benchmarks/rails7.1_sql_injection.js +74 -0
- data/docs/banner.svg +203 -0
- data/docs/config.md +123 -0
- data/docs/rails.md +70 -0
- data/lib/aikido/zen/actor.rb +1 -1
- data/lib/aikido/zen/agent/heartbeats_manager.rb +66 -0
- data/lib/aikido/zen/agent.rb +100 -112
- data/lib/aikido/zen/collector/hosts.rb +15 -0
- data/lib/aikido/zen/collector/routes.rb +64 -0
- data/lib/aikido/zen/{stats → collector}/sink_stats.rb +1 -1
- data/lib/aikido/zen/collector/stats.rb +111 -0
- data/lib/aikido/zen/{stats → collector}/users.rb +6 -2
- data/lib/aikido/zen/collector.rb +117 -0
- data/lib/aikido/zen/config.rb +17 -11
- data/lib/aikido/zen/context.rb +8 -1
- data/lib/aikido/zen/errors.rb +3 -1
- data/lib/aikido/zen/event.rb +7 -4
- data/lib/aikido/zen/internals.rb +4 -0
- data/lib/aikido/zen/{libzen-v0.1.26.aarch64.dylib → libzen-v0.1.31.aarch64.dylib} +0 -0
- data/lib/aikido/zen/middleware/set_context.rb +4 -1
- data/lib/aikido/zen/rails_engine.rb +27 -18
- data/lib/aikido/zen/request/schema/builder.rb +0 -2
- data/lib/aikido/zen/request.rb +6 -0
- data/lib/aikido/zen/runtime_settings.rb +6 -11
- data/lib/aikido/zen/scanners/ssrf_scanner.rb +12 -6
- data/lib/aikido/zen/sinks/action_controller.rb +64 -0
- data/lib/aikido/zen/sinks/http.rb +1 -1
- data/lib/aikido/zen/sinks/pg.rb +13 -12
- data/lib/aikido/zen/sinks/typhoeus.rb +1 -1
- data/lib/aikido/zen/sinks.rb +1 -0
- data/lib/aikido/zen/version.rb +2 -2
- data/lib/aikido/zen/worker.rb +82 -0
- data/lib/aikido/zen.rb +55 -50
- data/tasklib/bench.rake +70 -0
- metadata +20 -9
- data/CODE_OF_CONDUCT.md +0 -132
- data/lib/aikido/zen/stats/routes.rb +0 -53
- data/lib/aikido/zen/stats.rb +0 -171
data/lib/aikido/zen/config.rb
CHANGED
@@ -8,6 +8,13 @@ require_relative "context"
|
|
8
8
|
|
9
9
|
module Aikido::Zen
|
10
10
|
class Config
|
11
|
+
# @return [Boolean] whether Aikido should be turned completely off (no
|
12
|
+
# intercepting calls to protect the app, no agent process running, no
|
13
|
+
# middleware installed). Defaults to false (so, enabled). Can be set
|
14
|
+
# via the AIKIDO_DISABLED environment variable.
|
15
|
+
attr_accessor :disabled
|
16
|
+
alias_method :disabled?, :disabled
|
17
|
+
|
11
18
|
# @return [Boolean] whether Aikido should only report infractions or block
|
12
19
|
# the request by raising an Exception. Defaults to whether AIKIDO_BLOCKING
|
13
20
|
# is set to a non-empty value in your environment, or +false+ otherwise.
|
@@ -45,7 +52,7 @@ module Aikido::Zen
|
|
45
52
|
# into an Object. Defaults to the standard library's JSON.parse method.
|
46
53
|
attr_accessor :json_decoder
|
47
54
|
|
48
|
-
# @
|
55
|
+
# @return [Logger]
|
49
56
|
attr_accessor :logger
|
50
57
|
|
51
58
|
# @return [Integer] maximum number of timing measurements to keep in memory
|
@@ -83,11 +90,9 @@ module Aikido::Zen
|
|
83
90
|
# differentiate different clients. By default this uses the request IP.
|
84
91
|
attr_accessor :rate_limiting_discriminator
|
85
92
|
|
86
|
-
# @return [
|
87
|
-
#
|
88
|
-
|
89
|
-
attr_accessor :api_schema_collection_enabled
|
90
|
-
alias_method :api_schema_collection_enabled?, :api_schema_collection_enabled
|
93
|
+
# @return [Integer] max number of requests we sample per endpoint when
|
94
|
+
# computing the schema.
|
95
|
+
attr_accessor :api_schema_max_samples
|
91
96
|
|
92
97
|
# @api private
|
93
98
|
# @return [Integer] max number of levels deep we want to read a nested
|
@@ -125,7 +130,8 @@ module Aikido::Zen
|
|
125
130
|
attr_accessor :imds_allowed_hosts
|
126
131
|
|
127
132
|
def initialize
|
128
|
-
self.
|
133
|
+
self.disabled = read_boolean_from_env(ENV.fetch("AIKIDO_DISABLED", false))
|
134
|
+
self.blocking_mode = read_boolean_from_env(ENV.fetch("AIKIDO_BLOCKING", false))
|
129
135
|
self.api_timeouts = 10
|
130
136
|
self.api_base_url = ENV.fetch("AIKIDO_BASE_URL", DEFAULT_API_BASE_URL)
|
131
137
|
self.runtime_api_base_url = ENV.fetch("AIKIDO_RUNTIME_URL", DEFAULT_RUNTIME_BASE_URL)
|
@@ -146,11 +152,9 @@ module Aikido::Zen
|
|
146
152
|
self.server_rate_limit_deadline = 1800 # 30 min
|
147
153
|
self.client_rate_limit_period = 3600 # 1 hour
|
148
154
|
self.client_rate_limit_max_events = 100
|
149
|
-
|
150
|
-
self.api_schema_collection_enabled = read_boolean_from_env(ENV.fetch("AIKIDO_FEATURE_COLLECT_API_SCHEMA", false))
|
155
|
+
self.api_schema_max_samples = Integer(ENV.fetch("AIKIDO_MAX_API_DISCOVERY_SAMPLES", 10))
|
151
156
|
self.api_schema_collection_max_depth = 20
|
152
157
|
self.api_schema_collection_max_properties = 20
|
153
|
-
|
154
158
|
self.imds_allowed_hosts = ["metadata.google.internal", "metadata.goog"]
|
155
159
|
end
|
156
160
|
|
@@ -224,6 +228,8 @@ module Aikido::Zen
|
|
224
228
|
end
|
225
229
|
|
226
230
|
# @!visibility private
|
227
|
-
DEFAULT_RATE_LIMITING_DISCRIMINATOR = ->(request) {
|
231
|
+
DEFAULT_RATE_LIMITING_DISCRIMINATOR = ->(request) {
|
232
|
+
request.actor ? "actor:#{request.actor.id}" : request.ip
|
233
|
+
}
|
228
234
|
end
|
229
235
|
end
|
data/lib/aikido/zen/context.rb
CHANGED
@@ -7,13 +7,20 @@ require_relative "payload"
|
|
7
7
|
|
8
8
|
module Aikido::Zen
|
9
9
|
class Context
|
10
|
+
# Build a Context object for the current HTTP request based on the currently
|
11
|
+
# configured request builder.
|
12
|
+
#
|
13
|
+
# @param env [Hash] the Rack env hash.
|
14
|
+
# @param config [Aikido::Zen::Config]
|
15
|
+
# @return [Aikido::Zen::Context]
|
10
16
|
def self.from_rack_env(env, config = Aikido::Zen.config)
|
11
17
|
config.request_builder.call(env)
|
12
18
|
end
|
13
19
|
|
20
|
+
# @return [Aikido::Zen::Request]
|
14
21
|
attr_reader :request
|
15
22
|
|
16
|
-
# @param [Rack::Request] a Request object that implements the
|
23
|
+
# @param request [Rack::Request] a Request object that implements the
|
17
24
|
# Rack::Request API, to which we will delegate behavior.
|
18
25
|
# @param settings [Aikido::Zen::RuntimeSettings]
|
19
26
|
#
|
data/lib/aikido/zen/errors.rb
CHANGED
@@ -3,11 +3,13 @@
|
|
3
3
|
require "forwardable"
|
4
4
|
|
5
5
|
module Aikido
|
6
|
+
# @!visibility private
|
6
7
|
# Support rescuing Aikido::Error without forcing a single base class to all
|
7
8
|
# errors (so things that should be e.g. a TypeError, can have the correct
|
8
9
|
# superclass).
|
9
|
-
module Error; end
|
10
|
+
module Error; end # :nodoc:
|
10
11
|
|
12
|
+
# @!visibility private
|
11
13
|
# Generic error for problems with the Agent.
|
12
14
|
class ZenError < RuntimeError
|
13
15
|
include Error
|
data/lib/aikido/zen/event.rb
CHANGED
@@ -48,17 +48,20 @@ module Aikido::Zen
|
|
48
48
|
end
|
49
49
|
|
50
50
|
class Heartbeat < Event
|
51
|
-
def initialize(stats:, **opts)
|
51
|
+
def initialize(stats:, users:, hosts:, routes:, **opts)
|
52
52
|
super(type: "heartbeat", **opts)
|
53
53
|
@stats = stats
|
54
|
+
@users = users
|
55
|
+
@hosts = hosts
|
56
|
+
@routes = routes
|
54
57
|
end
|
55
58
|
|
56
59
|
def as_json
|
57
60
|
super.update(
|
58
61
|
stats: @stats.as_json,
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
+
users: @users.as_json,
|
63
|
+
routes: @routes.as_json,
|
64
|
+
hostnames: @hosts.as_json
|
62
65
|
)
|
63
66
|
end
|
64
67
|
end
|
data/lib/aikido/zen/internals.rb
CHANGED
@@ -31,6 +31,8 @@ module Aikido::Zen
|
|
31
31
|
attach_function :detect_sql_injection_native, :detect_sql_injection,
|
32
32
|
[:string, :string, :int], :int
|
33
33
|
rescue LoadError, FFI::NotFoundError => err
|
34
|
+
# :nocov:
|
35
|
+
|
34
36
|
# Emit an $stderr warning at startup.
|
35
37
|
warn "Zen could not load its binary extension #{libzen_name}: #{err}"
|
36
38
|
|
@@ -38,6 +40,8 @@ module Aikido::Zen
|
|
38
40
|
attempt = format("%p for SQL injection", query)
|
39
41
|
raise InternalsError.new(attempt, "loading", Internals.libzen_name)
|
40
42
|
end
|
43
|
+
|
44
|
+
# :nocov:
|
41
45
|
else
|
42
46
|
# Analyzes the SQL query to detect if the provided user input is being
|
43
47
|
# passed as-is without escaping.
|
Binary file
|
@@ -3,6 +3,9 @@
|
|
3
3
|
require_relative "../context"
|
4
4
|
|
5
5
|
module Aikido::Zen
|
6
|
+
# @!visibility private
|
7
|
+
ENV_KEY = "aikido.context"
|
8
|
+
|
6
9
|
module Middleware
|
7
10
|
# Rack middleware that keeps the current context in a Thread/Fiber-local
|
8
11
|
# variable so that other parts of the agent/firewall can access it.
|
@@ -14,7 +17,7 @@ module Aikido::Zen
|
|
14
17
|
def call(env)
|
15
18
|
context = Context.from_rack_env(env)
|
16
19
|
|
17
|
-
Aikido::Zen.current_context = context
|
20
|
+
Aikido::Zen.current_context = env[ENV_KEY] = context
|
18
21
|
Aikido::Zen.track_request(context.request)
|
19
22
|
|
20
23
|
@app.call(env)
|
@@ -5,44 +5,53 @@ require "action_dispatch"
|
|
5
5
|
module Aikido::Zen
|
6
6
|
class RailsEngine < ::Rails::Engine
|
7
7
|
config.before_configuration do
|
8
|
-
# Access library configuration at `Rails.application.config.
|
9
|
-
config.
|
8
|
+
# Access library configuration at `Rails.application.config.zen`.
|
9
|
+
config.zen = Aikido::Zen.config
|
10
10
|
end
|
11
11
|
|
12
12
|
initializer "aikido.add_middleware" do |app|
|
13
|
+
next if config.zen.disabled?
|
14
|
+
|
13
15
|
app.middleware.use Aikido::Zen::Middleware::SetContext
|
14
16
|
app.middleware.use Aikido::Zen::Middleware::CheckAllowedAddresses
|
15
|
-
|
16
|
-
|
17
|
-
# Due to how Rails sets up its middleware chain, the routing is evaluated
|
18
|
-
# (and the Request object constructed) in the app that terminates the
|
19
|
-
# chain, so no amount of middleware will be able to access it.
|
20
|
-
#
|
21
|
-
# This way, we overwrite the Request object as early as we can in the
|
22
|
-
# request handling, so that by the time we start evaluating inputs, we
|
23
|
-
# have assigned the request correctly.
|
17
|
+
|
24
18
|
ActiveSupport.on_load(:action_controller) do
|
19
|
+
# Due to how Rails sets up its middleware chain, the routing is evaluated
|
20
|
+
# (and the Request object constructed) in the app that terminates the
|
21
|
+
# chain, so no amount of middleware will be able to access it.
|
22
|
+
#
|
23
|
+
# This way, we overwrite the Request object as early as we can in the
|
24
|
+
# request handling, so that by the time we start evaluating inputs, we
|
25
|
+
# have assigned the request correctly.
|
25
26
|
before_action { Aikido::Zen.current_context.update_request(request) }
|
26
27
|
end
|
27
28
|
end
|
28
29
|
|
29
30
|
initializer "aikido.configuration" do |app|
|
30
|
-
|
31
|
-
|
31
|
+
# Allow the logger to be configured before checking if disabled? so we can
|
32
|
+
# let the user know that the agent is disabled.
|
33
|
+
app.config.zen.logger = ::Rails.logger.tagged("aikido")
|
34
|
+
|
35
|
+
next if config.zen.disabled?
|
36
|
+
|
37
|
+
app.config.zen.request_builder = Aikido::Zen::Context::RAILS_REQUEST_BUILDER
|
32
38
|
|
33
39
|
# Plug Rails' JSON encoder/decoder, but only if the user hasn't changed
|
34
40
|
# them for something else.
|
35
|
-
if app.config.
|
36
|
-
app.config.
|
41
|
+
if app.config.zen.json_encoder == Aikido::Zen::Config::DEFAULT_JSON_ENCODER
|
42
|
+
app.config.zen.json_encoder = ActiveSupport::JSON.method(:encode)
|
37
43
|
end
|
38
44
|
|
39
|
-
if app.config.
|
40
|
-
app.config.
|
45
|
+
if app.config.zen.json_decoder == Aikido::Zen::Config::DEFAULT_JSON_DECODER
|
46
|
+
app.config.zen.json_decoder = ActiveSupport::JSON.method(:decode)
|
41
47
|
end
|
42
48
|
end
|
43
49
|
|
44
50
|
config.after_initialize do
|
45
|
-
|
51
|
+
if config.zen.disabled?
|
52
|
+
config.zen.logger.warn("Zen has been disabled and will not run.")
|
53
|
+
next
|
54
|
+
end
|
46
55
|
|
47
56
|
# Make sure this is run at the end of the initialization process, so
|
48
57
|
# that any gems required after aikido-zen are detected and patched
|
data/lib/aikido/zen/request.rb
CHANGED
@@ -11,6 +11,12 @@ module Aikido::Zen
|
|
11
11
|
# @return [Aikido::Zen::Router]
|
12
12
|
attr_reader :router
|
13
13
|
|
14
|
+
# The current user, if set by the host app.
|
15
|
+
#
|
16
|
+
# @return [Aikido::Zen::Actor, nil]
|
17
|
+
# @see Aikido::Zen.track_user
|
18
|
+
attr_accessor :actor
|
19
|
+
|
14
20
|
def initialize(delegate, framework:, router:)
|
15
21
|
super(delegate)
|
16
22
|
@framework = framework
|
@@ -11,16 +11,11 @@ module Aikido::Zen
|
|
11
11
|
#
|
12
12
|
# You can subscribe to changes with +#add_observer(object, func_name)+, which
|
13
13
|
# will call the function passing the settings as an argument.
|
14
|
-
|
15
|
-
:updated_at, :heartbeat_interval, :endpoints, :blocked_user_ids, :skip_protection_for_ips, :received_any_stats
|
16
|
-
)
|
17
|
-
include Concurrent::Concern::Observable
|
18
|
-
|
14
|
+
RuntimeSettings = Struct.new(:updated_at, :heartbeat_interval, :endpoints, :blocked_user_ids, :skip_protection_for_ips, :received_any_stats) do
|
19
15
|
def initialize(*)
|
20
|
-
self.observers = Concurrent::Collection::CopyOnWriteObserverSet.new
|
21
16
|
super
|
22
|
-
self.endpoints ||= Endpoints.new
|
23
|
-
self.skip_protection_for_ips ||= IPSet.new
|
17
|
+
self.endpoints ||= RuntimeSettings::Endpoints.new
|
18
|
+
self.skip_protection_for_ips ||= RuntimeSettings::IPSet.new
|
24
19
|
end
|
25
20
|
|
26
21
|
# @!attribute [rw] updated_at
|
@@ -56,12 +51,12 @@ module Aikido::Zen
|
|
56
51
|
|
57
52
|
self.updated_at = Time.at(data["configUpdatedAt"].to_i / 1000)
|
58
53
|
self.heartbeat_interval = (data["heartbeatIntervalInMS"].to_i / 1000)
|
59
|
-
self.endpoints = Endpoints.from_json(data["endpoints"])
|
54
|
+
self.endpoints = RuntimeSettings::Endpoints.from_json(data["endpoints"])
|
60
55
|
self.blocked_user_ids = data["blockedUserIds"]
|
61
|
-
self.skip_protection_for_ips = IPSet.from_json(data["allowedIPAddresses"])
|
56
|
+
self.skip_protection_for_ips = RuntimeSettings::IPSet.from_json(data["allowedIPAddresses"])
|
62
57
|
self.received_any_stats = data["receivedAnyStats"]
|
63
58
|
|
64
|
-
|
59
|
+
Aikido::Zen.agent.updated_settings! if updated_at != last_updated_at
|
65
60
|
end
|
66
61
|
end
|
67
62
|
end
|
@@ -112,7 +112,8 @@ module Aikido::Zen
|
|
112
112
|
is_port_relevant = input_uri.port != input_uri.default_port
|
113
113
|
return false if is_port_relevant && input_uri.port != conn_uri.port
|
114
114
|
|
115
|
-
conn_uri.hostname == input_uri.hostname
|
115
|
+
conn_uri.hostname == input_uri.hostname &&
|
116
|
+
conn_uri.port == input_uri.port
|
116
117
|
end
|
117
118
|
|
118
119
|
def private_ip?(hostname)
|
@@ -128,8 +129,11 @@ module Aikido::Zen
|
|
128
129
|
# * The input itself, if it already looks like a URI.
|
129
130
|
# * The input prefixed with http://
|
130
131
|
# * The input prefixed with https://
|
132
|
+
# * The input prefixed with the scheme of the request's URI, to consider
|
133
|
+
# things like an FTP request (to "ftp://localhost") with a plain host
|
134
|
+
# as a user-input ("localhost").
|
131
135
|
#
|
132
|
-
# @return [
|
136
|
+
# @return [Array<URI>] a list of unique URIs based on the above criteria.
|
133
137
|
def uris_from_input
|
134
138
|
input = @input.to_s
|
135
139
|
|
@@ -138,10 +142,12 @@ module Aikido::Zen
|
|
138
142
|
# valid hostname. We should do the same for the input.
|
139
143
|
input = format("[%s]", input) if unescaped_ipv6?(input)
|
140
144
|
|
141
|
-
[
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
+
[
|
146
|
+
input,
|
147
|
+
"http://#{input}",
|
148
|
+
"https://#{input}",
|
149
|
+
"#{@request_uri.scheme}://#{input}"
|
150
|
+
].map { |candidate| as_uri(candidate) }.compact.uniq
|
145
151
|
end
|
146
152
|
|
147
153
|
def as_uri(string)
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aikido::Zen
|
4
|
+
module Sinks
|
5
|
+
module ActionController
|
6
|
+
# Implements the "middleware" for rate limiting in Rails apps, where we
|
7
|
+
# need to check at the end of the `before_action` chain, rather than in
|
8
|
+
# an actual Rack middleware, to allow for calls to Zen.track_user being
|
9
|
+
# made from before_actions in the host app, thus allowing rate-limiting
|
10
|
+
# by user ID rather than solely by IP.
|
11
|
+
class Throttler
|
12
|
+
def initialize(
|
13
|
+
config: Aikido::Zen.config,
|
14
|
+
settings: Aikido::Zen.runtime_settings,
|
15
|
+
rate_limiter: Aikido::Zen::RateLimiter.new
|
16
|
+
)
|
17
|
+
@config = config
|
18
|
+
@settings = settings
|
19
|
+
@rate_limiter = rate_limiter
|
20
|
+
end
|
21
|
+
|
22
|
+
def throttle(controller)
|
23
|
+
context = controller.request.env[Aikido::Zen::ENV_KEY]
|
24
|
+
request = context.request
|
25
|
+
|
26
|
+
if should_throttle?(request)
|
27
|
+
status, headers, body = @config.rate_limited_responder.call(request)
|
28
|
+
controller.headers.update(headers)
|
29
|
+
controller.render plain: Array(body).join, status: status
|
30
|
+
|
31
|
+
return true
|
32
|
+
end
|
33
|
+
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
private def should_throttle?(request)
|
38
|
+
return false if @settings.skip_protection_for_ips.include?(request.ip)
|
39
|
+
|
40
|
+
@rate_limiter.throttle?(request)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.throttler
|
45
|
+
@throttler ||= Aikido::Zen::Sinks::ActionController::Throttler.new
|
46
|
+
end
|
47
|
+
|
48
|
+
module Extensions
|
49
|
+
def run_callbacks(kind, *)
|
50
|
+
return super unless kind == :process_action
|
51
|
+
|
52
|
+
super do
|
53
|
+
rate_limiter = Aikido::Zen::Sinks::ActionController.throttler
|
54
|
+
throttled = rate_limiter.throttle(self)
|
55
|
+
|
56
|
+
yield if block_given? && !throttled
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
::AbstractController::Callbacks.prepend(Aikido::Zen::Sinks::ActionController::Extensions)
|
data/lib/aikido/zen/sinks/pg.rb
CHANGED
@@ -7,6 +7,17 @@ module Aikido::Zen
|
|
7
7
|
module PG
|
8
8
|
SINK = Sinks.add("pg", scanners: [Scanners::SQLInjectionScanner])
|
9
9
|
|
10
|
+
# For some reason, the ActiveRecord pg adapter does not wrap exceptions in
|
11
|
+
# StatementInvalid, which leads to inconsistent handling. This guarantees
|
12
|
+
# that all Zen errors are wrapped in a StatementInvalid, so documentation
|
13
|
+
# can be consistent.
|
14
|
+
WRAP_EXCEPTIONS = if defined?(ActiveRecord::StatementInvalid)
|
15
|
+
<<~RUBY
|
16
|
+
rescue Aikido::Zen::SQLInjectionError
|
17
|
+
raise ActiveRecord::StatementInvalid
|
18
|
+
RUBY
|
19
|
+
end
|
20
|
+
|
10
21
|
module Extensions
|
11
22
|
%i[
|
12
23
|
send_query exec sync_exec async_exec
|
@@ -16,12 +27,7 @@ module Aikido::Zen
|
|
16
27
|
def #{method}(query, *)
|
17
28
|
SINK.scan(query: query, dialect: :postgresql, operation: :#{method})
|
18
29
|
super
|
19
|
-
|
20
|
-
# The pg adapter does not wrap exceptions in StatementInvalid, which
|
21
|
-
# leads to inconsistent handling. This guarantees that all Aikido
|
22
|
-
# errors are wrapped in a StatementInvalid, so documentation can be
|
23
|
-
# consistent.
|
24
|
-
raise ActiveRecord::StatementInvalid
|
30
|
+
#{WRAP_EXCEPTIONS}
|
25
31
|
end
|
26
32
|
RUBY
|
27
33
|
end
|
@@ -33,12 +39,7 @@ module Aikido::Zen
|
|
33
39
|
def #{method}(_, query, *)
|
34
40
|
SINK.scan(query: query, dialect: :postgresql, operation: :#{method})
|
35
41
|
super
|
36
|
-
|
37
|
-
# The pg adapter does not wrap exceptions in StatementInvalid, which
|
38
|
-
# leads to inconsistent handling. This guarantees that all Aikido
|
39
|
-
# errors are wrapped in a StatementInvalid, so documentation can be
|
40
|
-
# consistent.
|
41
|
-
raise ActiveRecord::StatementInvalid
|
42
|
+
#{WRAP_EXCEPTIONS}
|
42
43
|
end
|
43
44
|
RUBY
|
44
45
|
end
|
data/lib/aikido/zen/sinks.rb
CHANGED
@@ -4,6 +4,7 @@ require_relative "sink"
|
|
4
4
|
|
5
5
|
require_relative "sinks/socket"
|
6
6
|
|
7
|
+
require_relative "sinks/action_controller" if defined?(::ActionController)
|
7
8
|
require_relative "sinks/resolv" if defined?(::Resolv)
|
8
9
|
require_relative "sinks/net_http" if defined?(::Net::HTTP)
|
9
10
|
require_relative "sinks/http" if defined?(::HTTP)
|
data/lib/aikido/zen/version.rb
CHANGED
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent"
|
4
|
+
|
5
|
+
module Aikido::Zen
|
6
|
+
# @api private
|
7
|
+
#
|
8
|
+
# The worker manages the background thread in which Zen communicates with the
|
9
|
+
# Aikido server.
|
10
|
+
class Worker
|
11
|
+
# @return [Concurrent::ExecutorService]
|
12
|
+
attr_reader :executor
|
13
|
+
|
14
|
+
# @!visibility private
|
15
|
+
attr_reader :timers, :deferrals
|
16
|
+
|
17
|
+
def initialize(config: Aikido::Zen.config)
|
18
|
+
@config = config
|
19
|
+
@timers = []
|
20
|
+
@deferrals = []
|
21
|
+
@executor = Concurrent::SingleThreadExecutor.new
|
22
|
+
end
|
23
|
+
|
24
|
+
# Queue a block to be run asynchronously in the background thread.
|
25
|
+
#
|
26
|
+
# @return [void]
|
27
|
+
def perform(&block)
|
28
|
+
executor.post do
|
29
|
+
yield
|
30
|
+
rescue Exception => err # rubocop:disable Lint/RescueException
|
31
|
+
@config.logger.error "Error in background worker: #{err.inspect}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Queue a block to be run asynchronously after a delay.
|
36
|
+
#
|
37
|
+
# @param interval [Integer] amount of seconds to wait.
|
38
|
+
# @return [void]
|
39
|
+
def delay(interval, &task)
|
40
|
+
Concurrent::ScheduledTask
|
41
|
+
.execute(interval, executor: executor) { perform(&task) }
|
42
|
+
.tap { |deferral| @deferrals << deferral }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Queue a block to run repeatedly on a timer on the background thread. The
|
46
|
+
# timer will consider how long the block takes to run to schedule the next
|
47
|
+
# run. For example, if you schedule a block to run every 10 seconds, and the
|
48
|
+
# block itself takes 2 seconds, the second iteration will be run 8 seconds
|
49
|
+
# after the first one.
|
50
|
+
#
|
51
|
+
# If the block takes longer than the given interval, the second iteration
|
52
|
+
# will be run immediately.
|
53
|
+
#
|
54
|
+
# @param interval [Integer] amount of seconds to wait between runs.
|
55
|
+
# @param run_now [Boolean] whether to run the block immediately, or wait for
|
56
|
+
# +interval+ seconds before the first run. Defaults to +true+.
|
57
|
+
# @return [void]
|
58
|
+
def every(interval, run_now: true, &task)
|
59
|
+
Concurrent::TimerTask
|
60
|
+
.execute(
|
61
|
+
run_now: run_now,
|
62
|
+
executor: executor,
|
63
|
+
interval_type: :fixed_rate,
|
64
|
+
execution_interval: interval
|
65
|
+
) {
|
66
|
+
perform(&task)
|
67
|
+
}
|
68
|
+
.tap { |timer| @timers << timer }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Safely clean up and kill the thread, giving time to kill any ongoing tasks
|
72
|
+
# on the queue.
|
73
|
+
#
|
74
|
+
# @return [void]
|
75
|
+
def shutdown
|
76
|
+
@deferrals.each { |task| task.cancel if task.pending? }
|
77
|
+
@timers.each { |task| task.shutdown }
|
78
|
+
@executor.shutdown
|
79
|
+
@executor.wait_for_termination(30)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|