Skip to content

Commit

Permalink
Return early from allow list when possible
Browse files Browse the repository at this point in the history
  • Loading branch information
camilo committed Dec 21, 2023
1 parent b2fa8b6 commit fdcdcab
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 18 deletions.
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ inherit_gem:
AllCops:
TargetRubyVersion: 2.7
NewCops: enable
Exclude:
- test/allow_list_bench.rb

Layout/LineLength:
Exclude:
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ gem "rake-compiler"

group :test do
gem "benchmark-memory"
gem "benchmark-ips"
gem "memory_profiler"
gem "minitest"
gem "mocha"
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock

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

41 changes: 29 additions & 12 deletions lib/semian/activerecord_trilogy_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,34 @@ module ActiveRecordTrilogyAdapter
ResourceBusyError = ::ActiveRecord::ConnectionAdapters::TrilogyAdapter::ResourceBusyError
CircuitOpenError = ::ActiveRecord::ConnectionAdapters::TrilogyAdapter::CircuitOpenError

QUERY_ALLOWLIST = %r{\A(?:/\*.*?\*/)?\s*(ROLLBACK|COMMIT|RELEASE\s+SAVEPOINT)}i

# The common case here is NOT to have transaction management statements, therefore
# we are exploiting the fact that ActiveRecord will use COMMIT/ROLLBACK as
# the suffix of the command string and
# name savepoints by level of nesting as `active_record_1` ... n.
#
# Since looking at the last characters in a sring using `end_with?` is a LOT cheaper than
# running a regex, we are returning early if the last characters of
# the SQL statements are NOT the last characters of the known transaction
# control statements
#
# Running `test/alllow_list_bench.rb` as of now (Dec 2023 using ruby 3.1) shows
# this method to be ~20x faster in the common case vs matching the full regular expression
class << self
def query_allowlisted?(sql, *)
if !sql.end_with?(";") && !sql.end_with?("T") && !sql.end_with?("K") && !sql.end_with?("_1") && !sql.end_with?("_2")
false
else
QUERY_ALLOWLIST.match?(sql)
end
rescue ArgumentError
return false unless sql.valid_encoding?

raise
end
end

attr_reader :raw_semian_options, :semian_identifier

def initialize(*options)
Expand All @@ -48,7 +76,7 @@ def initialize(*options)
end

def raw_execute(sql, *)
if query_allowlisted?(sql)
if Semian::ActiveRecordTrilogyAdapter.query_allowlisted?(sql)
super
else
acquire_semian_resource(adapter: :trilogy_adapter, scope: :query) do
Expand Down Expand Up @@ -90,17 +118,6 @@ def resource_exceptions
]
end

# TODO: share this with Mysql2
QUERY_ALLOWLIST = %r{\A(?:/\*.*?\*/)?\s*(ROLLBACK|COMMIT|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
Expand Down
11 changes: 5 additions & 6 deletions test/adapters/activerecord_trilogy_adapter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -329,26 +329,25 @@ def test_semian_allows_commit
def test_query_allowlisted_returns_false_for_binary_sql
binary_query = File.read(File.expand_path("../../fixtures/binary.sql", __FILE__))

refute(@adapter.send(:query_allowlisted?, binary_query))
refute(Semian::ActiveRecordTrilogyAdapter.query_allowlisted?(binary_query))
end

def test_semian_allows_rollback_to_safepoint
def test_semian_allows_set_savepoint
@adapter.execute("START TRANSACTION;")
@adapter.execute("SAVEPOINT foobar;")

Semian[:mysql_testing].acquire do
@adapter.execute("ROLLBACK TO foobar;")
@adapter.execute("SAVEPOINT active_record_99;")
end

@adapter.execute("ROLLBACK;")
end

def test_semian_allows_release_savepoint
@adapter.execute("START TRANSACTION;")
@adapter.execute("SAVEPOINT foobar;")
@adapter.execute("SAVEPOINT active_record_99;")

Semian[:mysql_testing].acquire do
@adapter.execute("RELEASE SAVEPOINT foobar;")
@adapter.execute("RELEASE SAVEPOINT active_record_99;")
end

@adapter.execute("ROLLBACK;")
Expand Down
20 changes: 20 additions & 0 deletions test/allow_list_bench.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

require "semian"
require "active_record"
require "semian/activerecord_trilogy_adapter"
require "benchmark/ips"

# sql_commit = "/*key:value,key:morevalue,morekey:morevalue,id:IDIDIDIDI,anotherkey:anothervalue,k:v,k:v*/ COMMIT"

# Common case
sql_not_commit = "/*key:value,key:morevalue,morekey:morevalue,id:IDIDIDIDID,anotherkey:anothervalue,k:v,k:v*/ SELECT /*+ 1111111111111111111111111*/ `line_items`.`id`, `line_items`.`shop_id`, `line_items`.`title`, `line_items`.`sku`, `line_items`.`vendor`,`line_items`.`variant_id`, `line_items`.`variant_title`, `line_items`.`order_id`, `line_items`.`currency`, `line_items`.`presentment_price`, `line_items`.`price`, `line_items`.`gift_card` FROM `line_items` WHERE `line_items`.`id` = ASKJAKJSDASDKJ"

Benchmark.ips do |x|
x.report("end-with-case?") do
Semian::ActiveRecordTrilogyAdapter.query_allowlisted?(sql_not_commit)
end

x.report("regex") { Semian::ActiveRecordTrilogyAdapter::QUERY_ALLOWLIST.match?(sql_not_commit) }
x.compare!
end

0 comments on commit fdcdcab

Please sign in to comment.