activerecord-aurora-serverless-adapter 6.0.0 → 6.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 753090515e0b5d6dc65ac1fdabd712a1c7c673af742340d6e89c41464d1f5863
4
- data.tar.gz: d48a50c7bc45a756202b4377ee16693db6d1ea153ba52d89459a3cb64f85f726
3
+ metadata.gz: 0eb80620f3d44cff64b9263ffb94bec8cfc5507794fc4318892655a229ec3981
4
+ data.tar.gz: f4d986df84720fc51d62412328959fbdefa9e16203b9dfc537670fa50e9d54eb
5
5
  SHA512:
6
- metadata.gz: 418749f96453b0dedf58dc2183e4886f09180423c008ab2f7c0126f2db4abf031b9ac3f3df80cba3c2088b4a14a1798b3861df5f31cf0e0847ebcbf6f4c4092d
7
- data.tar.gz: 700cab49d4ea92f32c24b74c2cc1edee63a1c0ad42004db8dcc56560486144ea7a1491cee584d9fba60ffef3d1aad6619180fc8f97a4d987c5108465bb2846b8
6
+ metadata.gz: caa6fce9998662d5446e5f26c54afffa92c3560ce64cc79b43167a3eb10e426147a66750d064cdfcbad9331eb31346c24f61a49abb99463414b5a17123c08b8c
7
+ data.tar.gz: e28ee33496c94f9814b1a644dbf6a730b5db87bddcfe67cca0dcac3707d9564b444401562b192262d7e51eea3ef5d5677ebffbeac69735d1671c0ce186089fe4
@@ -83,9 +83,10 @@ GIT
83
83
  PATH
84
84
  remote: .
85
85
  specs:
86
- activerecord-aurora-serverless-adapter (6.0.0)
86
+ activerecord-aurora-serverless-adapter (6.0.1)
87
87
  activerecord (>= 6.0)
88
88
  aws-sdk-rdsdataservice
89
+ retriable
89
90
 
90
91
  GEM
91
92
  remote: https://rubygems.org/
@@ -96,8 +97,8 @@ GEM
96
97
  rake
97
98
  thor (>= 0.14.0)
98
99
  aws-eventstream (1.0.3)
99
- aws-partitions (1.260.0)
100
- aws-sdk-core (3.86.0)
100
+ aws-partitions (1.265.0)
101
+ aws-sdk-core (3.89.1)
101
102
  aws-eventstream (~> 1.0, >= 1.0.2)
102
103
  aws-partitions (~> 1, >= 1.239.0)
103
104
  aws-sigv4 (~> 1.1)
@@ -152,6 +153,7 @@ GEM
152
153
  rails-html-sanitizer (1.3.0)
153
154
  loofah (~> 2.3)
154
155
  rake (13.0.1)
156
+ retriable (3.1.2)
155
157
  ruby-progressbar (1.10.1)
156
158
  sprockets (4.0.0)
157
159
  concurrent-ruby (~> 1.0)
@@ -12,13 +12,16 @@ Gem::Specification.new do |spec|
12
12
  spec.homepage = 'https://github.com/customink/activerecord-aurora-serverless-adapter'
13
13
  spec.license = 'MIT'
14
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)/}) }
15
+ `git ls-files -z`.split("\x0").reject do |f|
16
+ f.match(%r{^(test|spec|features|docker)/i})
17
+ end
16
18
  end
17
19
  spec.bindir = 'exe'
18
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
21
  spec.require_paths = ['lib']
20
22
  spec.add_runtime_dependency 'activerecord', '>= 6.0'
21
23
  spec.add_runtime_dependency 'aws-sdk-rdsdataservice'
24
+ spec.add_runtime_dependency 'retriable'
22
25
  spec.add_development_dependency 'appraisal'
23
26
  spec.add_development_dependency 'dotenv'
24
27
  spec.add_development_dependency 'minitest'
@@ -23,6 +23,14 @@ module ActiveRecord
23
23
  "#<#{self.class} database: #{database.inspect}, raw_client: #{raw_client.inspect}>"
24
24
  end
25
25
 
