Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Proof of Concept] Make Semian work with activerecord-trilogy-adapter #435

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c4e490a
Install adapter gem, bump to Rails edge
adrianna-chang-shopify Oct 25, 2022
bbc7259
Basis for integrating Semian at Trilogy adapter layer
adrianna-chang-shopify Oct 26, 2022
80b7fb7
Open circuit on connection errors for TrilogyAdapter
adrianna-chang-shopify Oct 26, 2022
18070d0
Ensure we only rescue timeout errors for now
adrianna-chang-shopify Oct 26, 2022
b5d39ab
Namespace test
adrianna-chang-shopify Oct 26, 2022
c850713
Read timeout errors test
adrianna-chang-shopify Oct 26, 2022
6762924
Tests for instrumentation and resource identifier tagging
adrianna-chang-shopify Oct 26, 2022
b72e9e8
Tests for resource acquisition
adrianna-chang-shopify Oct 27, 2022
ec4846b
Test resource timeout and circuit breaker work
adrianna-chang-shopify Oct 28, 2022
10c80b8
Tests for query allowlist
adrianna-chang-shopify Oct 28, 2022
26d3fbc
Test unconfigured adapter
adrianna-chang-shopify Oct 31, 2022
6110bd5
Half open circuit timeout
adrianna-chang-shopify Oct 31, 2022
5ef74b2
Test to ensure circuit open errors won't trigger circuit breaker
adrianna-chang-shopify Oct 31, 2022
2c6fcfa
Note on rescuing ActiveRecord::StatementInvalid from acquire_semian_r…
adrianna-chang-shopify Oct 31, 2022
db32d28
Some small fixes
adrianna-chang-shopify Nov 1, 2022
7dba193
TrilogyAdapter#execute should take allow_retry option
adrianna-chang-shopify Nov 8, 2022
f9f0294
Update to new error class branches
adrianna-chang-shopify Jan 26, 2023
a30429d
Changes to Trilogy adapter for new error classes
adrianna-chang-shopify Jan 26, 2023
eca4b87
WIP -- generic AR adapter
adrianna-chang-shopify Jan 26, 2023
c42cd82
Revert back to modifying config for half open resource timeout
adrianna-chang-shopify Jan 27, 2023
7472390
SemianError should inherit from StatementInvalid
adrianna-chang-shopify Jan 27, 2023
3b43ec5
Ensure active? is circuit broken
adrianna-chang-shopify Jan 30, 2023
95e37a5
Linter
adrianna-chang-shopify Jan 30, 2023
2b1d8ce
Rescue ResourceBusy and CircuitOpen errors stemming from TrilogyAdapt…
adrianna-chang-shopify Jan 30, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ group :test do
# The last stable version for MacOS ARM darwin
gem "grpc", "1.47.0"
gem "mysql2", "~> 0.5"
gem "activerecord", ">= 7.0.3"
gem "trilogy", github: "https://github.com/github/trilogy/pull/41", glob: "contrib/ruby/*.gemspec"
gem "activerecord-trilogy-adapter", github: "https://github.com/github/activerecord-trilogy-adapter/pull/24"
gem "activerecord", github: "rails/rails", branch: "main"
gem "hiredis", "~> 0.6"
# NOTE: v0.12.0 required for ruby 3.2.0. https://github.com/redis-rb/redis-client/issues/58
gem "hiredis-client", ">= 0.12.0"
Expand Down
56 changes: 45 additions & 11 deletions Gemfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

118 changes: 118 additions & 0 deletions lib/semian/active_record_adapter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# frozen_string_literal: true

require "semian/adapter"
require "active_record/connection_adapters/abstract_adapter"

module ActiveRecord
module ConnectionAdapters
class ActiveRecordAdapter
ActiveRecord::ActiveRecordError.include(::Semian::AdapterError)

class SemianError < ActiveRecordError
def initialize(semian_identifier, *args)
super(*args)
@semian_identifier = semian_identifier
end
end

ResourceBusyError = Class.new(SemianError)
CircuitOpenError = Class.new(SemianError)
end
end
end

module Semian
module ActiveRecordAdapter
include Semian::Adapter

attr_reader :raw_semian_options, :semian_identifier

def initialize(*options)
*, config = options
@read_timeout = config[:read_timeout] || 0
@raw_semian_options = config.delete(:semian)
@semian_identifier = begin
name = semian_options && semian_options[:name]
unless name
host = config[:host] || "localhost"
port = config[:port] || 3306
name = "#{host}:#{port}"
end
:"activerecord_adapter_#{name}"
end
super
end

def execute(sql, name = nil, async: false, allow_retry: false)
if query_allowlisted?(sql)
super(sql, name, async: async, allow_retry: allow_retry)
else
acquire_semian_resource(adapter: :activerecord_adapter, scope: :execute) do
super(sql, name, async: async, allow_retry: allow_retry)
end
end
end

def with_resource_timeout(temp_timeout)
connection_was_nil = false

