nomagick commited on
Commit
528b3e5
·
unverified ·
1 Parent(s): ae29055

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-79b1e2c",
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-79b1e2c",
3678
- "resolved": "https://registry.npmjs.org/civkit/-/civkit-0.6.5-79b1e2c.tgz",
3679
- "integrity": "sha512-JwuDgfo6YMopniSHmYXwtzSOUs/i8FA+GNLhPUivh1AtfMp7nO6C354xNalVP+nP8TqZE2HgmLL2aiyxrX51sQ==",
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-79b1e2c",
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 page.removeExposedFunction('reportSnapshot');
86
- await page.browserContext().close();
 
 
 
 
 
 
 
 
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 e681cf89bd21d77469dd286b2348e4cf5fce76e7
 
1
+ Subproject commit e2a1d586063f8e8d663c013fa2febe9f621f9f8e