activerecord-aurora-serverless-adapter 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yaml +37 -0
- data/.gitignore +21 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dockerfile-cdk +3 -0
- data/Dockerfile-ci +2 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +189 -0
- data/LICENSE.txt +21 -0
- data/README.md +110 -0
- data/Rakefile +27 -0
- data/activerecord-aurora-serverless-adapter.gemspec +30 -0
- data/bin/_setup +5 -0
- data/bin/_test +23 -0
- data/bin/bootstrap +8 -0
- data/bin/run +8 -0
- data/bin/setup +8 -0
- data/bin/test +8 -0
- data/bin/test-ci-setup +16 -0
- data/docker-compose.yml +36 -0
- data/lib/active_record/connection_adapters/aurora_serverless/abstract.rb +29 -0
- data/lib/active_record/connection_adapters/aurora_serverless/client.rb +152 -0
- data/lib/active_record/connection_adapters/aurora_serverless/gem_hack.rb +15 -0
- data/lib/active_record/connection_adapters/aurora_serverless/mysql2.rb +90 -0
- data/lib/active_record/connection_adapters/aurora_serverless/mysql2/client.rb +71 -0
- data/lib/active_record/connection_adapters/aurora_serverless/mysql2/connection_handling.rb +20 -0
- data/lib/active_record/connection_adapters/aurora_serverless/mysql2/result.rb +116 -0
- data/lib/active_record/connection_adapters/aurora_serverless/version.rb +7 -0
- data/lib/active_record/connection_adapters/aurora_serverless_adapter.rb +7 -0
- data/lib/activerecord-aurora-serverless-adapter.rb +1 -0
- data/lib/mysql2.rb +4 -0
- metadata +217 -0
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require_relative 'test/support/aasa_paths'
|
4
|
+
require_relative 'test/support/aasa_rake'
|
5
|
+
|
6
|
+
namespace :test do
|
7
|
+
|
8
|
+
%w(mysql).each do |mode|
|
9
|
+
|
10
|
+
Rake::TestTask.new(mode) do |t|
|
11
|
+
t.libs = AASA::Paths.test_load_paths
|
12
|
+
t.test_files = AASA::Rake.test_files
|
13
|
+
t.warning = !!ENV['WARNING']
|
14
|
+
t.verbose = false
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
task 'mysql:env' do
|
20
|
+
ENV['ARCONN'] = 'mysql'
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
task test: ['test:mysql']
|
26
|
+
task 'test:mysql' => 'test:mysql:env'
|
27
|
+
task default: [:test]
|
@@ -0,0 +1,30 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "active_record/connection_adapters/aurora_serverless/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "activerecord-aurora-serverless-adapter"
|
7
|
+
spec.version = ActiveRecord::ConnectionAdapters::AuroraServerless::VERSION
|
8
|
+
spec.authors = ["Ken Collins"]
|
9
|
+
spec.email = ["kcollins@customink.com"]
|
10
|
+
spec.summary = %q{ActiveRecord Adapter for Amazon Aurora Serverless}
|
11
|
+
spec.description = %q{Amazon Aurora Serverless is an on-demand, auto-scaling configuration for Amazon Aurora (MySQL-compatible and PostgreSQL-compatible editions). Perfect for small Rails on AWS Lambda.}
|
12
|
+
spec.homepage = 'https://github.com/customink/activerecord-aurora-serverless-adapter'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
15
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
16
|
+
end
|
17
|
+
spec.bindir = 'exe'
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
spec.add_runtime_dependency 'activerecord', '>= 6.0'
|
21
|
+
spec.add_runtime_dependency 'aws-sdk-rdsdataservice'
|
22
|
+
spec.add_development_dependency 'appraisal'
|
23
|
+
spec.add_development_dependency 'dotenv'
|
24
|
+
spec.add_development_dependency 'minitest'
|
25
|
+
spec.add_development_dependency 'minitest-reporters'
|
26
|
+
spec.add_development_dependency 'minitest-retry'
|
27
|
+
spec.add_development_dependency 'pry'
|
28
|
+
spec.add_development_dependency 'rake'
|
29
|
+
spec.add_development_dependency 'sqlite3'
|
30
|
+
end
|
data/bin/_setup
ADDED
data/bin/_test
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
set -e
|
3
|
+
|
4
|
+
export AWS_PROFILE=${AWS_PROFILE:=default}
|
5
|
+
export AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:=us-east-1}
|
6
|
+
|
7
|
+
echo "Run our isolated tests..."
|
8
|
+
ONLY_AASA=1 bundle exec rake test
|
9
|
+
|
10
|
+
echo "Run ActiveRecord tests touching arunit2..."
|
11
|
+
ONLY_ACTIVERECORD=1 AASA_ARUNIT2=1 bundle exec rake test
|
12
|
+
|
13
|
+
echo "Run isolated AASA_ARHABTM test..."
|
14
|
+
ONLY_ACTIVERECORD=1 AASA_ARHABTM=1 bundle exec rake test
|
15
|
+
echo "Run isolated AASA_ARCONHANDLER test..."
|
16
|
+
ONLY_ACTIVERECORD=1 AASA_ARCONHANDLER=1 bundle exec rake test
|
17
|
+
|
18
|
+
echo "Run ActiveRecord test batches 1..."
|
19
|
+
ONLY_ACTIVERECORD=1 AASA_BATCH=1 bundle exec rake test
|
20
|
+
echo "Run ActiveRecord test batches 2..."
|
21
|
+
ONLY_ACTIVERECORD=1 AASA_BATCH=2 bundle exec rake test
|
22
|
+
echo "Run ActiveRecord test batches 3..."
|
23
|
+
ONLY_ACTIVERECORD=1 AASA_BATCH=3 bundle exec rake test
|
data/bin/bootstrap
ADDED
data/bin/run
ADDED
data/bin/setup
ADDED
data/bin/test
ADDED
data/bin/test-ci-setup
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
set -e
|
3
|
+
|
4
|
+
mkdir -p ~/.aws
|
5
|
+
|
6
|
+
cat >> ~/.aws/credentials << EOL
|
7
|
+
[default]
|
8
|
+
aws_access_key_id=${AWS_ACCESS_KEY_ID}
|
9
|
+
aws_secret_access_key=${AWS_SECRET_ACCESS_KEY}
|
10
|
+
EOL
|
11
|
+
|
12
|
+
cat >> ~/.aws/config << EOL
|
13
|
+
[default]
|
14
|
+
region = us-east-1
|
15
|
+
output = json
|
16
|
+
EOL
|
data/docker-compose.yml
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
version: "3.7"
|
2
|
+
services:
|
3
|
+
ci:
|
4
|
+
build:
|
5
|
+
context: .
|
6
|
+
dockerfile: Dockerfile-ci
|
7
|
+
environment:
|
8
|
+
- AWS_PROFILE=${AWS_PROFILE}
|
9
|
+
- AASA_MASTER_USER=${AASA_MASTER_USER}
|
10
|
+
- AASA_MASTER_PASS=${AASA_MASTER_PASS}
|
11
|
+
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
12
|
+
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
13
|
+
- AASA_SECRET_ARN=${AASA_SECRET_ARN}
|
14
|
+
- AASA_RESOURCE_ARN=${AASA_RESOURCE_ARN}
|
15
|
+
- AASA_SECRET_ARN2=${AASA_SECRET_ARN2}
|
16
|
+
- AASA_RESOURCE_ARN2=${AASA_RESOURCE_ARN2}
|
17
|
+
volumes:
|
18
|
+
- ~/.aws:/root/.aws:delegated
|
19
|
+
- .:/var/task:delegated
|
20
|
+
cdk:
|
21
|
+
build:
|
22
|
+
context: .
|
23
|
+
dockerfile: Dockerfile-cdk
|
24
|
+
environment:
|
25
|
+
- AWS_PROFILE=${AWS_PROFILE}
|
26
|
+
- AASA_MASTER_USER=${AASA_MASTER_USER}
|
27
|
+
- AASA_MASTER_PASS=${AASA_MASTER_PASS}
|
28
|
+
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
29
|
+
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
30
|
+
- AASA_SECRET_ARN=${AASA_SECRET_ARN}
|
31
|
+
- AASA_RESOURCE_ARN=${AASA_RESOURCE_ARN}
|
32
|
+
- AASA_SECRET_ARN2=${AASA_SECRET_ARN2}
|
33
|
+
- AASA_RESOURCE_ARN2=${AASA_RESOURCE_ARN2}
|
34
|
+
volumes:
|
35
|
+
- ~/.aws:/root/.aws:delegated
|
36
|
+
- .:/var/task:delegated
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module AuroraServerless
|
4
|
+
module Abstract
|
5
|
+
|
6
|
+
# AbstractAdapter
|
7
|
+
|
8
|
+
def prepared_statements
|
9
|
+
false
|
10
|
+
end
|
11
|
+
|
12
|
+
# Database Statements
|
13
|
+
|
14
|
+
def begin_db_transaction
|
15
|
+
@connection.begin_db_transaction
|
16
|
+
end
|
17
|
+
|
18
|
+
def commit_db_transaction
|
19
|
+
@connection.commit_db_transaction
|
20
|
+
end
|
21
|
+
|
22
|
+
def exec_rollback_db_transaction
|
23
|
+
@connection.exec_rollback_db_transaction
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module AuroraServerless
|
4
|
+
class Client
|
5
|
+
attr_reader :database,
|
6
|
+
:resource_arn,
|
7
|
+
:secret_arn,
|
8
|
+
:raw_client,
|
9
|
+
:affected_rows,
|
10
|
+
:last_id
|
11
|
+
|
12
|
+
def initialize(database, resource_arn, secret_arn, options = {})
|
13
|
+
@database = database
|
14
|
+
@resource_arn = resource_arn
|
15
|
+
@secret_arn = secret_arn
|
16
|
+
@raw_client = Aws::RDSDataService::Client.new(client_options(options))
|
17
|
+
@transactions = []
|
18
|
+
@affected_rows = 0
|
19
|
+
@debug_transactions = false # Development toggle.
|
20
|
+
end
|
21
|
+
|
22
|
+
def inspect
|
23
|
+
"#<#{self.class} database: #{database.inspect}, raw_client: #{raw_client.inspect}>"
|
24
|
+
end
|
25
|
+
|
26
|
+
def execute_statement(sql)
|
27
|
+
id = @transactions.first
|
28
|
+
debug_transactions "EXECUTE: #{sql}", id
|
29
|
+
raw_client.execute_statement({
|
30
|
+
sql: sql,
|
31
|
+
database: database,
|
32
|
+
secret_arn: secret_arn,
|
33
|
+
resource_arn: resource_arn,
|
34
|
+
include_result_metadata: true,
|
35
|
+
transaction_id: id
|
36
|
+
}).tap do |r|
|
37
|
+
@affected_rows = affected_rows_result(r)
|
38
|
+
@last_id = last_id_result(r)
|
39
|
+
end
|
40
|
+
rescue Exception => e
|
41
|
+
if id && e.message == "Transaction #{id} is not found"
|
42
|
+
@transactions.shift
|
43
|
+
retry
|
44
|
+
else
|
45
|
+
raise e
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def begin_db_transaction
|
50
|
+
id = raw_client.begin_transaction({
|
51
|
+
database: database,
|
52
|
+
secret_arn: secret_arn,
|
53
|
+
resource_arn: resource_arn
|
54
|
+
}).try(:transaction_id)
|
55
|
+
debug_transactions 'BEGIN', id
|
56
|
+
@transactions.unshift(id) if id
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
def commit_db_transaction
|
61
|
+
id = @transactions.shift
|
62
|
+
return unless id
|
63
|
+
debug_transactions 'COMMIT', id
|
64
|
+
raw_client.commit_transaction({
|
65
|
+
secret_arn: secret_arn,
|
66
|
+
resource_arn: resource_arn,
|
67
|
+
transaction_id: id
|
68
|
+
})
|
69
|
+
true
|
70
|
+
rescue
|
71
|
+
@transactions.unshift(id) # For imminent rollback.
|
72
|
+
end
|
73
|
+
|
74
|
+
def exec_rollback_db_transaction
|
75
|
+
id = @transactions.shift
|
76
|
+
return unless id
|
77
|
+
debug_transactions 'ROLLBACK', id
|
78
|
+
raw_client.rollback_transaction({
|
79
|
+
secret_arn: secret_arn,
|
80
|
+
resource_arn: resource_arn,
|
81
|
+
transaction_id: id
|
82
|
+
})
|
83
|
+
true
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def client_options(options)
|
89
|
+
options.slice(*CLIENT_OPTIONS)
|
90
|
+
end
|
91
|
+
|
92
|
+
def affected_rows_result(result)
|
93
|
+
result.number_of_records_updated || 0
|
94
|
+
end
|
95
|
+
|
96
|
+
def last_id_result(result)
|
97
|
+
fields = result.generated_fields || []
|
98
|
+
field = fields.last
|
99
|
+
return unless field
|
100
|
+
field.long_value || field.string_value || field.double_value
|
101
|
+
end
|
102
|
+
|
103
|
+
def debug_transactions(name, id = 'NOID')
|
104
|
+
return unless @debug_transactions
|
105
|
+
ActiveRecord::Base.logger.debug " \e[36m#{name} #{id} #{object_id}\e[0m"
|
106
|
+
end
|
107
|
+
|
108
|
+
# From AWS docs at https://amzn.to/35V6O8L
|
109
|
+
CLIENT_OPTIONS = %i[
|
110
|
+
credentials
|
111
|
+
region
|
112
|
+
access_key_id
|
113
|
+
active_endpoint_cache
|
114
|
+
client_side_monitoring
|
115
|
+
client_side_monitoring_client_id
|
116
|
+
client_side_monitoring_host
|
117
|
+
client_side_monitoring_port
|
118
|
+
client_side_monitoring_publisher
|
119
|
+
convert_params
|
120
|
+
disable_host_prefix_injection
|
121
|
+
endpoint
|
122
|
+
endpoint_cache_max_entries
|
123
|
+
endpoint_cache_max_threads
|
124
|
+
endpoint_cache_poll_interval
|
125
|
+
endpoint_discovery
|
126
|
+
log_formatter
|
127
|
+
log_level
|
128
|
+
logger
|
129
|
+
profile
|
130
|
+
retry_base_delay
|
131
|
+
retry_jitter
|
132
|
+
retry_limit
|
133
|
+
retry_max_delay
|
134
|
+
secret_access_key
|
135
|
+
session_token
|
136
|
+
stub_responses
|
137
|
+
validate_params
|
138
|
+
http_proxy
|
139
|
+
http_open_timeout
|
140
|
+
http_read_timeout
|
141
|
+
http_idle_timeout
|
142
|
+
http_continue_timeout
|
143
|
+
http_wire_trace
|
144
|
+
ssl_verify_peer
|
145
|
+
ssl_ca_bundle
|
146
|
+
ssl_ca_directory
|
147
|
+
].freeze
|
148
|
+
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Not perfect. We want to avoid bundling native gems
|
2
|
+
# required in each adapter, like mysql2.
|
3
|
+
#
|
4
|
+
ORIG_GEM_METHOD = method(:gem)
|
5
|
+
kernel = (class << ::Kernel; self; end)
|
6
|
+
[kernel, ::Kernel].each do |k|
|
7
|
+
k.send :remove_method, :gem
|
8
|
+
k.send :define_method, :gem do |dep, *reqs|
|
9
|
+
# TODO: [PG] Add 'postgresql' here.
|
10
|
+
unless ['mysql2'].include?(dep)
|
11
|
+
ORIG_GEM_METHOD.call(dep, *reqs)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
k.send :public, :gem
|
15
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'active_record/connection_adapters/aurora_serverless/mysql2/result'
|
2
|
+
require 'active_record/connection_adapters/aurora_serverless/mysql2/client'
|
3
|
+
require 'active_record/connection_adapters/aurora_serverless/mysql2/connection_handling'
|
4
|
+
require 'active_record/connection_adapters/aurora_serverless/gem_hack'
|
5
|
+
require 'active_record/connection_adapters/mysql2_adapter'
|
6
|
+
|
7
|
+
module ActiveRecord
|
8
|
+
module ConnectionAdapters
|
9
|
+
class AuroraServerlessAdapter < Mysql2Adapter
|
10
|
+
include AuroraServerless::Abstract
|
11
|
+
|
12
|
+
def self.name
|
13
|
+
'Mysql2Adapter'
|
14
|
+
end if ENV['AASA_ENV'] == 'test'
|
15
|
+
|
16
|
+
def mysql2_connection(config)
|
17
|
+
aurora_serverless_connection(config)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Abstract Mysql Adapter
|
21
|
+
|
22
|
+
def supports_advisory_locks?
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def connect
|
30
|
+
@connection = ConnectionHandling.aurora_serverless_connection_from_config(@config)
|
31
|
+
configure_connection
|
32
|
+
end
|
33
|
+
|
34
|
+
# Abstract Mysql Adapter
|
35
|
+
|
36
|
+
def translate_exception(exception, message:, sql:, binds:)
|
37
|
+
msg = exception.message
|
38
|
+
case msg
|
39
|
+
when /Duplicate entry/
|
40
|
+
RecordNotUnique.new(msg, sql: sql, binds: binds)
|
41
|
+
when /foreign key constraint fails/
|
42
|
+
InvalidForeignKey.new(msg, sql: sql, binds: binds)
|
43
|
+
when /Cannot add foreign key constraint/,
|
44
|
+
/referenced column .* in foreign key constraint .* are incompatible/
|
45
|
+
mismatched_foreign_key(msg, sql: sql, binds: binds)
|
46
|
+
when /Data too long for column/
|
47
|
+
ValueTooLong.new(msg, sql: sql, binds: binds)
|
48
|
+
when /Out of range value for column/
|
49
|
+
RangeError.new(msg, sql: sql, binds: binds)
|
50
|
+
when /Column .* cannot be null/,
|
51
|
+
/Field .* doesn't have a default value/
|
52
|
+
NotNullViolation.new(msg, sql: sql, binds: binds)
|
53
|
+
when /Deadlock found when trying to get lock/
|
54
|
+
Deadlocked.new(msg, sql: sql, binds: binds)
|
55
|
+
when /Lock wait timeout exceeded/
|
56
|
+
LockWaitTimeout.new(msg, sql: sql, binds: binds)
|
57
|
+
when /max_statement_time exceeded/, /Sort aborted/
|
58
|
+
StatementTimeout.new(msg, sql: sql, binds: binds)
|
59
|
+
when /Query execution was interrupted/
|
60
|
+
QueryCanceled.new(msg, sql: sql, binds: binds)
|
61
|
+
else
|
62
|
+
ActiveRecord::StatementInvalid.new(msg, sql: sql, binds: binds)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Database Statements
|
67
|
+
|
68
|
+
def execute_batch(sql, name = nil)
|
69
|
+
execute(sql, name)
|
70
|
+
end
|
71
|
+
|
72
|
+
def multi_statements_enabled?(flags)
|
73
|
+
false
|
74
|
+
end
|
75
|
+
|
76
|
+
def with_multi_statements
|
77
|
+
yield
|
78
|
+
end
|
79
|
+
|
80
|
+
def combine_multi_statements(total_sql)
|
81
|
+
total_sql
|
82
|
+
end
|
83
|
+
|
84
|
+
def max_allowed_packet
|
85
|
+
65536
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|