Read session lock to detect task completion

This commit is contained in:
Zaid Abdulla committed 2020-11-30 18:11:51 -08:00
1 parent 30bea8ac57
commit fa1e40e370
4 files changed
+173 -123

No files matched your search

+41 -30
View File
@@ -37,7 +37,7 @@ import { evaluate } from "./common/expression";
export async function connectionRequest( export async function connectionRequest(
deviceId: string, deviceId: string,
device?: Record<string, { value?: [boolean | number | string, string] }> device?: Record<string, { value?: [boolean | number | string, string] }>
): Promise<void> { ): Promise<string> {
if (!device) { if (!device) {
const res = await db.devicesCollection.findOne({ _id: deviceId }); const res = await db.devicesCollection.findOne({ _id: deviceId });
if (!res) throw new Error("No such device"); if (!res) throw new Error("No such device");
@@ -156,43 +156,54 @@ export async function connectionRequest(
); );
} }
try { const status = await httpConnectionRequest(
await httpConnectionRequest( connectionRequestUrl,
connectionRequestUrl, authExp,
authExp, CONNECTION_REQUEST_ALLOW_BASIC_AUTH,
CONNECTION_REQUEST_ALLOW_BASIC_AUTH, CONNECTION_REQUEST_TIMEOUT,
CONNECTION_REQUEST_TIMEOUT, debug,
debug, deviceId
deviceId );
);
} catch (err) { if (udpProm) {
if (!udpProm) throw err;
await udpProm; await udpProm;
return "";
} }
return status;
} }
export async function watchTask( export async function awaitSessionStart(
deviceId: string, deviceId: string,
taskId: string, lastInform: number,
timeout: number timeout: number
): Promise<string> { ): Promise<boolean> {
await new Promise((resolve) => setTimeout(resolve, 500)); const now = Date.now();
const device = await db.devicesCollection.findOne(
const task = await db.tasksCollection.findOne( { _id: deviceId },
{ _id: taskId }, { projection: { _lastInform: 1 } }
{ projection: { _id: 1 } }
); );
if (!task) return "completed"; const li = (device["_lastInform"] as Date).getTime();
if (li > lastInform) return true;
const token = await cache.get(`cwmp_session_${deviceId}`);
if (token) return true;
if (timeout < 500) return false;
await new Promise((resolve) => setTimeout(resolve, 500));
timeout -= Date.now() - now;
return awaitSessionStart(deviceId, lastInform, timeout);
}
const q = { _id: `${deviceId}:task_${taskId}` }; export async function awaitSessionEnd(
const fault = await db.faultsCollection.findOne(q, { deviceId: string,
projection: { _id: 1 }, timeout: number
}); ): Promise<boolean> {
if (fault) return "fault"; const now = Date.now();
const token = await cache.get(`cwmp_session_${deviceId}`);
if ((timeout -= 500) <= 0) return "timeout"; if (!token) return true;
if (timeout < 500) return false;
return watchTask(deviceId, taskId, timeout); await new Promise((resolve) => setTimeout(resolve, 500));
timeout -= Date.now() - now;
return awaitSessionEnd(deviceId, timeout);
} }
function sanitizeTask(task): void { function sanitizeTask(task): void {
+8 -11
View File
@@ -97,10 +97,10 @@ export async function httpConnectionRequest(
timeout: number, timeout: number,
_debug: boolean, _debug: boolean,
deviceId: string deviceId: string
): Promise<void> { ): Promise<string> {
const options: http.RequestOptions = parse(address); const options: http.RequestOptions = parse(address);
if (options.protocol !== "http:") if (options.protocol !== "http:")
throw new Error("Invalid connection request URL or protocol"); return "Invalid connection request URL or protocol";
options.agent = new http.Agent({ options.agent = new http.Agent({
maxSockets: 1, maxSockets: 1,
@@ -115,8 +115,7 @@ export async function httpConnectionRequest(
let opts = options; let opts = options;
if (authHeader) { if (authHeader) {
if (authHeader["method"] === "Basic") { if (authHeader["method"] === "Basic") {
if (!allowBasicAuth) if (!allowBasicAuth) return "Basic HTTP authentication not allowed";
throw new Error("Basic HTTP authentication not allowed");
opts = Object.assign( opts = Object.assign(
{ {
@@ -143,7 +142,7 @@ export async function httpConnectionRequest(
options options
); );
} else { } else {
throw new Error("Unrecognized auth method"); return "Unrecognized auth method";
} }
} }
@@ -152,8 +151,8 @@ export async function httpConnectionRequest(
// Workaround for some devices unexpectedly closing the connection // Workaround for some devices unexpectedly closing the connection
if (res.statusCode === 0 && authHeader) if (res.statusCode === 0 && authHeader)
res = await httpGet(opts, timeout, _debug, deviceId); res = await httpGet(opts, timeout, _debug, deviceId);
if (res.statusCode === 0) throw new Error("Device is offline"); if (res.statusCode === 0) return "Device is offline";
if (res.statusCode === 200 || res.statusCode === 204) return; if (res.statusCode === 200 || res.statusCode === 204) return "";
if (res.statusCode === 401 && res.headers["www-authenticate"]) { if (res.statusCode === 401 && res.headers["www-authenticate"]) {
authHeader = auth.parseWwwAuthenticateHeader( authHeader = auth.parseWwwAuthenticateHeader(
@@ -161,12 +160,10 @@ export async function httpConnectionRequest(
); );
[username, password, authExp] = await extractAuth(authExp, false); [username, password, authExp] = await extractAuth(authExp, false);
} else { } else {
throw new Error( return `Unexpected response code from device: ${res.statusCode}`;
`Unexpected response code from device: ${res.statusCode}`
);
} }
} }
throw new Error("Incorrect connection request credentials"); return "Incorrect connection request credentials";
} }
export async function udpConnectionRequest( export async function udpConnectionRequest(
+90 -71
View File
@@ -392,78 +392,97 @@ export function listener(
task.device = deviceId; task.device = deviceId;
apiFunctions apiFunctions
.insertTasks(task) .insertTasks(task)
.then(() => { .then(async () => {
cache const lastInform = Date.now();
.del(`${deviceId}_tasks_faults_operations`) const notInSession = await apiFunctions.awaitSessionEnd(
.then(() => { deviceId,
if (urlParts.query.connection_request != null) { 30000
apiFunctions );
.connectionRequest(deviceId) await cache.del(`${deviceId}_tasks_faults_operations`);
.then(() => { if (urlParts.query.connection_request == null) {
const taskTimeout = response.writeHead(202, {
(urlParts.query.timeout && "Content-Type": "application/json",
parseInt(urlParts.query.timeout as string)) ||
(config.get(
"DEVICE_ONLINE_THRESHOLD",
deviceId
) as number);
apiFunctions
.watchTask(deviceId, task._id, taskTimeout)
.then((status) => {
if (status === "timeout") {
response.writeHead(
202,
"Task queued but not processed",
{
"Content-Type": "application/json",
}
);
response.end(JSON.stringify(task));
} else if (status === "fault") {
collections.tasks.findOne(
{ _id: task._id },
(err, task2) => {
if (err)
return void throwError(err, response);
response.writeHead(202, "Task faulted", {
"Content-Type": "application/json",
});
response.end(JSON.stringify(task2));
}
);
} else {
response.writeHead(200, {
"Content-Type": "application/json",
});
response.end(JSON.stringify(task));
}
})
.catch((err) => {
setTimeout(() => {
throwError(err, response);
});
});
})
.catch((err) => {
response.writeHead(202, err.message, {
"Content-Type": "application/json",
});
response.end(JSON.stringify(task));
});
} else {
response.writeHead(202, {
"Content-Type": "application/json",
});
response.end(JSON.stringify(task));
}
})
.catch((err) => {
setTimeout(() => {
throwError(err, response);
});
}); });
response.end(JSON.stringify(task));
return;
}
if (!notInSession) {
response.writeHead(202, "Task queued but not processed", {
"Content-Type": "application/json",
});
response.end(JSON.stringify(task));
return;
}
const status = await apiFunctions.connectionRequest(deviceId);
if (status) {
response.writeHead(202, status, {
"Content-Type": "application/json",
});
response.end(JSON.stringify(task));
return;
}
const onlineThreshold =
(urlParts.query.timeout &&
parseInt(urlParts.query.timeout as string)) ||
(config.get("DEVICE_ONLINE_THRESHOLD", deviceId) as number);
const sessionStarted = await apiFunctions.awaitSessionStart(
deviceId,
lastInform,
onlineThreshold
);
if (!sessionStarted) {
response.writeHead(202, "Task queued but not processed", {
"Content-Type": "application/json",
});
response.end(JSON.stringify(task));
return;
}
const sessionEnded = await apiFunctions.awaitSessionEnd(
deviceId,
120000
);
if (!sessionEnded) {
response.writeHead(202, "Task queued but not processed", {
"Content-Type": "application/json",
});
response.end(JSON.stringify(task));
return;
}
const prom1 = collections.tasks.findOne(
{ _id: task._id },
{ projection: { _id: 1 } }
);
const prom2 = collections.faults.findOne(
{ _id: `${deviceId}:task_${task._id}` },
{
projection: { _id: 1 },
}
);
const [t, f] = await Promise.all([prom1, prom2]);
if (f) {
response.writeHead(202, "Task faulted", {
"Content-Type": "application/json",
});
response.end(JSON.stringify(t || task));
} else if (t) {
response.writeHead(202, "Task queued but not processed", {
"Content-Type": "application/json",
});
response.end(JSON.stringify(t));
} else {
response.writeHead(200, {
"Content-Type": "application/json",
});
response.end(JSON.stringify(task));
}
}) })
.catch((err) => { .catch((err) => {
setTimeout(() => { setTimeout(() => {
+34 -11
View File
@@ -24,7 +24,8 @@ import { getUsers } from "../local-cache";
import { hashPassword } from "../auth"; import { hashPassword } from "../auth";
import { import {
insertTasks, insertTasks,
watchTask, awaitSessionEnd,
awaitSessionStart,
connectionRequest, connectionRequest,
deleteDevice, deleteDevice,
} from "../api-functions"; } from "../api-functions";
@@ -104,23 +105,45 @@ export async function postTasks(
const statuses = tasks.map((t) => { const statuses = tasks.map((t) => {
return { _id: t._id, status: "pending" }; return { _id: t._id, status: "pending" };
}); });
const lastInform = Date.now();
await del(`${deviceId}_tasks_faults_operations`); const notInSession = await awaitSessionEnd(deviceId, 30000);
if (!notInSession) {
try {
await connectionRequest(deviceId, device);
} catch (err) {
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id)))); await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id))));
return { return {
connectionRequest: err.message, connectionRequest: "Session took too long to complete",
tasks: statuses, tasks: statuses,
}; };
} }
const sample = tasks[tasks.length - 1]; await del(`${deviceId}_tasks_faults_operations`);
// Waiting for session to finish or timeout const status = await connectionRequest(deviceId, device);
await watchTask(deviceId, sample._id, timeout);
if (status) {
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id))));
return {
connectionRequest: status,
tasks: statuses,
};
}
const sessionStarted = await awaitSessionStart(deviceId, lastInform, timeout);
if (!sessionStarted) {
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id))));
return {
connectionRequest: "No contact from CPE",
tasks: statuses,
};
}
const sessionEnded = await awaitSessionEnd(deviceId, 120000);
if (!sessionEnded) {
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id))));
return {
connectionRequest: "Session took too long to complete",
tasks: statuses,
};
}
const promises = []; const promises = [];
for (const s of statuses) { for (const s of statuses) {