Skip to content

Commit c166560

Browse files
committed
feat: add /healthcheck endpoint
1 parent 331d9e7 commit c166560

File tree

4 files changed

+101
-6
lines changed

4 files changed

+101
-6
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (C) 2024 Puter Technologies Inc.
3+
*
4+
* This file is part of Puter.
5+
*
6+
* Puter is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published
8+
* by the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
"use strict"
20+
const express = require('express');
21+
const router = new express.Router();
22+
23+
// -----------------------------------------------------------------------//
24+
// GET /healthcheck
25+
// -----------------------------------------------------------------------//
26+
router.get('/healthcheck', async (req, res) => {
27+
const svc_serverHealth = req.services.get('server-health');
28+
29+
const status = await svc_serverHealth.get_status();
30+
res.json(status);
31+
})
32+
module.exports = router

packages/backend/src/services/PuterAPIService.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class PuterAPIService extends BaseService {
7171
app.use(require('../routers/sites'))
7272
// app.use(require('../routers/filesystem_api/stat'))
7373
app.use(require('../routers/suggest_apps'))
74+
app.use(require('../routers/healthcheck'))
7475
app.use(require('../routers/test'))
7576
app.use(require('../routers/update-taskbar-items'))
7677
require('../routers/whoami')(app);

packages/backend/src/services/database/SqliteDatabaseAccessService.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
126126
svc_devConsole.add_widget(this.database_update_notice);
127127
})();
128128
}
129+
130+
const svc_serverHealth = this.services.get('server-health');
131+
132+
svc_serverHealth.add_check('sqlite', async () => {
133+
const [{ user_version }] = await this._requireRead('PRAGMA user_version');
134+
if ( user_version !== TARGET_VERSION ) {
135+
throw new Error(
136+
`Database version mismatch: expected ${TARGET_VERSION}, ` +
137+
`got ${user_version}`);
138+
}
139+
});
129140
}
130141

131142
async _read (query, params = []) {

packages/backend/src/services/runtime-analysis/ServerHealthService.js

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,18 @@
1919
const BaseService = require("../BaseService");
2020
const { SECOND } = require("../../util/time");
2121
const { parse_meminfo } = require("../../util/linux");
22-
const { asyncSafeSetInterval } = require("../../util/promise");
22+
const { asyncSafeSetInterval, TeePromise } = require("../../util/promise");
2323

2424
class ServerHealthService extends BaseService {
2525
static MODULES = {
2626
fs: require('fs'),
2727
}
28+
_construct () {
29+
this.checks_ = [];
30+
this.failures_ = [];
31+
}
2832
async _init () {
29-
const ram_poll_interval = 10 * SECOND;
33+
this.init_service_checks_();
3034

3135
/*
3236
There's an interesting thread here:
@@ -53,7 +57,7 @@ class ServerHealthService extends BaseService {
5357
return;
5458
}
5559

56-
asyncSafeSetInterval(async () => {
60+
this.add_check('ram-usage', async () => {
5761
const meminfo_text = await this.modules.fs.promises.readFile(
5862
'/proc/meminfo', 'utf8'
5963
);
@@ -69,11 +73,46 @@ class ServerHealthService extends BaseService {
6973
if ( meminfo.MemAvailable < min_available_KiB ) {
7074
svc_alarm.create('low-available-memory', 'Low available memory', alarm_fields);
7175
}
72-
}, ram_poll_interval, null,{
76+
});
77+
}
78+
79+
init_service_checks_ () {
80+
const svc_alarm = this.services.get('alarm');
81+
asyncSafeSetInterval(async () => {
82+
const check_failures = [];
83+
for ( const { name, fn } of this.checks_ ) {
84+
const p_timeout = new TeePromise();
85+
const timeout = setTimeout(() => {
86+
p_timeout.reject(new Error('Health check timed out'));
87+
}, 5 * SECOND);
88+
try {
89+
await Promise.race([
90+
fn(),
91+
p_timeout,
92+
]);
93+
clearTimeout(timeout);
94+
} catch ( err ) {
95+
// Trigger an alarm if this check isn't already in the failure list
96+
97+
if ( this.failures_.some(v => v.name === name) ) {
98+
return;
99+
}
100+
101+
svc_alarm.create(
102+
'health-check-failure',
103+
`Health check ${name} failed`,
104+
{ error: err }
105+
);
106+
check_failures.push({ name });
107+
}
108+
}
109+
110+
this.failures_ = check_failures;
111+
}, 10 * SECOND, null, {
73112
onBehindSchedule: (drift) => {
74113
svc_alarm.create(
75-
'ram-usage-poll-behind-schedule',
76-
'RAM usage poll is behind schedule',
114+
'health-checks-behind-schedule',
115+
'Health checks are behind schedule',
77116
{ drift }
78117
);
79118
}
@@ -83,6 +122,18 @@ class ServerHealthService extends BaseService {
83122
async get_stats () {
84123
return { ...this.stats_ };
85124
}
125+
126+
add_check (name, fn) {
127+
this.checks_.push({ name, fn });
128+
}
129+
130+
get_status () {
131+
const failures = this.failures_.map(v => v.name);
132+
return {
133+
ok: failures.length === 0,
134+
...(failures.length ? { failed: failures } : {}),
135+
};
136+
}
86137
}
87138

88139
module.exports = { ServerHealthService };

0 commit comments

Comments
 (0)