Skip to content

Commit dc166e9

Browse files
Merge branch 'main' into chore/replace-renovate-with-dependabot
2 parents 1d4d706 + 9833ecf commit dc166e9

2 files changed

Lines changed: 78 additions & 47 deletions

File tree

hana/lib/HANAService.js

Lines changed: 61 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const hanaKeywords = keywords.reduce((prev, curr) => {
1414
return prev
1515
}, {})
1616

17-
const DEBUG = cds.debug('sql|db')
17+
const LOG = cds.log('sql|db'), DEBUG = cds.debug('sql|db')
1818
const SYSTEM_VERSIONED = '@hana.systemversioned'
1919

2020
/**
@@ -50,47 +50,68 @@ class HANAService extends SQLService {
5050
}
5151
const isMultitenant = !!service.options.credentials.sm_url || !!service.options.credentials.baseurl || ('multiTenant' in this.options ? this.options.multiTenant : cds.env.requires.multitenancy)
5252
const acquireTimeoutMillis = this.options.pool?.acquireTimeoutMillis || (cds.env.profiles.includes('production') ? 1000 : 10000)
53+
const maxRetries = 3
54+
55+
async function createSingleTenant(tenant, attempt = 1) {
56+
try {
57+
const dbc = new driver({ ...service.options.credentials, ...clientOptions })
58+
await dbc.connect()
59+
service.server.major = dbc.server.major || service.server.major
60+
return dbc
61+
} catch (err) {
62+
if (err.code === 10) return // authentication error, see error handler below
63+
if (attempt < maxRetries) {
64+
LOG.debug('connection failed:', err, '- retrying attempt', attempt, 'of', maxRetries)
65+
return createSingleTenant(tenant, attempt + 1)
66+
}
67+
throw err
68+
}
69+
}
70+
71+
async function createMultiTenant(tenant, start = Date.now(), attempt = 1) {
72+
let credentials
73+
try {
74+
const smTenant = await require('@sap/cds-mtxs/lib').xt.serviceManager.get(tenant, { disableCache: false })
75+
credentials = smTenant.credentials
76+
const dbc = new driver({ ...credentials, ...clientOptions })
77+
await dbc.connect()
78+
service.server.major = dbc.server.major || service.server.major
79+
return dbc
80+
} catch (err) {
81+
if (cds.requires.db?.pool?.builtin !== false && cds.env.features.pool !== 'generic-pool') {
82+
if (err.status === 404 || err.status === 429) {
83+
throw new Error(`Pool failed connecting to '${tenant}'`, { cause: err })
84+
}
85+
const deadline = start + acquireTimeoutMillis
86+
if (attempt <= maxRetries && Date.now() < deadline) {
87+
// Retry transient connection failures before invalidating credentials
88+
LOG.debug('connection failed:', err, '- retrying attempt', attempt, 'of', maxRetries)
89+
return createMultiTenant(tenant, start, attempt + 1)
90+
}
91+
try {
92+
await require('@sap/cds-mtxs/lib').xt.serviceManager.get(tenant, { invalidCredentials: credentials, retryUntil: deadline })
93+
} catch (smErr) {
94+
smErr.cause = err
95+
throw new Error(`Failed connecting to pool - could not get valid credentials from Service Manager`, { cause: smErr })
96+
}
97+
if (Date.now() < deadline) return createMultiTenant(tenant, start)
98+
else throw new Error(`Pool exceeded for '${tenant}' within ${acquireTimeoutMillis}ms`, { cause: err })
99+
} else {
100+
// Stop trying when the tenant does not exist or is rate limited
101+
if (err.status == 404 || err.status == 429) {
102+
return new Promise(function (_, reject) { // break retry loop for generic-pool
103+
setTimeout(() => reject(err), acquireTimeoutMillis)
104+
})
105+
}
106+
await require('@sap/cds-mtxs/lib').xt.serviceManager.get(tenant, { disableCache: true })
107+
throw new Error(`Pool failed connecting to'${tenant}'`, { cause: err }) // generic-pool will retry on errors
108+
}
109+
}
110+
}
111+
53112
return {
54113
options: this.options.pool || {},
55-
create: async function create(tenant, start = Date.now()) {
56-
let credentials
57-
try {
58-
const smTenant = isMultitenant
59-
? await require('@sap/cds-mtxs/lib').xt.serviceManager.get(tenant, { disableCache: false })
60-
: service.options
61-
credentials = smTenant.credentials
62-
const dbc = new driver({ ...credentials, ...clientOptions })
63-
await dbc.connect()
64-
service.server.major = dbc.server.major || service.server.major
65-
return dbc
66-
} catch (err) {
67-
if (isMultitenant) {
68-
if (cds.requires.db?.pool?.builtin !== false && cds.env.features.pool !== 'generic-pool') {
69-
if (err.status === 404 || err.status === 429) {
70-
throw new Error(`Pool failed connecting to '${tenant}'`, { cause: err })
71-
}
72-
const deadline = start + acquireTimeoutMillis
73-
try {
74-
await require('@sap/cds-mtxs/lib').xt.serviceManager.get(tenant, { disableCache: true, invalidCredentials: credentials, retryUntil: deadline })
75-
} catch (smErr) {
76-
smErr.cause = err
77-
throw new Error(`Failed connecting to pool - could not get valid credentials from Service Manager`, { cause: smErr })
78-
}
79-
if (Date.now() < deadline) return create(tenant, start)
80-
else throw new Error(`Pool exceeded for '${tenant}' within ${acquireTimeoutMillis}ms`, { cause: err })
81-
} else {
82-
// Stop trying when the tenant does not exist or is rate limited
83-
if (err.status == 404 || err.status == 429) {
84-
return new Promise(function (_, reject) { // break retry loop for generic-pool
85-
setTimeout(() => reject(err), acquireTimeoutMillis)
86-
})
87-
}
88-
await require('@sap/cds-mtxs/lib').xt.serviceManager.get(tenant, { disableCache: true })
89-
throw new Error(`Pool failed connecting to'${tenant}'`, { cause: err }) // generic-pool will retry on errors
90-
}
91-
} else if (err.code !== 10) throw err
92-
}
93-
},
114+
create: isMultitenant ? createMultiTenant : createSingleTenant,
94115
error: (err /*, tenant*/) => {
95116
// Check whether the connection error was an authentication error
96117
if (err.code === 10) {

postgres/lib/PostgresService.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const crypto = require('crypto')
55
const { Writable, Readable } = require('stream')
66
const sessionVariableMap = require('./session.json')
77

8+
const LOG = cds.log('sql|db')
9+
810
class PostgresService extends SQLService {
911
init() {
1012
if (!this.options.independentDeploy) {
@@ -18,10 +20,12 @@ class PostgresService extends SQLService {
1820
}
1921

2022
get factory() {
23+
const service = this
24+
const maxRetries = 3
2125
return {
2226
options: this.options.pool || {},
23-
create: async () => {
24-
const { credentials: cr = {}, client: clientOptions = {} } = this.options
27+
create: async function create(tenant, attempt = 1) {
28+
const { credentials: cr = {}, client: clientOptions = {} } = service.options
2529
const credentials = {
2630
// Cloud Foundry provides the user in the field username the pg npm module expects user
2731
user: cr.username || cr.user,
@@ -42,11 +46,17 @@ class PostgresService extends SQLService {
4246
ca: cr.sslrootcert,
4347
}),
4448
}
45-
const dbc = new Client({ ...credentials, ...clientOptions })
46-
await dbc.connect()
47-
dbc.open = true
48-
dbc.on('end', () => { dbc.open = false })
49-
return dbc
49+
try {
50+
const dbc = new Client({ ...credentials, ...clientOptions })
51+
await dbc.connect()
52+
dbc.open = true
53+
dbc.on('end', () => { dbc.open = false })
54+
return dbc
55+
} catch (err) {
56+
if (attempt >= maxRetries) throw err
57+
LOG.debug('connection failed:', err, '- retrying attempt', attempt, 'of', maxRetries)
58+
return create(tenant, attempt + 1)
59+
}
5060
},
5161
destroy: dbc => dbc.end(),
5262
validate: dbc => dbc.open,

0 commit comments

Comments
 (0)