You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-04-18 19:42:23 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17a7fcff2d | |||
| a937934bb1 |
@@ -45,7 +45,6 @@
|
||||
"markdown-it": "13.0.2",
|
||||
"mustache": "4.2.0",
|
||||
"node-cron": "3.0.3",
|
||||
"node-os-utils": "1.3.7",
|
||||
"nodemailer": "6.10.1",
|
||||
"nodemon": "3.1.11",
|
||||
"otplib": "12.0.1",
|
||||
@@ -76,7 +75,6 @@
|
||||
"@types/markdown-it": "13.0.9",
|
||||
"@types/mustache": "4.2.6",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/node-os-utils": "1.3.4",
|
||||
"@types/nodemailer": "6.4.21",
|
||||
"@types/yargs": "17.0.35",
|
||||
"@types/zxcvbn": "4.4.5",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import newModelFactory, { Models } from '../models/factory';
|
||||
import { DbConnection, disconnectDb } from '../db';
|
||||
import { Models } from '../models/factory';
|
||||
import { Config, Env } from '../utils/types';
|
||||
import BaseService from './BaseService';
|
||||
import { Event, EventType, TaskId, TaskState } from './database/types';
|
||||
@@ -68,29 +67,16 @@ export default class TaskService extends BaseService {
|
||||
|
||||
private tasks_: Tasks = {};
|
||||
private services_: Services;
|
||||
private taskStateModels_: Models;
|
||||
private taskStateDb_: DbConnection;
|
||||
|
||||
public constructor(env: Env, models: Models, config: Config, services: Services, taskStateDb: DbConnection = null) {
|
||||
public constructor(env: Env, models: Models, config: Config, services: Services) {
|
||||
super(env, models, config);
|
||||
this.services_ = services;
|
||||
this.taskStateDb_ = taskStateDb;
|
||||
this.taskStateModels_ = taskStateDb ? newModelFactory(taskStateDb, taskStateDb, config) : models;
|
||||
}
|
||||
|
||||
public async destroy() {
|
||||
await super.destroy();
|
||||
if (this.taskStateDb_) {
|
||||
await disconnectDb(this.taskStateDb_);
|
||||
this.taskStateDb_ = null;
|
||||
this.taskStateModels_ = null;
|
||||
}
|
||||
}
|
||||
|
||||
public async registerTask(task: Task) {
|
||||
if (this.tasks_[task.id]) throw new Error(`Already a task with this ID: ${task.id}`);
|
||||
this.tasks_[task.id] = task;
|
||||
await this.taskStateModels_.taskState().init(task.id);
|
||||
await this.models.taskState().init(task.id);
|
||||
}
|
||||
|
||||
public async registerTasks(tasks: Task[]) {
|
||||
@@ -106,7 +92,7 @@ export default class TaskService extends BaseService {
|
||||
}
|
||||
|
||||
public async taskStates(ids: TaskId[]): Promise<TaskState[]> {
|
||||
return this.taskStateModels_.taskState().loadByTaskIds(ids);
|
||||
return this.models.taskState().loadByTaskIds(ids);
|
||||
}
|
||||
|
||||
public async taskState(id: TaskId): Promise<TaskState> {
|
||||
@@ -117,17 +103,17 @@ export default class TaskService extends BaseService {
|
||||
|
||||
public async taskLastEvents(id: TaskId): Promise<TaskEvents> {
|
||||
return {
|
||||
taskStarted: await this.taskStateModels_.event().lastEventByTypeAndName(EventType.TaskStarted, id.toString()),
|
||||
taskCompleted: await this.taskStateModels_.event().lastEventByTypeAndName(EventType.TaskCompleted, id.toString()),
|
||||
taskStarted: await this.models.event().lastEventByTypeAndName(EventType.TaskStarted, id.toString()),
|
||||
taskCompleted: await this.models.event().lastEventByTypeAndName(EventType.TaskCompleted, id.toString()),
|
||||
};
|
||||
}
|
||||
|
||||
public async resetInterruptedTasks() {
|
||||
const taskStates = await this.taskStateModels_.taskState().all();
|
||||
const taskStates = await this.models.taskState().all();
|
||||
for (const taskState of taskStates) {
|
||||
if (taskState.running) {
|
||||
logger.warn(`Found a task that was in running state: ${this.taskDisplayString(taskState.task_id)} - resetting it.`);
|
||||
await this.taskStateModels_.taskState().stop(taskState.task_id);
|
||||
await this.models.taskState().stop(taskState.task_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,7 +130,7 @@ export default class TaskService extends BaseService {
|
||||
|
||||
public async runTask(id: TaskId, runType: RunType) {
|
||||
const displayString = this.taskDisplayString(id);
|
||||
const taskState = await this.taskStateModels_.taskState().loadByTaskId(id);
|
||||
const taskState = await this.models.taskState().loadByTaskId(id);
|
||||
if (!taskState) throw new Error(`Invalid task: ${id}: ${runType}`);
|
||||
|
||||
if (!taskState.enabled) {
|
||||
@@ -152,11 +138,11 @@ export default class TaskService extends BaseService {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.taskStateModels_.taskState().start(id);
|
||||
await this.models.taskState().start(id);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await this.taskStateModels_.event().create(EventType.TaskStarted, id.toString());
|
||||
await this.models.event().create(EventType.TaskStarted, id.toString());
|
||||
|
||||
try {
|
||||
logger.info(`Running ${displayString} (${runTypeToString(runType)})...`);
|
||||
@@ -165,14 +151,14 @@ export default class TaskService extends BaseService {
|
||||
logger.error(`On ${displayString}`, error);
|
||||
}
|
||||
|
||||
await this.taskStateModels_.taskState().stop(id);
|
||||
await this.taskStateModels_.event().create(EventType.TaskCompleted, id.toString());
|
||||
await this.models.taskState().stop(id);
|
||||
await this.models.event().create(EventType.TaskCompleted, id.toString());
|
||||
|
||||
logger.info(`Completed ${this.taskDisplayString(id)} in ${Date.now() - startTime}ms`);
|
||||
}
|
||||
|
||||
public async enableTask(taskId: TaskId, enabled = true) {
|
||||
await this.taskStateModels_.taskState().enable(taskId, enabled);
|
||||
await this.models.taskState().enable(taskId, enabled);
|
||||
}
|
||||
|
||||
public async runInBackground() {
|
||||
|
||||
@@ -6,13 +6,10 @@ describe('metrics', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
clearMetrics();
|
||||
jest.useFakeTimers({
|
||||
// Timers need to auto-advance to support node-os-utils.
|
||||
advanceTimers: true,
|
||||
});
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
it('should generate a heartbeat message', async () => {
|
||||
it('should generate a heartbeat message', () => {
|
||||
const requestId1 = Math.random().toString();
|
||||
const requestId2 = Math.random().toString();
|
||||
const requestId3 = Math.random().toString();
|
||||
@@ -22,18 +19,19 @@ describe('metrics', () => {
|
||||
onRequestStart(requestId3);
|
||||
onRequestComplete(requestId2);
|
||||
|
||||
jest.advanceTimersByTime(Second);
|
||||
|
||||
const regex = /Cpu: (.*?)%; Mem: (.*?) \/ (.*?) MB \((.*?)%\); Req: 3 \/ min; Active req: 2/;
|
||||
|
||||
const message = await heartbeatMessage();
|
||||
const message = heartbeatMessage();
|
||||
|
||||
const match = message.match(regex);
|
||||
expect(match.length).toBe(5);
|
||||
expect(Number(match[1])).toBeGreaterThan(0);
|
||||
expect(Number(match[2])).toBeLessThan(Number(match[3]));
|
||||
expect(Number(match[3])).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should count the number of requests per minute', async () => {
|
||||
it('should count the number of requests per minute', () => {
|
||||
const mockRequest = () => {
|
||||
const id = uuid.create();
|
||||
onRequestStart(id);
|
||||
@@ -44,18 +42,20 @@ describe('metrics', () => {
|
||||
mockRequest();
|
||||
jest.advanceTimersByTime(Second);
|
||||
}
|
||||
expect(await heartbeatMessage()).toMatch(/Req: 10 \/ min/);
|
||||
expect(heartbeatMessage()).toMatch(/Req: 10 \/ min/);
|
||||
|
||||
jest.advanceTimersByTime(Minute * 15);
|
||||
expect(await heartbeatMessage()).toMatch(/Req: 0 \/ min/);
|
||||
expect(heartbeatMessage()).toMatch(/Req: 0 \/ min/);
|
||||
mockRequest();
|
||||
expect(await heartbeatMessage()).toMatch(/Req: 1 \/ min/);
|
||||
jest.advanceTimersByTime(Second);
|
||||
expect(heartbeatMessage()).toMatch(/Req: 1 \/ min/);
|
||||
|
||||
jest.advanceTimersByTime(Minute * 2);
|
||||
mockRequest();
|
||||
jest.advanceTimersByTime(Second * 10);
|
||||
mockRequest();
|
||||
expect(await heartbeatMessage()).toMatch(/Req: 2 \/ min/);
|
||||
jest.advanceTimersByTime(Second);
|
||||
expect(heartbeatMessage()).toMatch(/Req: 2 \/ min/);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Provides metrics about the operating system and server application, and format them in a message
|
||||
// that can be printed to log.
|
||||
|
||||
import { MemUsedInfo, cpu, mem } from 'node-os-utils';
|
||||
import * as os from 'os';
|
||||
import { Minute } from './time';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
@@ -53,43 +53,26 @@ export const clearMetrics = () => {
|
||||
activeRequests_.clear();
|
||||
};
|
||||
|
||||
export const heartbeatMessage = async () => {
|
||||
const interval = 1000;
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
interface Info {
|
||||
cpu?: number;
|
||||
memory?: MemUsedInfo;
|
||||
}
|
||||
|
||||
const info: Info = {};
|
||||
|
||||
const getCpu = async () => {
|
||||
info.cpu = await cpu.usage(interval);
|
||||
};
|
||||
|
||||
const getMemory = async () => {
|
||||
info.memory = await mem.used();
|
||||
};
|
||||
|
||||
promises.push(getCpu());
|
||||
promises.push(getMemory());
|
||||
|
||||
await Promise.all(promises);
|
||||
export const heartbeatMessage = () => {
|
||||
const loadAvg = os.loadavg();
|
||||
const totalMemMb = Math.round(os.totalmem() / (1024 * 1024));
|
||||
const freeMemMb = Math.round(os.freemem() / (1024 * 1024));
|
||||
const usedMemMb = totalMemMb - freeMemMb;
|
||||
|
||||
const line: string[] = [];
|
||||
|
||||
line.push(`Cpu: ${info.cpu}%`);
|
||||
line.push(`Mem: ${info.memory.usedMemMb} / ${info.memory.totalMemMb} MB (${Math.round((info.memory.usedMemMb / info.memory.totalMemMb) * 100)}%)`);
|
||||
const cpuCount = os.cpus().length;
|
||||
const cpuPercent = Math.round((loadAvg[0] / cpuCount) * 100);
|
||||
line.push(`Cpu: ${cpuPercent}%`);
|
||||
line.push(`Mem: ${usedMemMb} / ${totalMemMb} MB (${Math.round((usedMemMb / totalMemMb) * 100)}%)`);
|
||||
line.push(`Req: ${requestsPerMinute()} / min`);
|
||||
line.push(`Active req: ${activeRequests_.size}`);
|
||||
|
||||
return line.join('; ');
|
||||
};
|
||||
|
||||
export const logHeartbeat = async () => {
|
||||
logger.info(await heartbeatMessage());
|
||||
export const logHeartbeat = () => {
|
||||
logger.info(heartbeatMessage());
|
||||
};
|
||||
|
||||
export const onRequestStart = (requestId: string) => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Models } from '../models/factory';
|
||||
import { connectDb } from '../db';
|
||||
import { TaskId } from '../services/database/types';
|
||||
import TaskService, { Task, taskIdToLabel } from '../services/TaskService';
|
||||
import { Services } from '../services/types';
|
||||
@@ -8,12 +7,7 @@ import { Config, Env } from './types';
|
||||
import { Day } from './time';
|
||||
|
||||
export default async function(env: Env, models: Models, config: Config, services: Services): Promise<TaskService> {
|
||||
// In production, use a separate DB connection pool for task state
|
||||
// management so that it is not affected by failed transactions in the
|
||||
// main connection pool. In dev/test, we reuse the main connection to
|
||||
// avoid exhausting Postgres connection slots in CI.
|
||||
const taskStateDb = env === Env.Prod ? await connectDb({ ...config.database, maxConnections: 1 }) : null;
|
||||
const taskService = new TaskService(env, models, config, services, taskStateDb);
|
||||
const taskService = new TaskService(env, models, config, services);
|
||||
|
||||
let tasks: Task[] = [
|
||||
{
|
||||
|
||||
@@ -13305,7 +13305,6 @@ __metadata:
|
||||
"@types/markdown-it": "npm:13.0.9"
|
||||
"@types/mustache": "npm:4.2.6"
|
||||
"@types/node": "npm:18.19.130"
|
||||
"@types/node-os-utils": "npm:1.3.4"
|
||||
"@types/nodemailer": "npm:6.4.21"
|
||||
"@types/qrcode": "npm:1.5.6"
|
||||
"@types/uuid": "npm:11.0.0"
|
||||
@@ -13330,7 +13329,6 @@ __metadata:
|
||||
mustache: "npm:4.2.0"
|
||||
node-cron: "npm:3.0.3"
|
||||
node-mocks-http: "npm:1.17.2"
|
||||
node-os-utils: "npm:1.3.7"
|
||||
nodemailer: "npm:6.10.1"
|
||||
nodemon: "npm:3.1.11"
|
||||
otplib: "npm:12.0.1"
|
||||
@@ -19431,13 +19429,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node-os-utils@npm:1.3.4":
|
||||
version: 1.3.4
|
||||
resolution: "@types/node-os-utils@npm:1.3.4"
|
||||
checksum: 10/e0adbb62b9503b86a16c5a09058104ec81975ad58c6c28594986de0602378f4c87d0b46d1f5445a778607e42df1b318cdea91e2ac54994572904490dc1a086af
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node-rsa@npm:1.1.4":
|
||||
version: 1.1.4
|
||||
resolution: "@types/node-rsa@npm:1.1.4"
|
||||
@@ -44292,13 +44283,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-os-utils@npm:1.3.7":
|
||||
version: 1.3.7
|
||||
resolution: "node-os-utils@npm:1.3.7"
|
||||
checksum: 10/728aa20051f6502158ed5c58de4c4073a5715285151dc99a311bf9968a163019ca0b96d4432c244758d5545c896137f1e0be00a54f7d6f88cac4ac0dc07c2ad2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-persist@npm:3.1.3":
|
||||
version: 3.1.3
|
||||
resolution: "node-persist@npm:3.1.3"
|
||||
|
||||
Reference in New Issue
Block a user