Skip to content

Commit c7ef412

Browse files
committed
atomic update
1 parent 273aa7d commit c7ef412

3 files changed

Lines changed: 153 additions & 1 deletion

File tree

server/src/repositories/monitors/IMonitorsRepository.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type MonitorType, type Monitor, type MonitorsSummary } from "@/types/index.js";
1+
import { type MonitorType, type Monitor, type MonitorsSummary, CheckSnapshot } from "@/types/index.js";
22

33
export interface TeamQueryConfig {
44
limit?: number;
@@ -29,6 +29,14 @@ export interface IMonitorsRepository {
2929

3030
// update
3131
updateById(monitorId: string, teamId: string, updates: Partial<Monitor>): Promise<Monitor>;
32+
updateStatusWindowAndChecks(
33+
monitorId: string,
34+
teamId: string,
35+
status: boolean,
36+
checkSnapshot: CheckSnapshot,
37+
windowSize: number,
38+
maxRecentChecks: number
39+
): Promise<Monitor>;
3240
togglePauseById(monitorId: string, teamId: string): Promise<Monitor>;
3341
// delete
3442
deleteById(monitorId: string, teamId: string): Promise<Monitor>;

server/src/repositories/monitors/MongoMonitorsRepository.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,31 @@ class MongoMonitorsRepository implements IMonitorsRepository {
182182
return this.toEntity(updatedMonitor);
183183
};
184184

185+
updateStatusWindowAndChecks = async (
186+
monitorId: string,
187+
teamId: string,
188+
status: boolean,
189+
checkSnapshot: CheckSnapshot,
190+
windowSize: number,
191+
maxRecentChecks: number
192+
): Promise<Monitor> => {
193+
const updatedMonitor = await MonitorModel.findOneAndUpdate(
194+
{ _id: monitorId, teamId },
195+
{
196+
$push: {
197+
statusWindow: { $each: [status], $slice: -windowSize },
198+
recentChecks: { $each: [checkSnapshot], $slice: -maxRecentChecks },
199+
},
200+
},
201+
{ returnDocument: "after" }
202+
);
203+
204+
if (!updatedMonitor) {
205+
throw new AppError({ message: `Failed to update status and checks for monitor with id ${monitorId}`, status: 500 });
206+
}
207+
return this.toEntity(updatedMonitor);
208+
};
209+
185210
togglePauseById = async (monitorId: string, teamId: string) => {
186211
const monitor = await MonitorModel.findOneAndUpdate(
187212
{ _id: monitorId, teamId },

server/src/service/infrastructure/statusService.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,125 @@ export class StatusService implements IStatusService {
217217
return { nextStatus, transitioned, breaches, nextCounters };
218218
};
219219

220+
private updateStatusWindowAndRecentChecks = async (monitor: Monitor, check: Check) => {
221+
const checkSnapshot = this.toCheckSnapshot(check);
222+
return await this.monitorsRepository.updateStatusWindowAndChecks(
223+
monitor.id,
224+
monitor.teamId,
225+
check.status,
226+
checkSnapshot,
227+
monitor.statusWindowSize,
228+
MAX_RECENT_CHECKS
229+
);
230+
};
231+
232+
newUpdateMonitorStatus = async (
233+
statusResponse: MonitorStatusResponse<
234+
| PingStatusPayload
235+
| HttpStatusPayload
236+
| PageSpeedStatusPayload
237+
| HardwareStatusPayload
238+
| DockerStatusPayload
239+
| PortStatusPayload
240+
| GameStatusPayload
241+
| GrpcStatusPayload
242+
| undefined
243+
>,
244+
check: Check
245+
): Promise<StatusChangeResult> => {
246+
try {
247+
const { monitorId, teamId, status, code } = statusResponse;
248+
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
249+
250+
// Update running stats
251+
await this.tryUpdateRunningStats(monitor, statusResponse);
252+
const updatedMonitor = await this.updateStatusWindowAndRecentChecks(monitor, check);
253+
const prevStatus = updatedMonitor.status;
254+
255+
// Return early if not enough data points
256+
if (updatedMonitor.statusWindow.length < updatedMonitor.statusWindowSize) {
257+
updatedMonitor.status = status === true ? "up" : "down";
258+
const updated = await this.monitorsRepository.updateById(updatedMonitor.id, updatedMonitor.teamId, { status: updatedMonitor.status });
259+
return {
260+
monitor: updated,
261+
statusChanged: false,
262+
prevStatus,
263+
code,
264+
timestamp: Date.now(),
265+
};
266+
}
267+
268+
// With a full window, a single raw check must not change UNLESS we are initializing. Otherwise, only the sliding-window threshold can trigger a transition.
269+
let newStatus: MonitorStatus;
270+
if (updatedMonitor.status === "initializing") {
271+
newStatus = status === true ? "up" : "down";
272+
} else {
273+
newStatus = updatedMonitor.status;
274+
}
275+
276+
let statusChanged = false;
277+
278+
// First evaluate reachability-based status changes, which apply to all monitor types and take precedence over hardware breaches.
279+
const reachabilityResult = this.computeReachability(newStatus, updatedMonitor.statusWindow, updatedMonitor.statusWindowThreshold);
280+
if (reachabilityResult.transitioned) {
281+
newStatus = reachabilityResult.nextStatus;
282+
statusChanged = true;
283+
}
284+
285+
// Evaluate hardware threshold breaches (only for hardware monitors with metrics payload)
286+
let thresholdBreaches: HardwareBreaches | undefined;
287+
const hardwarePayload = statusResponse.payload as HardwareStatusPayload | undefined;
288+
if (updatedMonitor.type === "hardware" && hardwarePayload?.data) {
289+
const hardware = this.computeHardwareStatus({
290+
currentStatus: newStatus,
291+
reachabilityDown: newStatus === "down",
292+
metrics: hardwarePayload.data,
293+
thresholds: {
294+
cpu: updatedMonitor.cpuAlertThreshold,
295+
memory: updatedMonitor.memoryAlertThreshold,
296+
disk: updatedMonitor.diskAlertThreshold,
297+
temp: updatedMonitor.tempAlertThreshold,
298+
},
299+
counters: {
300+
cpu: updatedMonitor.cpuAlertCounter,
301+
memory: updatedMonitor.memoryAlertCounter,
302+
disk: updatedMonitor.diskAlertCounter,
303+
temp: updatedMonitor.tempAlertCounter,
304+
},
305+
});
306+
307+
updatedMonitor.cpuAlertCounter = hardware.nextCounters.cpu;
308+
updatedMonitor.memoryAlertCounter = hardware.nextCounters.memory;
309+
updatedMonitor.diskAlertCounter = hardware.nextCounters.disk;
310+
updatedMonitor.tempAlertCounter = hardware.nextCounters.temp;
311+
thresholdBreaches = hardware.breaches;
312+
if (hardware.transitioned) {
313+
newStatus = hardware.nextStatus;
314+
statusChanged = true;
315+
}
316+
}
317+
318+
// Apply the final status
319+
updatedMonitor.status = newStatus;
320+
321+
const updated = await this.monitorsRepository.updateById(updatedMonitor.id, updatedMonitor.teamId, updatedMonitor);
322+
323+
return {
324+
monitor: updated,
325+
statusChanged,
326+
prevStatus,
327+
code,
328+
timestamp: new Date().getTime(),
329+
thresholdBreaches,
330+
};
331+
} catch (error: unknown) {
332+
throw new AppError({
333+
message: `Failed to update monitor with id ${check.metadata.monitorId} with status: ${error instanceof Error ? error.message : "Unknown error"}`,
334+
service: SERVICE_NAME,
335+
method: "updateMonitorStatus",
336+
});
337+
}
338+
};
220339
updateMonitorStatus = async (
221340
statusResponse: MonitorStatusResponse<
222341
| PingStatusPayload

0 commit comments

Comments
 (0)