Pool.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. const PendingOperation_1 = require("./PendingOperation");
  4. const Resource_1 = require("./Resource");
  5. const utils_1 = require("./utils");
  6. class Pool {
  7. constructor(opt) {
  8. this.destroyed = false;
  9. opt = opt || {};
  10. if (!opt.create) {
  11. throw new Error('Tarn: opt.create function most be provided');
  12. }
  13. if (!opt.destroy) {
  14. throw new Error('Tarn: opt.destroy function most be provided');
  15. }
  16. if (typeof opt.min !== 'number' || opt.min < 0 || opt.min !== Math.round(opt.min)) {
  17. throw new Error('Tarn: opt.min must be an integer >= 0');
  18. }
  19. if (typeof opt.max !== 'number' || opt.max <= 0 || opt.max !== Math.round(opt.max)) {
  20. throw new Error('Tarn: opt.max must be an integer > 0');
  21. }
  22. if (opt.min > opt.max) {
  23. throw new Error('Tarn: opt.max is smaller than opt.min');
  24. }
  25. if (!utils_1.checkOptionalTime(opt.acquireTimeoutMillis)) {
  26. throw new Error('Tarn: invalid opt.acquireTimeoutMillis ' + JSON.stringify(opt.acquireTimeoutMillis));
  27. }
  28. if (!utils_1.checkOptionalTime(opt.createTimeoutMillis)) {
  29. throw new Error('Tarn: invalid opt.createTimeoutMillis ' + JSON.stringify(opt.createTimeoutMillis));
  30. }
  31. if (!utils_1.checkOptionalTime(opt.destroyTimeoutMillis)) {
  32. throw new Error('Tarn: invalid opt.destroyTimeoutMillis ' + JSON.stringify(opt.destroyTimeoutMillis));
  33. }
  34. if (!utils_1.checkOptionalTime(opt.idleTimeoutMillis)) {
  35. throw new Error('Tarn: invalid opt.idleTimeoutMillis ' + JSON.stringify(opt.idleTimeoutMillis));
  36. }
  37. if (!utils_1.checkOptionalTime(opt.reapIntervalMillis)) {
  38. throw new Error('Tarn: invalid opt.reapIntervalMillis ' + JSON.stringify(opt.reapIntervalMillis));
  39. }
  40. if (!utils_1.checkOptionalTime(opt.createRetryIntervalMillis)) {
  41. throw new Error('Tarn: invalid opt.createRetryIntervalMillis ' +
  42. JSON.stringify(opt.createRetryIntervalMillis));
  43. }
  44. this.creator = opt.create;
  45. this.destroyer = opt.destroy;
  46. this.validate = typeof opt.validate === 'function' ? opt.validate : () => true;
  47. this.log = opt.log || (() => { });
  48. this.acquireTimeoutMillis = opt.acquireTimeoutMillis || 30000;
  49. this.createTimeoutMillis = opt.createTimeoutMillis || 30000;
  50. this.destroyTimeoutMillis = opt.destroyTimeoutMillis || 5000;
  51. this.idleTimeoutMillis = opt.idleTimeoutMillis || 30000;
  52. this.reapIntervalMillis = opt.reapIntervalMillis || 1000;
  53. this.createRetryIntervalMillis = opt.createRetryIntervalMillis || 200;
  54. this.propagateCreateError = !!opt.propagateCreateError;
  55. this.min = opt.min;
  56. this.max = opt.max;
  57. this.used = [];
  58. this.free = [];
  59. this.pendingCreates = [];
  60. this.pendingAcquires = [];
  61. this.destroyed = false;
  62. this.interval = null;
  63. }
  64. numUsed() {
  65. return this.used.length;
  66. }
  67. numFree() {
  68. return this.free.length;
  69. }
  70. numPendingAcquires() {
  71. return this.pendingAcquires.length;
  72. }
  73. numPendingCreates() {
  74. return this.pendingCreates.length;
  75. }
  76. acquire() {
  77. const pendingAcquire = new PendingOperation_1.PendingOperation(this.acquireTimeoutMillis);
  78. this.pendingAcquires.push(pendingAcquire);
  79. // If the acquire fails for whatever reason
  80. // remove it from the pending queue.
  81. pendingAcquire.promise = pendingAcquire.promise.catch(err => {
  82. remove(this.pendingAcquires, pendingAcquire);
  83. return Promise.reject(err);
  84. });
  85. this._tryAcquireOrCreate();
  86. return pendingAcquire;
  87. }
  88. release(resource) {
  89. for (let i = 0, l = this.used.length; i < l; ++i) {
  90. const used = this.used[i];
  91. if (used.resource === resource) {
  92. this.used.splice(i, 1);
  93. this.free.push(used.resolve());
  94. this._tryAcquireOrCreate();
  95. return true;
  96. }
  97. }
  98. return false;
  99. }
  100. isEmpty() {
  101. return ([this.numFree(), this.numUsed(), this.numPendingAcquires(), this.numPendingCreates()].reduce((total, value) => total + value) === 0);
  102. }
  103. check() {
  104. const timestamp = utils_1.now();
  105. const newFree = [];
  106. const minKeep = this.min - this.used.length;
  107. const maxDestroy = this.free.length - minKeep;
  108. let numDestroyed = 0;
  109. this.free.forEach(free => {
  110. if (utils_1.duration(timestamp, free.timestamp) > this.idleTimeoutMillis &&
  111. numDestroyed < maxDestroy) {
  112. numDestroyed++;
  113. this._destroy(free.resource);
  114. }
  115. else {
  116. newFree.push(free);
  117. }
  118. });
  119. this.free = newFree;
  120. // Pool is completely empty, stop reaping.
  121. // Next .acquire will start reaping interval again.
  122. if (this.isEmpty()) {
  123. this._stopReaping();
  124. }
  125. }
  126. destroy() {
  127. this._stopReaping();
  128. this.destroyed = true;
  129. // First wait for all the pending creates get ready.
  130. return utils_1.reflect(Promise.all(this.pendingCreates.map(create => utils_1.reflect(create.promise)))
  131. .then(() => {
  132. // Wait for all the used resources to be freed.
  133. return Promise.all(this.used.map(used => utils_1.reflect(used.promise)));
  134. })
  135. .then(() => {
  136. // Abort all pending acquires.
  137. return Promise.all(this.pendingAcquires.map(acquire => {
  138. acquire.abort();
  139. return utils_1.reflect(acquire.promise);
  140. }));
  141. })
  142. .then(() => {
  143. // Now we can destroy all the freed resources.
  144. return Promise.all(this.free.map(free => utils_1.reflect(this._destroy(free.resource))));
  145. })
  146. .then(() => {
  147. this.free = [];
  148. this.pendingAcquires = [];
  149. }));
  150. }
  151. _tryAcquireOrCreate() {
  152. if (this.destroyed) {
  153. return;
  154. }
  155. if (this._hasFreeResources()) {
  156. this._doAcquire();
  157. }
  158. else if (this._shouldCreateMoreResources()) {
  159. this._doCreate();
  160. }
  161. }
  162. _hasFreeResources() {
  163. return this.free.length > 0;
  164. }
  165. _doAcquire() {
  166. let didDestroyResources = false;
  167. while (this._canAcquire()) {
  168. const pendingAcquire = this.pendingAcquires[0];
  169. const free = this.free[this.free.length - 1];
  170. if (!this._validateResource(free.resource)) {
  171. this.free.pop();
  172. this._destroy(free.resource);
  173. didDestroyResources = true;
  174. continue;
  175. }
  176. this.pendingAcquires.shift();
  177. this.free.pop();
  178. this.used.push(free.resolve());
  179. //At least one active resource, start reaping
  180. this._startReaping();
  181. pendingAcquire.resolve(free.resource);
  182. }
  183. // If we destroyed invalid resources, we may need to create new ones.
  184. if (didDestroyResources) {
  185. this._tryAcquireOrCreate();
  186. }
  187. }
  188. _canAcquire() {
  189. return this.free.length > 0 && this.pendingAcquires.length > 0;
  190. }
  191. _validateResource(resource) {
  192. try {
  193. return !!this.validate(resource);
  194. }
  195. catch (err) {
  196. // There's nothing we can do here but log the error. This would otherwise
  197. // leak out as an unhandled exception.
  198. this.log('Tarn: resource validator threw an exception ' + err.stack, 'warn');
  199. return false;
  200. }
  201. }
  202. _shouldCreateMoreResources() {
  203. return (this.used.length + this.pendingCreates.length < this.max &&
  204. this.pendingCreates.length < this.pendingAcquires.length);
  205. }
  206. _doCreate() {
  207. const pendingAcquiresBeforeCreate = this.pendingAcquires.slice();
  208. const pendingCreate = this._create();
  209. pendingCreate.promise
  210. .then(() => {
  211. // Not returned on purpose.
  212. this._tryAcquireOrCreate();
  213. return null;
  214. })
  215. .catch(err => {
  216. if (this.propagateCreateError && this.pendingAcquires.length !== 0) {
  217. // If propagateCreateError is true, we don't retry the create
  218. // but reject the first pending acquire immediately. Intentionally
  219. // use `this.pendingAcquires` instead of `pendingAcquiresBeforeCreate`
  220. // in case some acquires in pendingAcquiresBeforeCreate have already
  221. // been resolved.
  222. this.pendingAcquires[0].reject(err);
  223. }
  224. // Save the create error to all pending acquires so that we can use it
  225. // as the error to reject the acquire if it times out.
  226. pendingAcquiresBeforeCreate.forEach(pendingAcquire => {
  227. pendingAcquire.possibleTimeoutCause = err;
  228. });
  229. // Not returned on purpose.
  230. utils_1.delay(this.createRetryIntervalMillis).then(() => this._tryAcquireOrCreate());
  231. });
  232. }
  233. _create() {
  234. const pendingCreate = new PendingOperation_1.PendingOperation(this.createTimeoutMillis);
  235. this.pendingCreates.push(pendingCreate);
  236. callbackOrPromise(this.creator)
  237. .then(resource => {
  238. remove(this.pendingCreates, pendingCreate);
  239. this.free.push(new Resource_1.Resource(resource));
  240. // Not returned on purpose.
  241. pendingCreate.resolve(resource);
  242. return null;
  243. })
  244. .catch(err => {
  245. remove(this.pendingCreates, pendingCreate);
  246. // Not returned on purpose.
  247. pendingCreate.reject(err);
  248. return null;
  249. });
  250. return pendingCreate;
  251. }
  252. _destroy(resource) {
  253. try {
  254. // this.destroyer can be both synchronous and asynchronous.
  255. // When it's synchronous, errors are handled by the try/catch
  256. // When it's asynchronous, errors are handled by .catch()
  257. const retVal = this.destroyer(resource);
  258. if (retVal && retVal.then && retVal.catch) {
  259. const pendingDestroy = new PendingOperation_1.PendingOperation(this.destroyTimeoutMillis);
  260. retVal
  261. .then(() => {
  262. pendingDestroy.resolve(resource);
  263. })
  264. .catch((err) => {
  265. pendingDestroy.reject(err);
  266. });
  267. // In case of an error there's nothing we can do here but log it.
  268. return pendingDestroy.promise.catch(err => this._logError(err));
  269. }
  270. return Promise.resolve(retVal);
  271. }
  272. catch (err) {
  273. // There's nothing we can do here but log the error. This would otherwise
  274. // leak out as an unhandled exception.
  275. this._logError(err);
  276. return Promise.resolve();
  277. }
  278. }
  279. _logError(err) {
  280. this.log('Tarn: resource destroyer threw an exception ' + err.stack, 'warn');
  281. }
  282. _startReaping() {
  283. if (!this.interval) {
  284. this.interval = setInterval(() => this.check(), this.reapIntervalMillis);
  285. }
  286. }
  287. _stopReaping() {
  288. if (this.interval !== null) {
  289. clearInterval(this.interval);
  290. }
  291. this.interval = null;
  292. }
  293. }
  294. exports.Pool = Pool;
  295. function remove(arr, item) {
  296. var idx = arr.indexOf(item);
  297. if (idx === -1) {
  298. return false;
  299. }
  300. else {
  301. arr.splice(idx, 1);
  302. return true;
  303. }
  304. }
  305. function callbackOrPromise(func) {
  306. return new Promise((resolve, reject) => {
  307. const callback = (err, resource) => {
  308. if (err) {
  309. reject(err);
  310. }
  311. else {
  312. resolve(resource);
  313. }
  314. };
  315. utils_1.tryPromise(() => func(callback))
  316. .then(res => {
  317. // If the result is falsy, we assume that the callback will
  318. // be called instead of interpreting the falsy value as a
  319. // result value.
  320. if (res) {
  321. resolve(res);
  322. }
  323. })
  324. .catch(err => {
  325. reject(err);
  326. });
  327. });
  328. }