dynapm 1.0.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/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # DynaPM
2
+
3
+ [中文文档](./README_zh.md)
4
+
5
+ This is a program management tool for dynamically starting and stopping programs, with some serverless-like features. The goal is to maintain a large number of low-frequency accessed programs while keeping a few high-frequency accessed programs running online, all while dealing with limited resources in a private deployment environment.
6
+
7
+ Each time a user accesses one of these programs, if the program is offline, the gateway temporarily suspends the request and immediately starts the program. Once the program starts successfully, the gateway acts as a reverse proxy.
8
+
9
+ When memory is insufficient or a program has been idle for a long time, the program is automatically shut down to free up server resources.
10
+
11
+ Starting the simplest Node.js HTTP program using pm2 takes approximately 600ms.
12
+
13
+ ```log
14
+ Starting [test]
15
+ Starting [test] took 601 ms
16
+ Stopping [test]
17
+ Starting [test]
18
+ Starting [test] took 592 ms
19
+ Stopping [test]
20
+ ```
21
+
22
+ ## Introduction
23
+
24
+ I often want to write programs that run continuously, and I hope to be able to access their websites or call their APIs at any time. However, these programs are not always working, so I also hope that when they are not working, they consume almost no CPU or RAM except for disk space. Previously, I looked into serverless solutions, but they were still quite麻烦 in terms of deployment and did not fully utilize my idle servers.
25
+
26
+ Now I can use DynaPM to keep thousands of programs ready for immediate access as long as my disk space allows. Of course, the actual number of programs that can run simultaneously may be limited to dozens depending on the machine's capacity, but most of the programs I have written in the past do not have long service times, so this limitation should not be a problem.
27
+
28
+ ## Features
29
+
30
+ - [x] Start programs using pm2 when a request arrives
31
+ - [x] Shut down programs when they are idle
32
+ - [ ] Support auto-scaling
33
+ - [ ] Support persistent operation (only shut down programs when idle RAM is insufficient)
34
+ - [ ] Support cron jobs
35
+ - [ ] Support dynamically starting and stopping Docker containers
36
+ - [ ] Support starting programs using spawn/fork
37
+
38
+ ## Performance
39
+
40
+ ### fastify + @fastify/reply-from
41
+
42
+ After a cold start, testing performance with `autocannon http://127.0.0.1:83` yields an average of 3000 req/s.
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.n = function(module) {
5
+ var getter = module && module.__esModule ? function() {
6
+ return module['default'];
7
+ } : function() {
8
+ return module;
9
+ };
10
+ __webpack_require__.d(getter, {
11
+ a: getter
12
+ });
13
+ return getter;
14
+ };
15
+ })();
16
+ (()=>{
17
+ __webpack_require__.d = function(exports1, definition) {
18
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
19
+ enumerable: true,
20
+ get: definition[key]
21
+ });
22
+ };
23
+ })();
24
+ (()=>{
25
+ __webpack_require__.o = function(obj, prop) {
26
+ return Object.prototype.hasOwnProperty.call(obj, prop);
27
+ };
28
+ })();
29
+ var __webpack_exports__ = {};
30
+ const external_fastify_namespaceObject = require("fastify");
31
+ var external_fastify_default = /*#__PURE__*/ __webpack_require__.n(external_fastify_namespaceObject);
32
+ const reply_from_namespaceObject = require("@fastify/reply-from");
33
+ var reply_from_default = /*#__PURE__*/ __webpack_require__.n(reply_from_namespaceObject);
34
+ const external_pm2_namespaceObject = require("pm2");
35
+ var external_pm2_default = /*#__PURE__*/ __webpack_require__.n(external_pm2_namespaceObject);
36
+ function getBase200(base) {
37
+ return fetch(base).then((res)=>200 === res.status).catch(()=>false);
38
+ }
39
+ async function runCheck(config) {
40
+ const time1 = Date.now();
41
+ if ('getBase200' === config.runCheck) while(true){
42
+ if (Date.now() - time1 > config.maxAwaitLaunchTime) throw new Error(`等待启动的时间超过了配置的 maxAwaitLaunchTime :${config.maxAwaitLaunchTime}`);
43
+ if (await getBase200(config.base)) return true;
44
+ }
45
+ else {
46
+ config.runCheck;
47
+ throw new Error(`尚未支持的 config check : ${config.runCheck}`);
48
+ }
49
+ }
50
+ const pm2Map = {
51
+ '127.0.0.1': {
52
+ base: 'http://127.0.0.1:3001',
53
+ latestTime: Date.now(),
54
+ stopTime: 5000,
55
+ maxAwaitLaunchTime: 5000,
56
+ runCheck: 'getBase200',
57
+ runStatus: 'unknown',
58
+ pm2Options: {
59
+ name: 'test',
60
+ script: './dist/test/test.mjs',
61
+ interpreter: 'node'
62
+ }
63
+ }
64
+ };
65
+ setInterval(()=>{
66
+ Object.entries(pm2Map).forEach(([hostname, target])=>{
67
+ if ('running' === target.runStatus && Date.now() - target.latestTime > target.stopTime) external_pm2_default().stop(target.pm2Options.name, (err, proc)=>{
68
+ if (err) console.log('[stop失败]', err);
69
+ else {
70
+ target.runStatus = 'stop';
71
+ console.log(`停止 [${target.pm2Options.name}] `);
72
+ }
73
+ });
74
+ });
75
+ }, 3000);
76
+ const pm2Connect = new Promise((r)=>{
77
+ external_pm2_default().connect(()=>{
78
+ r(1);
79
+ });
80
+ });
81
+ const pm2Manage = {
82
+ async start (target) {
83
+ const time1 = Date.now();
84
+ await pm2Connect;
85
+ let status = await getProcessStatus(target.pm2Options.name);
86
+ async function skipLaunching() {
87
+ if ('launching' === status) while('launching' === status){
88
+ target.runStatus = 'launching';
89
+ status = await getProcessStatus(target.pm2Options.name);
90
+ }
91
+ }
92
+ await skipLaunching();
93
+ if ('online' !== status) {
94
+ await new Promise((resolve, reject)=>{
95
+ external_pm2_default().start(target.pm2Options, (err, proc)=>{
96
+ console.log(`开始启动 [${target.pm2Options.name}]`);
97
+ if (err) {
98
+ console.log('启动程序失败', err);
99
+ reject(err);
100
+ } else {
101
+ status = 'launching';
102
+ resolve(1);
103
+ }
104
+ });
105
+ });
106
+ await skipLaunching();
107
+ }
108
+ if ('online' !== status) {
109
+ console.log('[status]', status);
110
+ throw new Error('进程启动异常');
111
+ }
112
+ {
113
+ target.latestTime = Date.now();
114
+ target.runStatus = 'running';
115
+ await runCheck(target);
116
+ const time2 = Date.now();
117
+ console.log(`启动 [${target.pm2Options.name}] 耗时 ${time2 - time1} ms`);
118
+ return true;
119
+ }
120
+ }
121
+ };
122
+ async function getProcessStatus(processID) {
123
+ await pm2Connect;
124
+ return new Promise((r)=>{
125
+ external_pm2_default().describe(processID, (err, process)=>{
126
+ err ? r('error') : r(process[0]?.pm2_env?.status);
127
+ });
128
+ });
129
+ }
130
+ const proxyPort = 83;
131
+ const proxy = external_fastify_default()();
132
+ proxy.register(reply_from_default(), {});
133
+ proxy.all('*', {}, async (request, reply)=>{
134
+ const target = pm2Map[request.hostname] || null;
135
+ if (!target) return reply.status(404).send('Not Found');
136
+ target.latestTime = Date.now();
137
+ if ('running' !== target.runStatus) await pm2Manage.start(target);
138
+ return reply.from(target.base + request.url);
139
+ });
140
+ proxy.listen({
141
+ port: proxyPort,
142
+ host: '127.0.0.1'
143
+ }, (err)=>{
144
+ if (err) console.log('[err]', err);
145
+ else {
146
+ console.log('服务启动成功');
147
+ console.log(`[proxy] listening on port ${proxyPort}`);
148
+ }
149
+ });
150
+ var __webpack_export_target__ = exports;
151
+ for(var __webpack_i__ in __webpack_exports__)__webpack_export_target__[__webpack_i__] = __webpack_exports__[__webpack_i__];
152
+ if (__webpack_exports__.__esModule) Object.defineProperty(__webpack_export_target__, '__esModule', {
153
+ value: true
154
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "dynapm",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "dynapm": "dist/src/index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1",
11
+ "build": "rslib build",
12
+ "publish2npm": "npm publish --registry=https://registry.npmjs.org",
13
+ "runTestServer": "pnpm tsx ./test/server1.ts"
14
+ },
15
+ "files": [
16
+ "dist/src/"
17
+ ],
18
+ "keywords": [
19
+ "serverless",
20
+ "pm2",
21
+ "self-host"
22
+ ],
23
+ "publishConfig": {
24
+ "registry": "https://registry.npmjs.org"
25
+ },
26
+ "author": "崮生",
27
+ "license": "ISC",
28
+ "dependencies": {
29
+ "@fastify/reply-from": "^12.0.2",
30
+ "fastify": "^5.2.1",
31
+ "pm2": "^5.4.3"
32
+ },
33
+ "devDependencies": {
34
+ "@rslib/core": "^0.4.1",
35
+ "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.51.0",
36
+ "@types/node": "^22.13.4",
37
+ "tsx": "^4.19.3"
38
+ }
39
+ }