Admin: Add message with failed / total nodes for nodes.json-task.

This commit is contained in:
baldo 2020-06-30 17:08:24 +02:00
parent fb87695b3e
commit b6a67d6e74
12 changed files with 120 additions and 20 deletions

View file

@ -22,6 +22,14 @@
color: red;
}
.task-message.task-result-okay {
color: green;
}
.task-message.task-result-warning {
color: red;
}
.task-description {
max-width: 220px;
}

View file

@ -383,7 +383,8 @@ angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, Rest
}
return 'task-' + field + ' ' +
(task.values.enabled ? 'task-enabled' : 'task-disabled') + ' '
+ 'task-state-' + task.values.state;
+ 'task-state-' + task.values.state + ' '
+ 'task-result-' + (task.values.result ? task.values.result : 'none');
};
}
@ -403,6 +404,7 @@ angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, Rest
nga.field('description').cssClasses(taskClasses('description')),
nga.field('schedule').cssClasses(taskClasses('schedule')),
nga.field('state').cssClasses(taskClasses('state')),
nga.field('message').cssClasses(taskClasses('message')),
nga.field('runningSince').map(formatMoment).cssClasses(taskClasses('runningSince')),
nga.field('lastRunStarted').map(formatMoment).cssClasses(taskClasses('lastRunStarted')),
nga.field('lastRunDuration').map(formatDuration).cssClasses(taskClasses('lastRunDuration'))

View file

@ -46,4 +46,3 @@ if confirm "Continue publishing?"; then
cd dist
npm publish
fi

View file

@ -1,8 +1,12 @@
import {fixNodeFilenames} from "../services/nodeService";
import {jobResultOkay} from "./scheduler";
export default {
name: 'FixNodeFilenamesJob',
description: 'Makes sure node files (holding fastd key, name, etc.) are correctly named.',
run: fixNodeFilenames
async run() {
await fixNodeFilenames();
return jobResultOkay();
},
}

View file

@ -1,8 +1,12 @@
import * as MailService from "../services/mailService"
import {jobResultOkay} from "./scheduler";
export default {
name: 'MailQueueJob',
description: 'Send pending emails (up to 5 attempts in case of failures).',
run: MailService.sendPendingMails,
}
async run() {
await MailService.sendPendingMails();
return jobResultOkay();
},
};

View file

@ -1,8 +1,12 @@
import * as MonitoringService from "../services/monitoringService";
import {jobResultOkay} from "./scheduler";
export default {
name: 'MonitoringMailsSendingJob',
description: 'Sends monitoring emails depending on the monitoring state of nodes retrieved by the NodeInformationRetrievalJob.',
run: MonitoringService.sendMonitoringMails,
async run() {
await MonitoringService.sendMonitoringMails();
return jobResultOkay();
},
};

View file

@ -1,8 +1,20 @@
import * as MonitoringService from "../services/monitoringService";
import {jobResultOkay, jobResultWarning} from "./scheduler";
export default {
name: 'NodeInformationRetrievalJob',
description: 'Fetches the nodes.json and calculates and stores the monitoring / online status for registered nodes.',
run: MonitoringService.retrieveNodeInformation,
async run () {
const result = await MonitoringService.retrieveNodeInformation();
if (result.failedParsingNodesCount > 0) {
return jobResultWarning(
`Warning: ${result.failedParsingNodesCount} of ${result.totalNodesCount} nodes could not be processed.`
);
} else {
return jobResultOkay(
`${result.totalNodesCount} nodes have been processed.`
);
}
},
};

View file

@ -1,8 +1,12 @@
import * as MonitoringService from "../services/monitoringService";
import {jobResultOkay} from "./scheduler";
export default {
name: 'OfflineNodesDeletionJob',
description: 'Delete nodes that are offline for more than 100 days.',
run: MonitoringService.deleteOfflineNodes,
async run() {
await MonitoringService.deleteOfflineNodes();
return jobResultOkay();
},
};

View file