26
+ def execute_statement_retry(sql)
27
+ if @connected
28
+ execute_statement(sql)
29
+ else
30
+ auto_paused_retry { execute_statement(sql) }
31
+ end
32
+ end
33
+
26
34
  def execute_statement(sql)
27
35
  id = @transactions.first
28
36
  debug_transactions "EXECUTE: #{sql}", id
@@ -34,6 +42,7 @@ module ActiveRecord
34
42
  include_result_metadata: true,
35
43
  transaction_id: id
36
44
  }).tap do |r|
45
+ @connected = true
37
46
  @affected_rows = affected_rows_result(r)
38
47
  @last_id = last_id_result(r)
39
48
  end
@@ -100,6 +109,22 @@ module ActiveRecord
100
109
  field.long_value || field.string_value || field.double_value
101
110
  end
102
111
 
112
+ def auto_paused_retry
113
+ error_klass = Aws::RDSDataService::Errors::BadRequestException
114
+ error_msg = /last packet sent successfully to the server was/
115
+ retry_msg = 'Aurora auto paused, retrying...'
116
+ on_retry = Proc.new { sleep(1) ; ::Rails.logger.info(retry_msg) }
117
+ Retriable.retriable({
118
+ on: { error_klass => error_msg },
119
+ on_retry: on_retry,
120
+ tries: auto_paused_retry_count
121
+ }) { yield }
122
+ end
123
+
124
+ def auto_paused_retry_count
125
+ 10
126
+ end
127
+
103
128
  def debug_transactions(name, id = 'NOID')
104
129
  return unless @debug_transactions
105
130
  ActiveRecord::Base.logger.debug " \e[36m#{name} #{id} #{object_id}\e[0m"
@@ -1,7 +1,7 @@
1
1
  module ActiveRecord
2
2
  module ConnectionAdapters
3
3
  module AuroraServerless
