@cap-js/db-service 2.0.1 → 2.1.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.
- package/CHANGELOG.md +15 -0
- package/lib/common/generic-pool.js +272 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
- The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
5
5
|
- This project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [2.1.1](https://github.com/cap-js/cds-dbs/compare/db-service-v2.1.0...db-service-v2.1.1) (2025-06-06)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
* Better opt in flag for builtin generic pool ([#1234](https://github.com/cap-js/cds-dbs/issues/1234)) ([98282ff](https://github.com/cap-js/cds-dbs/commit/98282ffd86cb143ebe9000f75b4a51156f6c3539))
|
|
13
|
+
* miscellaneous fixes for builtin pool ([#1235](https://github.com/cap-js/cds-dbs/issues/1235)) ([c657a0b](https://github.com/cap-js/cds-dbs/commit/c657a0ba2292fd429adb1b69aa9ead3eec9d5be0))
|
|
14
|
+
|
|
15
|
+
## [2.1.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.0.1...db-service-v2.1.0) (2025-06-04)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
* opt-in replacement for `generic-pool` ([#815](https://github.com/cap-js/cds-dbs/issues/815)) ([32c08f4](https://github.com/cap-js/cds-dbs/commit/32c08f474eab67c71dad3b5e6a378f8bc52520b0))
|
|
21
|
+
|
|
7
22
|
## [2.0.1](https://github.com/cap-js/cds-dbs/compare/db-service-v2.0.0...db-service-v2.0.1) (2025-05-27)
|
|
8
23
|
|
|
9
24
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
const
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
const LOG = cds.log('db')
|
|
3
|
+
|
|
4
|
+
const use_new_pool = cds.requires.db?.pool?.builtin || cds.env.features.pool === 'builtin'
|
|
5
|
+
const createPool = use_new_pool ? (...args) => new Pool(...args) : require('generic-pool').createPool
|
|
2
6
|
|
|
3
7
|
function ConnectionPool (factory, tenant) {
|
|
4
8
|
let bound_factory = { __proto__: factory, create: factory.create.bind(null, tenant) }
|
|
@@ -29,4 +33,270 @@ function TrackedConnectionPool (factory, tenant) {
|
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
const DEBUG = /\bpool\b/.test(process.env.DEBUG)
|
|
32
|
-
module.exports = DEBUG ? TrackedConnectionPool : ConnectionPool
|
|
36
|
+
module.exports = DEBUG && !use_new_pool ? TrackedConnectionPool : ConnectionPool
|
|
37
|
+
|
|
38
|
+
// Drop-in replacement for https://github.com/coopernurse/node-pool
|
|
39
|
+
// TODO: fifo: true? relevant for our use case?
|
|
40
|
+
|
|
41
|
+
const { EventEmitter } = require('events')
|
|
42
|
+
|
|
43
|
+
const ResourceState = Object.freeze({
|
|
44
|
+
ALLOCATED: 'allocated',
|
|
45
|
+
IDLE: 'idle',
|
|
46
|
+
INVALID: 'invalid',
|
|
47
|
+
VALIDATION: 'validation'
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const RequestState = Object.freeze({
|
|
51
|
+
PENDING: 'pending',
|
|
52
|
+
RESOLVED: 'resolved',
|
|
53
|
+
REJECTED: 'rejected'
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
class Request {
|
|
57
|
+
constructor (ttl) {
|
|
58
|
+
this.state = RequestState.PENDING
|
|
59
|
+
this.promise = new Promise((resolve, reject) => {
|
|
60
|
+
this._resolve = value => {
|
|
61
|
+
clearTimeout(this._timeout)
|
|
62
|
+
this.state = RequestState.RESOLVED
|
|
63
|
+
resolve(value)
|
|
64
|
+
}
|
|
65
|
+
this._reject = reason => {
|
|
66
|
+
clearTimeout(this._timeout)
|
|
67
|
+
this.state = RequestState.REJECTED
|
|
68
|
+
reject(reason)
|
|
69
|
+
}
|
|
70
|
+
if (typeof ttl === 'number' && ttl >= 0) {
|
|
71
|
+
const err = new Error(`Pool resource could not be acquired within ${ttl / 1000}s`)
|
|
72
|
+
this._timeout = setTimeout(() => this._reject(err), ttl).unref()
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
resolve (v) { if (this.state === RequestState.PENDING) this._resolve(v) }
|
|
77
|
+
reject (e) { if (this.state === RequestState.PENDING) this._reject(e) }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class PooledResource {
|
|
81
|
+
constructor(resource) {
|
|
82
|
+
this.obj = resource
|
|
83
|
+
this.creationTime = Date.now()
|
|
84
|
+
this.idle()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
update(newState) {
|
|
88
|
+
this.state = newState
|
|
89
|
+
}
|
|
90
|
+
idle() {
|
|
91
|
+
this.state = ResourceState.IDLE
|
|
92
|
+
this.lastIdleTime = Date.now()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class Pool extends EventEmitter {
|
|
97
|
+
|
|
98
|
+
constructor (factory, options = {}) {
|
|
99
|
+
super()
|
|
100
|
+
this.factory = factory
|
|
101
|
+
|
|
102
|
+
this.options = Object.assign({
|
|
103
|
+
testOnBorrow: false,
|
|
104
|
+
evictionRunIntervalMillis: 100000,
|
|
105
|
+
numTestsPerEvictionRun: 3,
|
|
106
|
+
softIdleTimeoutMillis: -1,
|
|
107
|
+
idleTimeoutMillis: 30000,
|
|
108
|
+
acquireTimeoutMillis: null,
|
|
109
|
+
destroyTimeoutMillis: null,
|
|
110
|
+
fifo: false,
|
|
111
|
+
min: 0,
|
|
112
|
+
max: 10
|
|
113
|
+
}, options)
|
|
114
|
+
|
|
115
|
+
/** @type {boolean} */
|
|
116
|
+
this._draining = false
|
|
117
|
+
|
|
118
|
+
/** @type {Set<PooledResource>} */
|
|
119
|
+
this._available = new Set()
|
|
120
|
+
|
|
121
|
+
/** @type {Map<any, { pooledResource: PooledResource }>} */
|
|
122
|
+
this._loans = new Map()
|
|
123
|
+
|
|
124
|
+
/** @type {Set<PooledResource>} */
|
|
125
|
+
this._all = new Set()
|
|
126
|
+
|
|
127
|
+
/** @type {Set<Promise<void>>} */
|
|
128
|
+
this._creates = new Set()
|
|
129
|
+
|
|
130
|
+
/** @type {Request[]} */
|
|
131
|
+
this._queue = []
|
|
132
|
+
|
|
133
|
+
this.#scheduleEviction()
|
|
134
|
+
|
|
135
|
+
const initial = this.options.min - this.size
|
|
136
|
+
for (let i = 0; i < initial; i++) this.#createResource()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async acquire() {
|
|
140
|
+
if (this._draining) throw new Error('Pool is draining and cannot accept new requests')
|
|
141
|
+
const request = new Request(this.options.acquireTimeoutMillis)
|
|
142
|
+
this._queue.push(request)
|
|
143
|
+
this.#dispense()
|
|
144
|
+
return request.promise
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async release(resource) {
|
|
148
|
+
const loan = this._loans.get(resource)
|
|
149
|
+
if (!loan) throw new Error('Resource not currently part of this pool')
|
|
150
|
+
this._loans.delete(resource)
|
|
151
|
+
const pooledResource = loan.pooledResource
|
|
152
|
+
pooledResource.idle()
|
|
153
|
+
this._available.add(pooledResource)
|
|
154
|
+
this.#dispense()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async destroy(resource) {
|
|
158
|
+
const loan = this._loans.get(resource)
|
|
159
|
+
if (!loan) throw new Error('Resource not currently part of this pool')
|
|
160
|
+
this._loans.delete(resource)
|
|
161
|
+
const pooledResource = loan.pooledResource
|
|
162
|
+
await this.#destroy(pooledResource)
|
|
163
|
+
this.#dispense()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async drain() {
|
|
167
|
+
this._draining = true
|
|
168
|
+
for (const request of this._queue.splice(0)) {
|
|
169
|
+
if (request.state === RequestState.PENDING) request.reject(new Error('Pool is draining and cannot fulfil request'))
|
|
170
|
+
}
|
|
171
|
+
clearTimeout(this._scheduledEviction)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async clear() {
|
|
175
|
+
await Promise.allSettled(Array.from(this._creates))
|
|
176
|
+
await Promise.allSettled(Array.from(this._all).map(resource => this.#destroy(resource)))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async #createResource() {
|
|
180
|
+
const createPromise = (async () => {
|
|
181
|
+
try {
|
|
182
|
+
const resource = new PooledResource(await this.factory.create())
|
|
183
|
+
this._all.add(resource)
|
|
184
|
+
this._available.add(resource)
|
|
185
|
+
} catch (err) {
|
|
186
|
+
this._queue.shift()?.reject(err)
|
|
187
|
+
}
|
|
188
|
+
})()
|
|
189
|
+
this._creates.add(createPromise)
|
|
190
|
+
createPromise.finally(() => {
|
|
191
|
+
this._creates.delete(createPromise)
|
|
192
|
+
this.#dispense()
|
|
193
|
+
})
|
|
194
|
+
return createPromise
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async #dispense() {
|
|
198
|
+
const waiting = this._queue.length
|
|
199
|
+
if (waiting === 0) return
|
|
200
|
+
const capacity = this._available.size + this._creates.size
|
|
201
|
+
const shortfall = waiting - capacity
|
|
202
|
+
if (shortfall > 0 && this.size < this.options.max) {
|
|
203
|
+
const needed = Math.min(shortfall, this.options.max - this.size)
|
|
204
|
+
for (let i = 0; i < needed; i++) this.#createResource()
|
|
205
|
+
}
|
|
206
|
+
const dispense = async resource => {
|
|
207
|
+
const request = this._queue.shift()
|
|
208
|
+
if (!request) {
|
|
209
|
+
resource.idle()
|
|
210
|
+
this._available.add(resource)
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
if (request.state !== RequestState.PENDING) {
|
|
214
|
+
this.#dispense()
|
|
215
|
+
return false
|
|
216
|
+
}
|
|
217
|
+
this._loans.set(resource.obj, { pooledResource: resource })
|
|
218
|
+
resource.update(ResourceState.ALLOCATED)
|
|
219
|
+
request.resolve(resource.obj)
|
|
220
|
+
return true
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const _dispenses = []
|
|
224
|
+
for (let i = 0; i < Math.min(this._available.size, waiting); i++) {
|
|
225
|
+
const resource = this._available.values().next().value
|
|
226
|
+
this._available.delete(resource)
|
|
227
|
+
if (this.options.testOnBorrow) {
|
|
228
|
+
const validationPromise = (async () => {
|
|
229
|
+
resource.update(ResourceState.VALIDATION)
|
|
230
|
+
try {
|
|
231
|
+
const isValid = await this.factory.validate(resource.obj)
|
|
232
|
+
if (isValid) return dispense(resource)
|
|
233
|
+
} catch {/* marked as invalid below */}
|
|
234
|
+
resource.update(ResourceState.INVALID)
|
|
235
|
+
await this.#destroy(resource)
|
|
236
|
+
this.#dispense()
|
|
237
|
+
return false
|
|
238
|
+
})()
|
|
239
|
+
_dispenses.push(validationPromise)
|
|
240
|
+
} else {
|
|
241
|
+
_dispenses.push(dispense(resource))
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
await Promise.all(_dispenses)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async #destroy(resource) {
|
|
248
|
+
resource.update(ResourceState.INVALID)
|
|
249
|
+
this._all.delete(resource)
|
|
250
|
+
this._available.delete(resource)
|
|
251
|
+
this._loans.delete(resource.obj)
|
|
252
|
+
try {
|
|
253
|
+
const destroyPromise = Promise.resolve(this.factory.destroy(resource.obj))
|
|
254
|
+
const { destroyTimeoutMillis } = this.options
|
|
255
|
+
if (destroyTimeoutMillis && destroyTimeoutMillis > 0) {
|
|
256
|
+
const timeout = new Promise((_, reject) =>
|
|
257
|
+
setTimeout(() => reject(new Error(`Resource destruction timed out after ${destroyTimeoutMillis}ms`)), destroyTimeoutMillis).unref()
|
|
258
|
+
)
|
|
259
|
+
await Promise.race([destroyPromise, timeout])
|
|
260
|
+
} else {
|
|
261
|
+
await destroyPromise
|
|
262
|
+
}
|
|
263
|
+
} catch (e) {
|
|
264
|
+
LOG.error(e)
|
|
265
|
+
/* FIXME: We have to ignore errors here due to a TypeError in hdb */
|
|
266
|
+
/* This was also a problem with the old (generic-pool) implementation */
|
|
267
|
+
/* Root cause in hdb needs to be fixed */
|
|
268
|
+
} finally {
|
|
269
|
+
if (!this._draining && this.size < this.options.min) this.#createResource()
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#scheduleEviction() {
|
|
274
|
+
const { evictionRunIntervalMillis, numTestsPerEvictionRun, softIdleTimeoutMillis, min, idleTimeoutMillis } = this.options
|
|
275
|
+
if (evictionRunIntervalMillis <= 0) return
|
|
276
|
+
this._scheduledEviction = setTimeout(async () => {
|
|
277
|
+
try {
|
|
278
|
+
const evictionCandidates = Array.from(this._available).slice(0, numTestsPerEvictionRun)
|
|
279
|
+
const destructionPromises = []
|
|
280
|
+
for (const resource of evictionCandidates) {
|
|
281
|
+
const idleTime = Date.now() - resource.lastIdleTime
|
|
282
|
+
const softEvict = softIdleTimeoutMillis > 0 && softIdleTimeoutMillis < idleTime && this._all.size > min
|
|
283
|
+
const hardEvict = idleTimeoutMillis < idleTime
|
|
284
|
+
if (softEvict || hardEvict) {
|
|
285
|
+
if (this._available.delete(resource)) {
|
|
286
|
+
destructionPromises.push(this.#destroy(resource))
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
await Promise.all(destructionPromises)
|
|
291
|
+
} finally {
|
|
292
|
+
this.#scheduleEviction()
|
|
293
|
+
}
|
|
294
|
+
}, evictionRunIntervalMillis).unref()
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
get size() { return this._all.size + this._creates.size }
|
|
298
|
+
get available() { return this._available.size }
|
|
299
|
+
get borrowed() { return this._loans.size }
|
|
300
|
+
get pending() { return this._queue.length }
|
|
301
|
+
get tenant() { return this.options.tenant }
|
|
302
|
+
}
|
package/package.json
CHANGED