@ -10,11 +10,35 @@ import NodeInformationRetrievalJob from "./NodeInformationRetrievalJob";
import MonitoringMailsSendingJob from "./MonitoringMailsSendingJob";
import OfflineNodesDeletionJob from "./OfflineNodesDeletionJob";
export enum JobResultState {
OKAY = "okay",
WARNING = "warning",
}
export type JobResult = {
state: JobResultState,
message?: string,
};
export function jobResultOkay(message?: string): JobResult {
return {
state: JobResultState.OKAY,
message
}
}
export function jobResultWarning(message?: string): JobResult {
return {
state: JobResultState.WARNING,
message
}
}
export interface Job {
name: string,
description: string,
run(): Promise<void>,
run(): Promise<JobResult>,
}
export enum TaskState {
@ -34,6 +58,7 @@ export class Task {
public lastRunStarted: moment.Moment | null,
public lastRunDuration: number | null,
public state: TaskState,
public result: JobResult | null,
public enabled: boolean,
) {}
@ -47,7 +72,7 @@ export class Task {
this.lastRunStarted = this.runningSince;
this.state = TaskState.RUNNING;
const done = (state: TaskState):void => {
const done = (state: TaskState, result: JobResult | null): void => {
const now = moment();
const duration = now.diff(this.runningSince || now);
Logger.tag('jobs').profile('[%sms]\t%s', duration, this.name);
@ -55,13 +80,14 @@ export class Task {
this.runningSince = null;
this.lastRunDuration = duration;
this.state = state;
this.result = result;
};
this.job.run().then(() => {
done(TaskState.IDLE);
this.job.run().then(result => {
done(TaskState.IDLE, result);
}).catch((err: any) => {
Logger.tag('jobs').error("Job %s failed: %s", this.name, err);
done(TaskState.FAILED);
done(TaskState.FAILED, null);
});
}
}
@ -92,7 +118,8 @@ function schedule(expr: string, job: Job): void {
null,
null,
TaskState.IDLE,
true
null,
true,
);
cron.schedule(expr, task.run);

View file

@ -3,11 +3,11 @@ import _ from "lodash";
import CONSTRAINTS from "../validation/constraints";
import ErrorTypes from "../utils/errorTypes";
import * as Resources from "../utils/resources";
import {getTasks, Task} from "../jobs/scheduler";
import {Entity} from "../utils/resources";
import {getTasks, Task, TaskState} from "../jobs/scheduler";
import {normalizeString} from "../utils/strings";
import {forConstraint} from "../validation/validator";
import {Request, Response} from "express";
import {Entity} from "../utils/resources";
const isValidId = forConstraint(CONSTRAINTS.id, false);
@ -20,6 +20,8 @@ interface ExternalTask {
lastRunStarted: number | null,
lastRunDuration: number | null,
state: string,
result: string | null,
message: string | null,
enabled: boolean,
}
@ -33,6 +35,8 @@ function toExternalTask(task: Task): ExternalTask {
lastRunStarted: task.lastRunStarted && task.lastRunStarted.unix(),
lastRunDuration: task.lastRunDuration || null,
state: task.state,
result: task.state !== TaskState.RUNNING && task.result ? task.result.state : null,
message:task.state !== TaskState.RUNNING && task.result ? task.result.message || null : null,
enabled: task.enabled
};
}

View file

@ -389,6 +389,8 @@ test('parseNodesJson() should succeed parsing no nodes', () => {
// then
expect(result.importTimestamp.isValid()).toBe(true);
expect(result.nodes).toEqual([]);
expect(result.failedNodesCount).toEqual(0);
expect(result.totalNodesCount).toEqual(0);
});
test('parseNodesJson() should skip parsing invalid nodes', () => {
@ -423,6 +425,8 @@ test('parseNodesJson() should skip parsing invalid nodes', () => {
// then
expect(result.importTimestamp.isValid()).toBe(true);
expect(result.nodes).toEqual([]);
expect(result.failedNodesCount).toEqual(2);
expect(result.totalNodesCount).toEqual(2);
expect(mockedLogger.getMessages('error', 'monitoring', 'parsing-nodes-json').length).toEqual(2);
});
@ -467,5 +471,7 @@ test('parseNodesJson() should parse valid nodes', () => {
expect(result.importTimestamp.isValid()).toBe(true);
expect(result.nodes).toEqual([expectedParsedNode]);
expect(result.failedNodesCount).toEqual(1);
expect(result.totalNodesCount).toEqual(2);
expect(mockedLogger.getMessages('error', 'monitoring', 'parsing-nodes-json').length).toEqual(1);
});

View file

@ -45,8 +45,15 @@ export type ParsedNode = {
export type NodesParsingResult = {
importTimestamp: Moment,
nodes: ParsedNode[],
failedNodesCount: number,
totalNodesCount: number,
}
export type RetrieveNodeInformationResult = {
failedParsingNodesCount: number,
totalNodesCount: number,
};
let previousImportTimestamp: Moment | null = null;
async function insertNodeInformation(nodeData: ParsedNode, node: Node): Promise<void> {
@ -228,7 +235,9 @@ export function parseNodesJson (body: string): NodesParsingResult {
const result: NodesParsingResult = {
importTimestamp: parseTimestamp(json.timestamp),
nodes: []
nodes: [],
failedNodesCount: 0,
totalNodesCount: 0,
};
if (!result.importTimestamp.isValid()) {
@ -240,12 +249,14 @@ export function parseNodesJson (body: string): NodesParsingResult {
}
for (const nodeData of json.nodes) {
result.totalNodesCount += 1;
try {
const parsedNode = parseNode(result.importTimestamp, nodeData);
Logger.tag('monitoring', 'parsing-nodes-json').debug(`Parsing node successful: ${parsedNode.mac}`);
result.nodes.push(parsedNode);
}
catch (error) {
result.failedNodesCount += 1;
Logger.tag('monitoring', 'parsing-nodes-json').error("Could not parse node.", error, nodeData);
}
}
@ -434,11 +445,15 @@ async function withUrlsData(urls: string[]): Promise<NodesParsingResult[]> {
return results;
}
async function retrieveNodeInformationForUrls(urls: string[]): Promise<void> {
async function retrieveNodeInformationForUrls(urls: string[]): Promise<RetrieveNodeInformationResult> {
const datas = await withUrlsData(urls);
let maxTimestamp = datas[0].importTimestamp;
let minTimestamp = maxTimestamp;
let failedParsingNodesCount = 0;
let totalNodesCount = 0;
for (const data of datas) {
if (data.importTimestamp.isAfter(maxTimestamp)) {
maxTimestamp = data.importTimestamp;
@ -446,6 +461,9 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise<void> {
if (data.importTimestamp.isBefore(minTimestamp)) {
minTimestamp = data.importTimestamp;
}
failedParsingNodesCount += data.failedNodesCount;
totalNodesCount += data.totalNodesCount;
}
if (previousImportTimestamp !== null && !maxTimestamp.isAfter(previousImportTimestamp)) {
@ -456,7 +474,10 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise<void> {
maxTimestamp.format(),
previousImportTimestamp.format()
);
return;
return {
failedParsingNodesCount,
totalNodesCount,
};
}
previousImportTimestamp = maxTimestamp;
@ -502,6 +523,11 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise<void> {
minTimestamp.unix()
]
);
return {
failedParsingNodesCount,
totalNodesCount,
}
}
export async function getAll(restParams: RestParams): Promise<{total: number, monitoringStates: any[]}> {
@ -605,7 +631,7 @@ export async function disable(token: string): Promise<Node> {
return newNode;
}
export async function retrieveNodeInformation(): Promise<void> {
export async function retrieveNodeInformation(): Promise<RetrieveNodeInformationResult> {
const urls = config.server.map.nodesJsonUrl;
if (_.isEmpty(urls)) {
throw new Error('No nodes.json-URLs set. Please adjust config.json: server.map.nodesJsonUrl')