Spaces:
Build error
Build error
fix: add health check to detect puppeteer stall
Browse files
backend/functions/package-lock.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
| 14 |
"archiver": "^6.0.1",
|
| 15 |
"axios": "^1.3.3",
|
| 16 |
"bcrypt": "^5.1.0",
|
| 17 |
-
"civkit": "^0.6.5-
|
| 18 |
"cors": "^2.8.5",
|
| 19 |
"dayjs": "^1.11.9",
|
| 20 |
"express": "^4.19.2",
|
|
@@ -3674,9 +3674,9 @@
|
|
| 3674 |
}
|
| 3675 |
},
|
| 3676 |
"node_modules/civkit": {
|
| 3677 |
-
"version": "0.6.5-
|
| 3678 |
-
"resolved": "https://registry.npmjs.org/civkit/-/civkit-0.6.5-
|
| 3679 |
-
"integrity": "sha512-
|
| 3680 |
"dependencies": {
|
| 3681 |
"lodash": "^4.17.21",
|
| 3682 |
"tslib": "^2.5.0"
|
|
|
|
| 14 |
"archiver": "^6.0.1",
|
| 15 |
"axios": "^1.3.3",
|
| 16 |
"bcrypt": "^5.1.0",
|
| 17 |
+
"civkit": "^0.6.5-7a4ba56",
|
| 18 |
"cors": "^2.8.5",
|
| 19 |
"dayjs": "^1.11.9",
|
| 20 |
"express": "^4.19.2",
|
|
|
|
| 3674 |
}
|
| 3675 |
},
|
| 3676 |
"node_modules/civkit": {
|
| 3677 |
+
"version": "0.6.5-7a4ba56",
|
| 3678 |
+
"resolved": "https://registry.npmjs.org/civkit/-/civkit-0.6.5-7a4ba56.tgz",
|
| 3679 |
+
"integrity": "sha512-WAKnZn7DwuHkjEaH/bGXN4ZSYFvzM06ky1S9LjzHd1Ud+fMd3sEJR0b68BprzqXdeBNB5LyPHO4Gikf1z7J1bA==",
|
| 3680 |
"dependencies": {
|
| 3681 |
"lodash": "^4.17.21",
|
| 3682 |
"tslib": "^2.5.0"
|
backend/functions/package.json
CHANGED
|
@@ -34,7 +34,7 @@
|
|
| 34 |
"archiver": "^6.0.1",
|
| 35 |
"axios": "^1.3.3",
|
| 36 |
"bcrypt": "^5.1.0",
|
| 37 |
-
"civkit": "^0.6.5-
|
| 38 |
"cors": "^2.8.5",
|
| 39 |
"dayjs": "^1.11.9",
|
| 40 |
"express": "^4.19.2",
|
|
|
|
| 34 |
"archiver": "^6.0.1",
|
| 35 |
"axios": "^1.3.3",
|
| 36 |
"bcrypt": "^5.1.0",
|
| 37 |
+
"civkit": "^0.6.5-7a4ba56",
|
| 38 |
"cors": "^2.8.5",
|
| 39 |
"dayjs": "^1.11.9",
|
| 40 |
"express": "^4.19.2",
|
backend/functions/src/services/puppeteer.ts
CHANGED
|
@@ -2,7 +2,7 @@ import os from 'os';
|
|
| 2 |
import fs from 'fs';
|
| 3 |
import { container, singleton } from 'tsyringe';
|
| 4 |
import genericPool from 'generic-pool';
|
| 5 |
-
import { AsyncService, Defer, marshalErrorLike, AssertionFailureError } from 'civkit';
|
| 6 |
import { Logger } from '../shared/services/logger';
|
| 7 |
|
| 8 |
import type { Browser, CookieParam, Page } from 'puppeteer';
|
|
@@ -82,8 +82,16 @@ export class PuppeteerControl extends AsyncService {
|
|
| 82 |
return page;
|
| 83 |
},
|
| 84 |
destroy: async (page) => {
|
| 85 |
-
await
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
},
|
| 88 |
validate: async (page) => {
|
| 89 |
return page.browser().connected && !page.isClosed();
|
|
@@ -95,13 +103,20 @@ export class PuppeteerControl extends AsyncService {
|
|
| 95 |
testOnBorrow: true,
|
| 96 |
testOnReturn: true,
|
| 97 |
autostart: false,
|
|
|
|
| 98 |
});
|
| 99 |
|
|
|
|
|
|
|
| 100 |
constructor(protected globalLogger: Logger) {
|
| 101 |
super(...arguments);
|
| 102 |
}
|
| 103 |
|
| 104 |
override async init() {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
await this.dependencyReady();
|
| 106 |
this.logger.info(`PuppeteerControl initializing with pool size ${this.pagePool.max}`, { poolSize: this.pagePool.max });
|
| 107 |
this.pagePool.start();
|
|
@@ -110,7 +125,7 @@ export class PuppeteerControl extends AsyncService {
|
|
| 110 |
if (this.browser.connected) {
|
| 111 |
await this.browser.close();
|
| 112 |
} else {
|
| 113 |
-
this.browser.process()?.kill();
|
| 114 |
}
|
| 115 |
}
|
| 116 |
this.browser = await puppeteer.launch({
|
|
@@ -130,6 +145,27 @@ export class PuppeteerControl extends AsyncService {
|
|
| 130 |
this.logger.info(`Browser launched: ${this.browser.process()?.pid}`);
|
| 131 |
|
| 132 |
this.emit('ready');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
}
|
| 134 |
|
| 135 |
async newPage() {
|
|
|
|
| 2 |
import fs from 'fs';
|
| 3 |
import { container, singleton } from 'tsyringe';
|
| 4 |
import genericPool from 'generic-pool';
|
| 5 |
+
import { AsyncService, Defer, marshalErrorLike, AssertionFailureError, delay, maxConcurrency } from 'civkit';
|
| 6 |
import { Logger } from '../shared/services/logger';
|
| 7 |
|
| 8 |
import type { Browser, CookieParam, Page } from 'puppeteer';
|
|
|
|
| 82 |
return page;
|
| 83 |
},
|
| 84 |
destroy: async (page) => {
|
| 85 |
+
await Promise.race([
|
| 86 |
+
(async () => {
|
| 87 |
+
const ctx = page.browserContext();
|
| 88 |
+
await page.removeExposedFunction('reportSnapshot');
|
| 89 |
+
await page.close();
|
| 90 |
+
await ctx.close();
|
| 91 |
+
})(), delay(5000)
|
| 92 |
+
]).catch((err) => {
|
| 93 |
+
this.logger.error(`Failed to destroy page`, { err: marshalErrorLike(err) });
|
| 94 |
+
});
|
| 95 |
},
|
| 96 |
validate: async (page) => {
|
| 97 |
return page.browser().connected && !page.isClosed();
|
|
|
|
| 103 |
testOnBorrow: true,
|
| 104 |
testOnReturn: true,
|
| 105 |
autostart: false,
|
| 106 |
+
priorityRange: 3
|
| 107 |
});
|
| 108 |
|
| 109 |
+
private __healthCheckInterval?: NodeJS.Timeout;
|
| 110 |
+
|
| 111 |
constructor(protected globalLogger: Logger) {
|
| 112 |
super(...arguments);
|
| 113 |
}
|
| 114 |
|
| 115 |
override async init() {
|
| 116 |
+
if (this.__healthCheckInterval) {
|
| 117 |
+
clearInterval(this.__healthCheckInterval);
|
| 118 |
+
this.__healthCheckInterval = undefined;
|
| 119 |
+
}
|
| 120 |
await this.dependencyReady();
|
| 121 |
this.logger.info(`PuppeteerControl initializing with pool size ${this.pagePool.max}`, { poolSize: this.pagePool.max });
|
| 122 |
this.pagePool.start();
|
|
|
|
| 125 |
if (this.browser.connected) {
|
| 126 |
await this.browser.close();
|
| 127 |
} else {
|
| 128 |
+
this.browser.process()?.kill('SIGKILL');
|
| 129 |
}
|
| 130 |
}
|
| 131 |
this.browser = await puppeteer.launch({
|
|
|
|
| 145 |
this.logger.info(`Browser launched: ${this.browser.process()?.pid}`);
|
| 146 |
|
| 147 |
this.emit('ready');
|
| 148 |
+
|
| 149 |
+
this.__healthCheckInterval = setInterval(() => this.healthCheck(), 30_000);
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
@maxConcurrency(1)
|
| 153 |
+
async healthCheck() {
|
| 154 |
+
const healthyPage = await Promise.race([this.pagePool.acquire(3), delay(60_000).then(() => null)]).catch((err) => {
|
| 155 |
+
this.logger.error(`Health check failed`, { err: marshalErrorLike(err) });
|
| 156 |
+
return null;
|
| 157 |
+
});
|
| 158 |
+
|
| 159 |
+
if (healthyPage) {
|
| 160 |
+
this.pagePool.release(healthyPage);
|
| 161 |
+
return;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
this.logger.warn(`Health check failed, trying to clean up.`);
|
| 165 |
+
await this.pagePool.clear();
|
| 166 |
+
this.browser.process()?.kill('SIGKILL');
|
| 167 |
+
Reflect.deleteProperty(this, 'browser');
|
| 168 |
+
this.emit('crippled');
|
| 169 |
}
|
| 170 |
|
| 171 |
async newPage() {
|
thinapps-shared
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
Subproject commit
|
|
|
|
| 1 |
+
Subproject commit e2a1d586063f8e8d663c013fa2febe9f621f9f8e
|