if connection.nil?
prev_read_timeout = @read_timeout # Use read_timeout from when we first connected
connection_was_nil = true
else
prev_read_timeout = connection.read_timeout
connection.read_timeout = temp_timeout
end

yield

connection.read_timeout = temp_timeout if connection_was_nil
ensure
connection&.read_timeout = prev_read_timeout
end

private

# AR adapter translates some of the raw connection errors, so we need special handling for the different adapters
def acquire_semian_resource(**)
super
rescue ActiveRecord::StatementInvalid => error
# if error.cause.is_a?(Trilogy::TimeoutError)
# semian_resource.mark_failed(error)
# error.semian_identifier = semian_identifier
# end
raise
end

def resource_exceptions
[ActiveRecord::ConnectionNotEstablished]
end

# TODO: share this with Mysql2
QUERY_ALLOWLIST = Regexp.union(
%r{\A(?:/\*.*?\*/)?\s*ROLLBACK}i,
%r{\A(?:/\*.*?\*/)?\s*COMMIT}i,
%r{\A(?:/\*.*?\*/)?\s*RELEASE\s+SAVEPOINT}i,
)

def query_allowlisted?(sql, *)
QUERY_ALLOWLIST.match?(sql)
rescue ArgumentError
return false unless sql.valid_encoding?

raise
end

def connect(*args)
acquire_semian_resource(adapter: :activerecord_adapter, scope: :connection) do
super
end
end

def exceptions_to_handle
[Trilogy::TimeoutError]
end
end
end

ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(Semian::TrilogyAdapter)
121 changes: 121 additions & 0 deletions lib/semian/trilogy_adapter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# frozen_string_literal: true

require "semian/adapter"
require "activerecord-trilogy-adapter"
require "active_record/connection_adapters/trilogy_adapter"

module ActiveRecord
module ConnectionAdapters
class TrilogyAdapter
ActiveRecord::ActiveRecordError.include(::Semian::AdapterError)

# Ensure we can rescue ResourceBusyError and CircuitOpenError as ActiveRecord::StatementInvalid
class SemianError < StatementInvalid
adrianna-chang-shopify marked this conversation as resolved.
Show resolved Hide resolved
def initialize(semian_identifier, *args)
super(*args)
@semian_identifier = semian_identifier
end
end

ResourceBusyError = Class.new(SemianError)
CircuitOpenError = Class.new(SemianError)
end
end
end

module Semian
module TrilogyAdapter
include Semian::Adapter

ResourceBusyError = ::ActiveRecord::ConnectionAdapters::TrilogyAdapter::ResourceBusyError
CircuitOpenError = ::ActiveRecord::ConnectionAdapters::TrilogyAdapter::CircuitOpenError

attr_reader :raw_semian_options, :semian_identifier

def initialize(*options)
*, config = options
@raw_semian_options = config.delete(:semian)
@semian_identifier = begin
name = semian_options && semian_options[:name]
unless name
host = config[:host] || "localhost"
port = config[:port] || 3306
name = "#{host}:#{port}"
end
:"trilogy_adapter_#{name}"
end
super
end

def execute(sql, name = nil, async: false, allow_retry: false)
if query_allowlisted?(sql)
super(sql, name, async: async, allow_retry: allow_retry)
else
acquire_semian_resource(adapter: :trilogy_adapter, scope: :execute) do
super(sql, name, async: async, allow_retry: allow_retry)
end
end
end

def active?
acquire_semian_resource(adapter: :trilogy_adapter, scope: :ping) do
super
end
rescue ResourceBusyError, CircuitOpenError => error
false
end

def with_resource_timeout(temp_timeout)
if connection.nil?
prev_read_timeout = @config[:read_timeout] || 0
@config.merge!(read_timeout: temp_timeout) # Create new client with temp_timeout for read timeout
else
prev_read_timeout = connection.read_timeout
connection.read_timeout = temp_timeout
end
yield
ensure
@config.merge!(read_timeout: prev_read_timeout)
connection&.read_timeout = prev_read_timeout
end

private

def acquire_semian_resource(**)
super
rescue ActiveRecord::StatementInvalid => error
if error.cause.is_a?(Trilogy::TimeoutError)
semian_resource.mark_failed(error)
error.semian_identifier = semian_identifier
end
raise
end

def resource_exceptions
[ActiveRecord::ConnectionNotEstablished]
end

# TODO: share this with Mysql2
QUERY_ALLOWLIST = Regexp.union(
%r{\A(?:/\*.*?\*/)?\s*ROLLBACK}i,
%r{\A(?:/\*.*?\*/)?\s*COMMIT}i,
%r{\A(?:/\*.*?\*/)?\s*RELEASE\s+SAVEPOINT}i,
)

def query_allowlisted?(sql, *)
QUERY_ALLOWLIST.match?(sql)
rescue ArgumentError
return false unless sql.valid_encoding?

raise
end

def connect(*args)
acquire_semian_resource(adapter: :trilogy_adapter, scope: :connection) do
super
end
end
end
end

ActiveRecord::ConnectionAdapters::TrilogyAdapter.prepend(Semian::TrilogyAdapter)
Loading