Skip to content

Commit e7459ba

Browse files
authored
fix: make RDS initializer wait for DB readiness (#18)
* fix: retry DB connection in RDS init lambda * fix: wait for secret attachment before DB init
1 parent 69d99ad commit e7459ba

File tree

2 files changed

+68
-11
lines changed

2 files changed

+68
-11
lines changed

demos/rds-init-example.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ export class RdsInitStackExample extends Stack {
7272
})
7373
// manage resources dependency
7474
initializer.customResource.node.addDependency(dbServer)
75+
const credsAttachment = creds.node.tryFindChild('Attachment')
76+
if (credsAttachment) {
77+
initializer.customResource.node.addDependency(credsAttachment)
78+
}
7579

7680
// allow the initializer function to connect to the RDS instance
7781
dbServer.connections.allowFrom(initializer.function, Port.tcp(3306)) // note: not required for LocalStack

demos/rds-init-fn-code/index.js

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ const fs = require('fs')
77
const path = require('path')
88
require('dotenv').config();
99

10+
const DB_CONNECT_RETRY_MAX_ATTEMPTS = Number.parseInt(process.env.DB_CONNECT_RETRY_MAX_ATTEMPTS || '24', 10)
11+
const DB_CONNECT_RETRY_DELAY_MS = Number.parseInt(process.env.DB_CONNECT_RETRY_DELAY_MS || '5000', 10)
12+
1013
// the env AWS_ENDPOINT_URL is automatically injected and available
1114
const endpoint = process.env.AWS_ENDPOINT_URL;
1215
const url = new URL(endpoint);
@@ -21,19 +24,10 @@ const secrets = new AWS.SecretsManager({
2124
})
2225

2326
exports.handler = async (e) => {
27+
let connection
2428
try {
2529
const { config } = e.params
26-
const { password, username, dbname, port } = await getSecretValue(config.credsSecretName)
27-
const connection = mysql.createConnection({
28-
host: hostname,
29-
user: username,
30-
database: dbname,
31-
port,
32-
password,
33-
multipleStatements: true
34-
})
35-
36-
connection.connect()
30+
connection = await createConnectionWithRetry(config.credsSecretName)
3731

3832
const sqlScript = fs.readFileSync(path.join(__dirname, 'script.sql')).toString()
3933
const res = await query(connection, sqlScript)
@@ -48,6 +42,10 @@ exports.handler = async (e) => {
4842
err,
4943
message: err.message
5044
}
45+
} finally {
46+
if (connection) {
47+
await closeConnection(connection)
48+
}
5149
}
5250
}
5351

@@ -61,6 +59,61 @@ function query (connection, sql) {
6159
})
6260
}
6361

62+
async function createConnectionWithRetry (connectionConfig) {
63+
let lastError
64+
for (let attempt = 1; attempt <= DB_CONNECT_RETRY_MAX_ATTEMPTS; attempt += 1) {
65+
const { password, username, dbname, port } = await getSecretValue(connectionConfig)
66+
const connection = mysql.createConnection({
67+
host: hostname,
68+
user: username,
69+
database: dbname,
70+
port,
71+
password,
72+
multipleStatements: true
73+
})
74+
try {
75+
await connect(connection)
76+
return connection
77+
} catch (error) {
78+
connection.destroy()
79+
lastError = error
80+
if (!shouldRetryConnectionError(error) || attempt === DB_CONNECT_RETRY_MAX_ATTEMPTS) {
81+
break
82+
}
83+
84+
const retryInSeconds = DB_CONNECT_RETRY_DELAY_MS / 1000
85+
console.log(`Database connection attempt ${attempt}/${DB_CONNECT_RETRY_MAX_ATTEMPTS} failed (port=${port}) with '${error.code || error.message}'. Retrying in ${retryInSeconds}s...`)
86+
await sleep(DB_CONNECT_RETRY_DELAY_MS)
87+
}
88+
}
89+
90+
throw lastError
91+
}
92+
93+
function connect (connection) {
94+
return new Promise((resolve, reject) => {
95+
connection.connect((error) => {
96+
if (error) return reject(error)
97+
98+
return resolve()
99+
})
100+
})
101+
}
102+
103+
function shouldRetryConnectionError (error) {
104+
return ['ECONNREFUSED', 'ETIMEDOUT', 'EHOSTUNREACH', 'ENOTFOUND', 'PROTOCOL_CONNECTION_LOST'].includes(error?.code)
105+
}
106+
107+
function sleep (delayMs) {
108+
return new Promise((resolve) => setTimeout(resolve, delayMs))
109+
}
110+
111+
function closeConnection (connection) {
112+
return new Promise((resolve) => {
113+
connection.end(() => resolve())
114+
})
115+
}
116+
64117
function getSecretValue (secretId) {
65118
return new Promise((resolve, reject) => {
66119
secrets.getSecretValue({ SecretId: secretId }, (err, data) => {

0 commit comments

Comments
 (0)