Skip to content

Commit c81a368

Browse files
author
Alexander Dmitrjuk
committed
First commit
0 parents  commit c81a368

File tree

12 files changed

+601
-0
lines changed

12 files changed

+601
-0
lines changed

.github/workflows/tests.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- '*'
7+
- '!stable/**'
8+
9+
jobs:
10+
build:
11+
timeout-minutes: 7
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- uses: actions/checkout@v2
16+
- uses: actions/setup-node@v1
17+
with:
18+
node-version: '15'
19+
- name: Install npm dependencies
20+
run: npm ci
21+
- name: Run docker-compose
22+
run: docker-compose up -d
23+
- name: Run tests
24+
run: docker-compose exec -T node npm run test

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package-lock.json
2+
.idea
3+
node_modules

.mocharc.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extension": ["ts"],
3+
"exit": true,
4+
"recursive": true,
5+
"require": ["ts-node/register"],
6+
"inspect": "0.0.0.0:9231"
7+
}

.npmignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
test
2+
tsconfig.json
3+
.mocharc.json
4+
LICENCE
5+
node_modules
6+
.gitignore
7+
docker-compose.yml
8+
package-lock.json

LICENCE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021 Alexander Dmitryuk
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Redis functions balancer
2+
[![NPM](https://nodei.co/coden/redis-functions-balancer.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/coden/redis-functions-balancer)
3+
4+
Balance executable NodeJs function with redis.
5+
6+
For example, if you have several functions (A, B, C) doing the same things (http requests, long-running code), and you want to execute it evenly.
7+
8+
Working in clusters (PM2, NodeJs Cluster).
9+
10+
Uses [Redis][0] list with rank and [Javascript iterators][1].
11+
12+
Ready to use with TypeScript and JavaScript.
13+
14+
## Installation
15+
```
16+
npm install @coden/redis-functions-balancer --save-prod
17+
```
18+
19+
## Usage
20+
```typescript
21+
import CallableBalancer from "@coden/redis-functions-balancer";
22+
const redis = require("redis");
23+
const redisClient = redis.createClient(6379, 'redis');
24+
25+
// Your functions here
26+
// ... //
27+
const A = () => {};
28+
const B = () => {};
29+
const C = () => {};
30+
// ... //
31+
let balancer = new CallableBalancer([A, B, C], redisClient);
32+
// or reuse balancer variable with another functions
33+
balancer.setMethods([A, B]);
34+
// ... //
35+
// Get async iterator {done, value}
36+
while ( (foo = await balancer.getAsyncIterator().next()) && !foo.done) {
37+
// Your function A|B|C will be here evenly
38+
let method = foo.value;
39+
40+
try {
41+
// Executing on your way (
42+
foo.value();
43+
} catch (e) {
44+
// something happen badly and you want to postpone executes of the function next 10 runs
45+
balancer.increaseMethodRank(method, 10);
46+
}
47+
}
48+
49+
```
50+
51+
[0]: https://www.npmjs.com/package/redis
52+
[1]: https://www.typescriptlang.org/docs/handbook/iterators-and-generators.html

docker-compose.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
version: "3.5"
2+
services:
3+
redis:
4+
image: redis:alpine
5+
depends_on:
6+
- node
7+
node:
8+
image: node:latest
9+
volumes:
10+
- .:/app
11+
working_dir: /app
12+
entrypoint: "sleep 100"
13+
ports:
14+
- 9231:9231

index.js

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
"use strict";
2+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4+
return new (P || (P = Promise))(function (resolve, reject) {
5+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8+
step((generator = generator.apply(thisArg, _arguments || [])).next());
9+
});
10+
};
11+
var __generator = (this && this.__generator) || function (thisArg, body) {
12+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14+
function verb(n) { return function (v) { return step([n, v]); }; }
15+
function step(op) {
16+
if (f) throw new TypeError("Generator is already executing.");
17+
while (_) try {
18+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19+
if (y = 0, t) op = [op[0] & 2, t.value];
20+
switch (op[0]) {
21+
case 0: case 1: t = op; break;
22+
case 4: _.label++; return { value: op[1], done: false };
23+
case 5: _.label++; y = op[1]; op = [0]; continue;
24+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
25+
default:
26+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30+
if (t[2]) _.ops.pop();
31+
_.trys.pop(); continue;
32+
}
33+
op = body.call(thisArg, _);
34+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36+
}
37+
};
38+
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
39+
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
40+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
41+
var g = generator.apply(thisArg, _arguments || []), i, q = [];
42+
return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i;
43+
function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }
44+
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
45+
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
46+
function fulfill(value) { resume("next", value); }
47+
function reject(value) { resume("throw", value); }
48+
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
49+
};
50+
var __spreadArrays = (this && this.__spreadArrays) || function () {
51+
for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
52+
for (var r = Array(s), k = 0, i = 0; i < il; i++)
53+
for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
54+
r[k] = a[j];
55+
return r;
56+
};
57+
Object.defineProperty(exports, "__esModule", { value: true });
58+
var util_1 = require("util");
59+
var CallableBalancer = /** @class */ (function () {
60+
/**
61+
*
62+
* @param methods not empty array of functions
63+
* @param redisClient
64+
*/
65+
function CallableBalancer(methods, redisClient) {
66+
this._STORE_PREFIX = 'balancer';
67+
this.INC_VALUE = 1;
68+
this._redisClient = redisClient;
69+
this._methods = methods;
70+
this._storeKey = this.makeStoreKey(methods);
71+
// Initialize Redis functions as async await
72+
this._functions = {
73+
delAsync: util_1.promisify(redisClient.DEL).bind(this._redisClient),
74+
zAddAsync: util_1.promisify(redisClient.ZADD).bind(this._redisClient),
75+
zRangeAsync: util_1.promisify(redisClient.zrange).bind(this._redisClient),
76+
zIncRbyAsync: util_1.promisify(redisClient.zincrby).bind(this._redisClient),
77+
};
78+
}
79+
CallableBalancer.prototype.setMethods = function (methods) {
80+
this._methods = methods;
81+
this._storeKey = this.makeStoreKey(methods);
82+
};
83+
CallableBalancer.prototype.increaseMethodRank = function (method, incValue) {
84+
if (incValue === void 0) { incValue = this.INC_VALUE; }
85+
return __awaiter(this, void 0, void 0, function () {
86+
return __generator(this, function (_a) {
87+
switch (_a.label) {
88+
case 0: return [4 /*yield*/, this._functions.zIncRbyAsync(this._storeKey, incValue, method.name)];
89+
case 1:
90+
_a.sent();
91+
return [2 /*return*/];
92+
}
93+
});
94+
});
95+
};
96+
CallableBalancer.prototype.getAsyncIterator = function () {
97+
return __asyncGenerator(this, arguments, function getAsyncIterator_1() {
98+
var storedMethodNames, _i, storedMethodNames_1, methodName, _a, _b, method;
99+
return __generator(this, function (_c) {
100+
switch (_c.label) {
101+
case 0: return [4 /*yield*/, __await(this.getRange())];
102+
case 1:
103+
storedMethodNames = _c.sent();
104+
_i = 0, storedMethodNames_1 = storedMethodNames;
105+
_c.label = 2;
106+
case 2:
107+
if (!(_i < storedMethodNames_1.length)) return [3 /*break*/, 9];
108+
methodName = storedMethodNames_1[_i];
109+
_a = 0, _b = this._methods;
110+
_c.label = 3;
111+
case 3:
112+
if (!(_a < _b.length)) return [3 /*break*/, 8];
113+
method = _b[_a];
114+
if (!(method.name === methodName)) return [3 /*break*/, 7];
115+
return [4 /*yield*/, __await(this.increaseMethodRank(method, this.INC_VALUE))];
116+
case 4:
117+
_c.sent();
118+
return [4 /*yield*/, __await(method)];
119+
case 5: return [4 /*yield*/, _c.sent()];
120+
case 6:
121+
_c.sent();
122+
_c.label = 7;
123+
case 7:
124+
_a++;
125+
return [3 /*break*/, 3];
126+
case 8:
127+
_i++;
128+
return [3 /*break*/, 2];
129+
case 9: return [2 /*return*/];
130+
}
131+
});
132+
});
133+
};
134+
/**
135+
* Clear store
136+
*/
137+
CallableBalancer.prototype.resetStore = function () {
138+
return __awaiter(this, void 0, void 0, function () {
139+
return __generator(this, function (_a) {
140+
switch (_a.label) {
141+
case 0: return [4 /*yield*/, this._functions.delAsync(this._storeKey)];
142+
case 1:
143+
_a.sent();
144+
return [2 /*return*/];
145+
}
146+
});
147+
});
148+
};
149+
CallableBalancer.prototype.getStoreKey = function () {
150+
return this._storeKey;
151+
};
152+
/**
153+
* Return redis key to store list of methods with ranks
154+
* @param methods
155+
* @protected
156+
*/
157+
CallableBalancer.prototype.makeStoreKey = function (methods) {
158+
var storeKeyArray = [this._STORE_PREFIX];
159+
methods.forEach(function (method) {
160+
storeKeyArray.push(method.name);
161+
});
162+
return storeKeyArray.join('.');
163+
};
164+
/**
165+
* Returns an Array stored in Redis in Rank order
166+
* @private
167+
*/
168+
CallableBalancer.prototype.getRange = function () {
169+
return __awaiter(this, void 0, void 0, function () {
170+
var storedMethodNames, args_1, result_1;
171+
var _a;
172+
return __generator(this, function (_b) {
173+
switch (_b.label) {
174+
case 0: return [4 /*yield*/, this._functions.zRangeAsync(this._storeKey, 0, -1)];
175+
case 1:
176+
storedMethodNames = _b.sent();
177+
if (!(storedMethodNames.length !== this._methods.length)) return [3 /*break*/, 3];
178+
args_1 = [], result_1 = [];
179+
this._methods.forEach(function (method) {
180+
// Default rank is 1
181+
args_1.push("1", method.name);
182+
result_1.push(method.name);
183+
});
184+
return [4 /*yield*/, (_a = this._functions).zAddAsync.apply(_a, __spreadArrays([this._storeKey, 'NX'], args_1))];
185+
case 2:
186+
_b.sent();
187+
return [2 /*return*/, result_1];
188+
case 3: return [2 /*return*/, storedMethodNames];
189+
}
190+
});
191+
});
192+
};
193+
return CallableBalancer;
194+
}());
195+
exports.default = CallableBalancer;

0 commit comments

Comments
 (0)