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.
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