activerecord-aurora-serverless-adapter 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|