4
- VERSION = "6.0.0"
4
+ VERSION = "6.0.1"
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,160 @@
1
+ ENV['AASA_ENV'] = 'test'
2
+ require 'bundler/setup'
3
+ Bundler.require :default, :development
4
+ Dotenv.load('.env')
5
+ require 'minitest/spec'
6
+ require 'minitest/autorun'
7
+ require 'minitest/retry'
8
+ require 'minitest/reporters'
9
+ require 'cases/helper' unless ENV['TEST_FILES'] || ENV['ONLY_AASA']
10
+ require_relative 'support/aasa_coerceable'
11
+ require_relative 'support/aasa_env'
12
+ require_relative 'support/aasa_fixtures'
13
+ require_relative 'support/aasa_minitest'
14
+ Rails.backtrace_cleaner.remove_silencers! if ENV['REMOVE_SILENCERS']
15
+
16
+ module ActiveRecord
17
+ module ConnectionAdapters
18
+ module AuroraServerless
19
+ class TestCase < Minitest::Spec
20
+
21
+ before { setup_table }
22
+ after { drop_table }
23
+
24
+ private
25
+
26
+ def client
27
+ @client ||= AuroraServerless::Client.new(
28
+ 'activerecord_unittest',
29
+ ENV['AASA_RESOURCE_ARN'],
30
+ ENV['AASA_SECRET_ARN']
31
+ )
32
+ end
33
+
34
+ def execute(sql)
35
+ r = client.execute_statement(sql)
36
+ # TODO: [PG] Make this a conditional result wrapper.
37
+ AuroraServerless::Mysql2::Result.new(r)
38
+ end
39
+
40
+ def setup_table
41
+ # TODO: [PG] Make this a conditional for constant SQL.
42
+ execute MYSQL_CREATE_TABLE_SQL
43
+ execute MYSQL_INSERT_SQL
44
+ end
45
+
46
+ def drop_table
47
+ execute 'DROP TABLE IF EXISTS aurora_test'
48
+ end
49
+
50
+ MYSQL_CREATE_TABLE_SQL = %[
51
+ CREATE TABLE IF NOT EXISTS aurora_test (
52
+ id int NOT NULL AUTO_INCREMENT,
53
+ PRIMARY KEY (id),
54
+ null_test VARCHAR(10),
55
+ bit_test BIT,
56
+ tiny_int_test TINYINT,
57
+ small_int_test SMALLINT,
58
+ medium_int_test MEDIUMINT,
59
+ int_test INT,
60
+ big_int_test BIGINT,
61
+ float_test FLOAT(10,3),
62
+ float_zero_test FLOAT(10,3),
63
+ double_test DOUBLE(10,3),
64
+ decimal_test DECIMAL(10,3),
65
+ decimal_zero_test DECIMAL(10,3),
66
+ date_test DATE,
67
+ date_time_test DATETIME,
68
+ timestamp_test TIMESTAMP,
69
+ time_test TIME,
70
+ year_test YEAR(4),
71
+ char_test CHAR(10),
72
+ varchar_test VARCHAR(10),
73
+ binary_test BINARY(10),
74
+ varbinary_test VARBINARY(10),
75
+ tiny_blob_test TINYBLOB,
76
+ tiny_text_test TINYTEXT,
77
+ blob_test BLOB,
78
+ text_test TEXT,
79
+ medium_blob_test MEDIUMBLOB,
80
+ medium_text_test MEDIUMTEXT,
81
+ long_blob_test LONGBLOB,
82
+ long_text_test LONGTEXT,
83
+ enum_test ENUM('val1', 'val2'),
84
+ set_test SET('val1', 'val2')
85
+ ) DEFAULT CHARSET=utf8
86
+ ]
87
+
88
+ MYSQL_INSERT_SQL = %[
89
+ INSERT INTO aurora_test (
90
+ null_test,
91
+ bit_test,
92
+ tiny_int_test,
93
+ small_int_test,
94
+ medium_int_test,
95
+ int_test,
96
+ big_int_test,
97
+ float_test,
98
+ float_zero_test,
99
+ double_test,
100
+ decimal_test,
101
+ decimal_zero_test,
102
+ date_test,
103
+ date_time_test,
104
+ timestamp_test,
105
+ time_test,
106
+ year_test,
107
+ char_test,
108
+ varchar_test,
109
+ binary_test,
110
+ varbinary_test,
111
+ tiny_blob_test,
112
+ tiny_text_test,
113
+ blob_test,
114
+ text_test,
115
+ medium_blob_test,
116
+ medium_text_test,
117
+ long_blob_test,
118
+ long_text_test,
119
+ enum_test,
120
+ set_test
121
+ )
122
+ VALUES (
123
+ NULL,
124
+ 1,
125
+ 5,
126
+ 32766,
127
+ 8388606,
128
+ 2147483646,
129
+ 9223372036854775806,
130
+ 156.68449197860963,
131
+ 0.0,
132
+ 606682.8877005348,
133
+ 676254.5454545454,
134
+ 0,
135
+ '2010-4-4',
136
+ '2010-4-4 11:44:00',
137
+ '2010-4-4 11:44:00',
138
+ '11:44:00',
139
+ 2019,
140
+ 'abcdefg',
141
+ 'abcdefg',
142
+ 'abcdefg',
143
+ 'abcdefg',
144
+ 'abcdefg',
145
+ 'abcdefg',
146
+ 'abcdefg',
147
+ 'abcdefg',
148
+ 'abcdefg',
149
+ 'abcdefg',
150
+ 'abcdefg',
151
+ 'abcdefg',
152
+ 'val1',
153
+ 'val1,val2'
154
+ )
155
+ ]
156
+
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import "source-map-support/register";
3
+ import cdk = require("@aws-cdk/core");
4
+ import { AuroraServerlessStack } from "../lib/aurora-serverless-stack";
5
+
6
+ const APP = new cdk.App();
7
+
8
+ new AuroraServerlessStack(APP, "AuroraServerlessStack", {
9
+ stackName: "aasa-mysql-unit",
10
+ description: "Test customink/activerecord-aurora-serverless-adapter.",
11
+ tags: {
12
+ env: "dev",
13
+ group: "shared",
14
+ application: "activerecord-aurora-serverless-adapter",
15
+ owner: "kcollins"
16
+ }
17
+ });
@@ -0,0 +1,3 @@
1
+ {
2
+ "@aws-cdk/core:enableStackNameDuplicates": "true"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "app": "ts-node bin/aurora-serverless.ts"
3
+ }
@@ -0,0 +1,194 @@
1
+ // Thanks to https://almirzulic.com/posts/create-serverless-aurora-cluster-with-cdk/
2
+
3
+ import { CfnOutput, Construct, Stack, StackProps } from "@aws-cdk/core";
4
+ import { Vpc, SubnetType } from "@aws-cdk/aws-ec2";
5
+ import {
6
+ CfnDBCluster,
7
+ CfnDBSubnetGroup,
8
+ CfnDBClusterProps,
9
+ CfnDBClusterParameterGroup
10
+ } from "@aws-cdk/aws-rds";
11
+ import { CfnSecret, CfnSecretProps } from "@aws-cdk/aws-secretsmanager";
12
+ import { User, Policy, PolicyStatement, CfnAccessKey } from "@aws-cdk/aws-iam";
13
+
14
+ const MASTER_USER = process.env.AASA_MASTER_USER;
15
+ const MASTER_PASS = process.env.AASA_MASTER_PASS;
16
+ const DB_NAME = "activerecord_unittest";
17
+ const DB_CLUSTER_ID = "aasa-mysql-unit";
18
+
19
+ function auroraProps(
20
+ dbName: string,
21
+ dbClusterId: string,
22
+ dbSubnetGroupName: string | undefined,
23
+ dbParamGroup: CfnDBClusterParameterGroup
24
+ ) {
25
+ return {
26
+ databaseName: dbName,
27
+ dbClusterIdentifier: dbClusterId,
28
+ engine: "aurora",
29
+ engineMode: "serverless",
30
+ masterUsername: MASTER_USER,
31
+ masterUserPassword: MASTER_PASS,
32
+ enableHttpEndpoint: true,
33
+ port: 3306,
34
+ dbSubnetGroupName: dbSubnetGroupName,
35
+ dbClusterParameterGroupName: dbParamGroup.ref,
36
+ scalingConfiguration: {
37
+ autoPause: true,
38
+ minCapacity: 1,
39
+ maxCapacity: 4,
40
+ secondsUntilAutoPause: 3600
41
+ }
42
+ } as CfnDBClusterProps;
43
+ }
44
+
45
+ function secretProps(aurora: CfnDBCluster, dbClusterId: string) {
46
+ return {
47
+ secretString: JSON.stringify({
48
+ username: MASTER_USER,
49
+ password: MASTER_PASS,
50
+ engine: "mysql",
51
+ host: aurora.attrEndpointAddress,
52
+ port: aurora.attrEndpointPort,
53
+ dbClusterIdentifier: dbClusterId
54
+ }),
55
+ description: "AASA Master Credentials."
56
+ } as CfnSecretProps;
57
+ }
58
+
59
+ export class AuroraServerlessStack extends Stack {
60
+ constructor(scope: Construct, id: string, props?: StackProps) {
61
+ super(scope, id, props);
62
+
63
+ // VPC
64
+
65
+ const vpc = new Vpc(this, "Vpc", {
66
+ cidr: "10.0.0.0/16",
67
+ natGateways: 0,
68
+ subnetConfiguration: [
69
+ { name: "aasa_isolated", subnetType: SubnetType.ISOLATED }
70
+ ]
71
+ });
72
+ const subnetIds: string[] = [];
73
+ vpc.isolatedSubnets.forEach(subnet => {
74
+ subnetIds.push(subnet.subnetId);
75
+ });
76
+
77
+ // SUBNET GROUP
78
+
79
+ const dbSubnetGroup: CfnDBSubnetGroup = new CfnDBSubnetGroup(
80
+ this,
81
+ "AuroraSubnetGroup",
82
+ {
83
+ dbSubnetGroupDescription: "Subnet group to AASA Aurora",
84
+ dbSubnetGroupName: "aasa-subnet-group",
85
+ subnetIds
86
+ }
87
+ );
88
+
89
+ // RDS PARAMETER GROUP
90
+
91
+ const dbParamGroup = new CfnDBClusterParameterGroup(
92
+ this,
93
+ "ParameterGroup",
94
+ {
95
+ family: "aurora5.6",
96
+ description: "Test customink/activerecord-aurora-serverless-adapter.",
97
+ parameters: {
98
+ innodb_large_prefix: "1",
99
+ innodb_file_per_table: "1",
100
+ innodb_file_format: "Barracuda",
101
+ character_set_client: "utf8mb4",
102
+ character_set_connection: "utf8mb4",
103
+ character_set_database: "utf8mb4",
104
+ character_set_results: "utf8mb4",
105
+ character_set_server: "utf8mb4",
106
+ collation_server: "utf8mb4_unicode_ci",
107
+ collation_connection: "utf8mb4_unicode_ci"
108
+ }
109
+ }
110
+ );
111
+ dbParamGroup.addDependsOn(dbSubnetGroup);
112
+
113
+ // AURORA SERVERLESS CLUSTERS
114
+
115
+ const aurora = new CfnDBCluster(
116
+ this,
117
+ "AuroraServerless",
118
+ auroraProps(
119
+ DB_NAME,
120
+ DB_CLUSTER_ID,
121
+ dbSubnetGroup.dbSubnetGroupName,
122
+ dbParamGroup
123
+ )
124
+ );
125
+ const aurora2 = new CfnDBCluster(
126
+ this,
127
+ "AuroraServerless2",
128
+ auroraProps(
129
+ `${DB_NAME}2`,
130
+ `${DB_CLUSTER_ID}2`,
131
+ dbSubnetGroup.dbSubnetGroupName,
132
+ dbParamGroup
133
+ )
134
+ );
135
+ aurora.addDependsOn(dbParamGroup);
136
+ aurora2.addDependsOn(dbParamGroup);
137
+ new CfnOutput(this, "AASAResourceArn", {
138
+ value: `arn:aws:rds:${this.region}:${this.account}:cluster:${DB_CLUSTER_ID}`
139
+ });
140
+ new CfnOutput(this, "AASAResourceArn2", {
141
+ value: `arn:aws:rds:${this.region}:${this.account}:cluster:${DB_CLUSTER_ID}2`
142
+ });
143
+
144
+ // SECRETS
145
+
146
+ const secret = new CfnSecret(
147
+ this,
148
+ "Secret",
149
+ secretProps(aurora, DB_CLUSTER_ID)
150
+ );
151
+ const secret2 = new CfnSecret(
152
+ this,
153
+ "Secret2",
154
+ secretProps(aurora2, `${DB_CLUSTER_ID}2`)
155
+ );
156
+ secret.addDependsOn(aurora);
157
+ secret2.addDependsOn(aurora2);
158
+ new CfnOutput(this, "AASASecretArn", {
159
+ value: secret.ref
160
+ });
161
+ new CfnOutput(this, "AASASecretArn2", {
162
+ value: secret2.ref
163
+ });
164
+
165
+ // TEST USER
166
+
167
+ const user = new User(this, "TestUser");
168
+ const policy = new Policy(this, "TestUserPolicy", {
169
+ statements: [
170
+ new PolicyStatement({
171
+ actions: ["rds-data:*"],
172
+ resources: [
173
+ `arn:aws:rds:${this.region}:${this.account}:cluster:${DB_CLUSTER_ID}*`,
174
+ `arn:aws:rds:${this.region}:${this.account}:cluster:${DB_CLUSTER_ID}2*`
175
+ ]
176
+ }),
177
+ new PolicyStatement({
178
+ actions: ["secretsmanager:*"],
179
+ resources: [`${secret.ref}*`, `${secret2.ref}*`]
180
+ })
181
+ ]
182
+ });
183
+ user.attachInlinePolicy(policy);
184
+ const key = new CfnAccessKey(this, "TestUserKey", {
185
+ userName: user.userName
186
+ });
187
+ new CfnOutput(this, "AASAUserAccessKeyId", {
188
+ value: key.ref
189
+ });
190
+ new CfnOutput(this, "AASAUserSecretAccessKey", {
191
+ value: key.attrSecretAccessKey
192
+ });
193
+ }
194
+ }