@cap-js/db-service 2.0.1 → 2.1.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.
- package/CHANGELOG.md +7 -0
- package/lib/common/generic-pool.js +260 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
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.0](https://github.com/cap-js/cds-dbs/compare/db-service-v2.0.1...db-service-v2.1.0) (2025-06-04)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* 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))
|
|
13
|
+
|
|
7
14
|
## [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
15
|
|
|
9
16
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
const
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
const LOG = cds.log('db')
|
|
3
|
+
|
|
4
|
+
const createPool = (factory, config) => {
|
|
5
|
+
if (cds.requires.db?.pool?.builtin) return new Pool(factory, config)
|
|
6
|
+
return require('generic-pool').createPool(factory, config)
|
|
7
|
+
}
|
|
2
8
|
|
|
3
9
|
function ConnectionPool (factory, tenant) {
|
|
4
10
|
let bound_factory = { __proto__: factory, create: factory.create.bind(null, tenant) }
|
|
@@ -29,4 +35,256 @@ function TrackedConnectionPool (factory, tenant) {
|
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
const DEBUG = /\bpool\b/.test(process.env.DEBUG)
|
|
32
|
-
module.exports = DEBUG ? TrackedConnectionPool : ConnectionPool
|
|
38
|
+
module.exports = DEBUG && !cds.requires.db?.pool?.builtin ? TrackedConnectionPool : ConnectionPool
|
|
39
|
+
|
|
40
|
+
// Drop-in replacement for https://github.com/coopernurse/node-pool
|
|
41
|
+
// TODO: fifo: true? relevant for our use case?
|
|
42
|
+
|
|
43
|
+
const { EventEmitter } = require('events')
|
|
44
|
+
|
|
45
|
+
const ResourceState = Object.freeze({
|
|
46
|
+
ALLOCATED: 'allocated',
|
|
47
|
+
IDLE: 'idle',
|
|
48
|
+
INVALID: 'invalid',
|
|
49
|
+
VALIDATION: 'validation'
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const RequestState = Object.freeze({
|
|
53
|
+
PENDING: 'pending',
|
|
54
|
+
RESOLVED: 'resolved',
|
|
55
|
+
REJECTED: 'rejected'
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
class Request {
|
|
59
|
+
constructor (ttl) {
|
|
60
|
+
this.state = 'pending'
|
|
61
|
+
this.promise = new Promise((resolve, reject) => {
|
|
62
|
+
this._resolve = value => {
|
|
63
|
+
clearTimeout(this._timeout)
|
|
64
|
+
this.state = RequestState.RESOLVED
|
|
65
|
+
resolve(value)
|
|
66
|
+
}
|
|
67
|
+
this._reject = reason => {
|
|
68
|
+
clearTimeout(this._timeout)
|
|
69
|
+
this.state = RequestState.REJECTED
|
|
70
|
+
reject(reason)
|
|
71
|
+
}
|
|
72
|
+
if (typeof ttl === 'number' && ttl >= 0) {
|
|
73
|
+
const err = new Error(`Pool resource could not be acquired within ${ttl / 1000}s`)
|
|
74
|
+
this._timeout = setTimeout(() => this._reject(err), ttl)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
resolve (v) { if (this.state === 'pending') this._resolve(v) }
|
|
79
|
+
reject (e) { if (this.state === 'pending') this._reject(e) }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class PooledResource {
|
|
83
|
+
constructor(resource) {
|
|
84
|
+
this.obj = resource
|
|
85
|
+
this.creationTime = Date.now()
|
|
86
|
+
this.idle()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
update(newState) {
|
|
90
|
+
this.state = newState
|
|
91
|
+
}
|
|
92
|
+
idle() {
|
|
93
|
+
this.state = ResourceState.IDLE
|
|
94
|
+
this.lastIdleTime = Date.now()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class Pool extends EventEmitter {
|
|
99
|
+
|
|
100
|
+
constructor (factory, options = {}) {
|
|
101
|
+
super()
|
|
102
|
+
this.factory = factory
|
|
103
|
+
|
|
104
|
+
this.options = Object.assign({
|
|
105
|
+
testOnBorrow: false,
|
|
106
|
+
evictionRunIntervalMillis: 100000,
|
|
107
|
+
numTestsPerEvictionRun: 3,
|
|
108
|
+
softIdleTimeoutMillis: -1,
|
|
109
|
+
idleTimeoutMillis: 30000,
|
|
110
|
+
acquireTimeoutMillis: null,
|
|
111
|
+
destroyTimeoutMillis: null,
|
|
112
|
+
fifo: false,
|
|
113
|
+
min: 0,
|
|
114
|
+
max: 10
|
|
115
|
+
}, options)
|
|
116
|
+
|
|
117
|
+
/** @type {boolean} */
|
|
118
|
+
this._draining = false
|
|
119
|
+
|
|
120
|
+
/** @type {Set<PooledResource>} */
|
|
121
|
+
this._available = new Set()
|
|
122
|
+
|
|
123
|
+
/** @type {Map<any, { pooledResource: PooledResource }>} */
|
|
124
|
+
this._loans = new Map()
|
|
125
|
+
|
|
126
|
+
/** @type {Set<PooledResource>} */
|
|
127
|
+
this._all = new Set()
|
|
128
|
+
|
|
129
|
+
/** @type {Set<Promise<void>>} */
|
|
130
|
+
this._creates = new Set()
|
|
131
|
+
|
|
132
|
+
/** @type {Request[]} */
|
|
133
|
+
this._queue = []
|
|
134
|
+
|
|
135
|
+
this.#scheduleEviction()
|
|
136
|
+
for (let i = 0; i < this.options.min - this.size; 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._available).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
|
+
await this.factory.destroy(resource.obj)
|
|
254
|
+
} catch (e) {
|
|
255
|
+
LOG.error(e)
|
|
256
|
+
/* FIXME: We have to ignore errors here due to a TypeError in hdb */
|
|
257
|
+
/* This was also a problem with the old (generic-pool) implementation */
|
|
258
|
+
/* Root cause in hdb needs to be fixed */
|
|
259
|
+
} finally {
|
|
260
|
+
if (!this._draining && this.size < this.options.min) {
|
|
261
|
+
await this.#createResource()
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#scheduleEviction() {
|
|
267
|
+
const { evictionRunIntervalMillis, numTestsPerEvictionRun, softIdleTimeoutMillis, min, idleTimeoutMillis } = this.options
|
|
268
|
+
if (evictionRunIntervalMillis <= 0) return
|
|
269
|
+
this._scheduledEviction = setTimeout(async () => {
|
|
270
|
+
try {
|
|
271
|
+
const resourcesToEvict = Array.from(this._available)
|
|
272
|
+
.slice(0, numTestsPerEvictionRun)
|
|
273
|
+
.filter(resource => {
|
|
274
|
+
const idleTime = Date.now() - resource.lastIdleTime
|
|
275
|
+
const softEvict = softIdleTimeoutMillis > 0 && softIdleTimeoutMillis < idleTime && min < this._available.size
|
|
276
|
+
return softEvict || idleTimeoutMillis < idleTime
|
|
277
|
+
})
|
|
278
|
+
await Promise.all(resourcesToEvict.map(resource => this.#destroy(resource)))
|
|
279
|
+
} finally {
|
|
280
|
+
this.#scheduleEviction()
|
|
281
|
+
}
|
|
282
|
+
}, evictionRunIntervalMillis).unref()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
get size() { return this._all.size + this._creates.size }
|
|
286
|
+
get available() { return this._available.size }
|
|
287
|
+
get borrowed() { return this._loans.size }
|
|
288
|
+
get pending() { return this._queue.length }
|
|
289
|
+
get tenant() { return this.options.tenant }
|
|
290
|
+
}
|
package/package.json
CHANGED