Utilize locks to prevent certain edge case issues

This commit is contained in:
Zaid Humaimidi committed 2023-12-06 11:10:23 -08:00
1 parent f4596db9f7
commit 342210faf1
7 files changed
+323 -269

No files matched your search

+43 -101
View File
@@ -20,9 +20,8 @@
import { ObjectId } from "mongodb";
import { collections } from "./db/db";
import {
count,
deleteConfig,
deleteFault,
deleteFault as dbDeleteFault,
deleteFile,
deletePermission,
deletePreset,
@@ -39,7 +38,7 @@ import {
} from "./ui/db";
import * as common from "./util";
import * as cache from "./cache";
import { lockExists } from "./lock";
import { acquireLock, getToken, releaseLock } from "./lock";
import {
getRevision,
getConfig,
@@ -54,6 +53,7 @@ import { Expression, Task } from "./types";
import { evaluate } from "./common/expression/util";
import { hashPassword } from "./auth";
import { flattenDevice } from "./ui/db";
import { ResourceLockedError } from "./common/errors";
export async function connectionRequest(
deviceId: string,
@@ -212,7 +212,8 @@ export async function awaitSessionStart(
);
const li = (device["_lastInform"] as Date).getTime();
if (li > lastInform) return true;
if (await lockExists(`cwmp_session_${deviceId}`)) return true;
const token = await getToken(`cwmp_session_${deviceId}`);
if (token?.startsWith("cwmp_session_")) return true;
if (timeout < 500) return false;
await new Promise((resolve) => setTimeout(resolve, 500));
timeout -= Date.now() - now;
@@ -224,7 +225,8 @@ export async function awaitSessionEnd(
timeout: number
): Promise<boolean> {
const now = Date.now();
if (!(await lockExists(`cwmp_session_${deviceId}`))) return true;
const token = await getToken(`cwmp_session_${deviceId}`);
if (!token?.startsWith("cwmp_session_")) return true;
if (timeout < 500) return false;
await new Promise((resolve) => setTimeout(resolve, 500));
timeout -= Date.now() - now;
@@ -354,20 +356,41 @@ export async function insertTasks(tasks: any[]): Promise<Task[]> {
}
export async function deleteDevice(deviceId: string): Promise<void> {
await Promise.all([
collections.tasks.deleteMany({ device: deviceId }),
collections.devices.deleteOne({ _id: deviceId }),
collections.faults.deleteMany({
_id: {
$regex: `^${common.escapeRegExp(deviceId)}\\:`,
},
}),
collections.operations.deleteMany({
_id: {
$regex: `^${common.escapeRegExp(deviceId)}\\:`,
},
}),
]);
const token = await acquireLock(`cwmp_session_${deviceId}`, 5000);
if (!token) throw new ResourceLockedError("Device is in session");
try {
await Promise.all([
collections.tasks.deleteMany({ device: deviceId }),
collections.devices.deleteOne({ _id: deviceId }),
collections.faults.deleteMany({
_id: {
$regex: `^${common.escapeRegExp(deviceId)}\\:`,
},
}),
collections.operations.deleteMany({
_id: {
$regex: `^${common.escapeRegExp(deviceId)}\\:`,
},
}),
]);
} finally {
await releaseLock(`cwmp_session_${deviceId}`, token);
}
}
export async function deleteFault(id: string): Promise<void> {
const deviceId = id.split(":", 1)[0];
const channel = id.slice(deviceId.length + 1);
const token = await acquireLock(`cwmp_session_${deviceId}`, 5000);
if (!token) throw new ResourceLockedError("Device is in session");
try {
const proms = [dbDeleteFault(id)];
if (channel.startsWith("task_"))
proms.push(deleteTask(new ObjectId(channel.slice(5))));
await Promise.all(proms);
} finally {
await releaseLock(`cwmp_session_${deviceId}`, token);
}
}
export async function deleteResource(
@@ -380,12 +403,7 @@ export async function deleteResource(
await deleteFile(id);
await cache.del("cwmp-local-cache-hash");
} else if (resource === "faults") {
const deviceId = id.split(":", 1)[0];
const channel = id.slice(deviceId.length + 1);
const proms = [deleteFault(id)];
if (channel.startsWith("task_"))
proms.push(deleteTask(new ObjectId(channel.slice(5))));
await Promise.all(proms);
await deleteFault(id);
} else if (resource === "provisions") {
await deleteProvision(id);
await cache.del("cwmp-local-cache-hash");
@@ -412,82 +430,6 @@ export async function deleteResource(
}
}
export async function postTasks(
deviceId: string,
tasks: Task[],
timeout: number,
device: Record<string, { value?: [boolean | number | string, string] }>
): Promise<{ connectionRequest: string; tasks: any[] }> {
for (const task of tasks) {
delete task._id;
task["device"] = deviceId;
}
tasks = await insertTasks(tasks);
const statuses = tasks.map((t) => {
return { _id: t._id, status: "pending" };
});
const lastInform = Date.now();
const notInSession = await awaitSessionEnd(deviceId, 30000);
if (!notInSession) {
await Promise.all(statuses.map((t) => deleteTask(new ObjectId(t._id))));
return {
connectionRequest: "Session took too long to complete",
tasks: statuses,
};
}
const status = await connectionRequest(deviceId, device);
if (status) {
await Promise.all(statuses.map((t) => 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) => 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) => deleteTask(new ObjectId(t._id))));
return {
connectionRequest: "Session took too long to complete",
tasks: statuses,
};
}
const promises: Promise<number>[] = [];
for (const s of statuses) {
promises.push(count("tasks", ["=", ["PARAM", "_id"], s._id]));
promises.push(
count("faults", ["=", ["PARAM", "_id"], `${deviceId}:task_${s._id}`])
);
}
const res = await Promise.all(promises);
for (const [i, r] of statuses.entries()) {
if (!res[i * 2]) {
r.status = "done";
} else if (res[i * 2 + 1]) {
r.status = "fault";
r["fault"] = res[i * 2 + 1][0];
}
}
await Promise.all(statuses.map((t) => deleteTask(new ObjectId(t._id))));
return { connectionRequest: "OK", tasks: statuses };
}
// TODO Implement validation
export async function putResource(
resource: string,