activerecord-aurora-serverless-adapter 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ bundle config --local path 'vendor/bundle'
5
+ bundle install --jobs 4 --retry 3
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ touch .env
5
+
6
+ docker-compose \
7
+ --project-name aasa \
8
+ build
data/bin/run ADDED
@@ -0,0 +1,8 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ docker-compose \
5
+ --project-name aasa \
6
+ run \
7
+ ci \
8
+ $@
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ docker-compose \
5
+ --project-name aasa \
6
+ run \
7
+ ci \
8
+ ./bin/_setup
data/bin/test ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ docker-compose \
5
+ --project-name aasa \
6
+ run \
7
+ ci \
8
+ ./bin/_test
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
@@ -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