@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 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 { createPool } = require('generic-pool')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "2.0.1",
3
+ "version": "2.1.1",
4
4
  "description": "CDS base database service",
5
5
  "homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service",
6
6
  "repository": {