@oneuptime/common 11.5.11 → 11.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/Models/AnalyticsModels/NetworkFlow.ts +12 -1
  2. package/Server/API/StatusPageAPI.ts +14 -1
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +0 -2
  4. package/Server/Services/IncidentService.ts +62 -67
  5. package/Server/Services/MonitorGroupService.ts +15 -2
  6. package/Server/Services/MonitorService.ts +91 -3
  7. package/Server/Services/MonitorStatusTimelineService.ts +285 -100
  8. package/Server/Services/StatusPageService.ts +59 -34
  9. package/Server/Utils/Monitor/MonitorResource.ts +43 -14
  10. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +32 -7
  11. package/Server/Utils/Monitor/MonitorStatusTimelineReconciler.ts +362 -0
  12. package/Tests/Server/Services/MonitorStatusTimelineService.test.ts +235 -0
  13. package/Tests/Server/Utils/Monitor/MonitorStatusTimelineReconciler.test.ts +555 -0
  14. package/Tests/Types/Database/CompareOperatorWireSerialization.test.ts +216 -0
  15. package/Tests/Types/Database/GreaterThan.test.ts +5 -1
  16. package/Tests/Types/Database/GreaterThanOrEqual.test.ts +5 -1
  17. package/Tests/Types/Database/GreaterThanOrNull.test.ts +24 -1
  18. package/Tests/Types/Database/LessThan.test.ts +5 -1
  19. package/Tests/Types/Database/LessThanOrEqual.test.ts +24 -1
  20. package/Tests/Types/Database/LessThanOrNull.test.ts +5 -1
  21. package/Tests/Utils/Uptime/UptimeUtil.test.ts +726 -0
  22. package/Types/BaseDatabase/GreaterThan.ts +8 -1
  23. package/Types/BaseDatabase/GreaterThanOrEqual.ts +8 -1
  24. package/Types/BaseDatabase/GreaterThanOrNull.ts +9 -1
  25. package/Types/BaseDatabase/LessThan.ts +8 -1
  26. package/Types/BaseDatabase/LessThanOrEqual.ts +9 -1
  27. package/Types/BaseDatabase/LessThanOrNull.ts +8 -1
  28. package/Utils/StatusPage/ResourceUptime.ts +11 -1
  29. package/Utils/Uptime/UptimeUtil.ts +142 -36
  30. package/build/dist/Models/AnalyticsModels/NetworkFlow.js +12 -1
  31. package/build/dist/Models/AnalyticsModels/NetworkFlow.js.map +1 -1
  32. package/build/dist/Server/API/StatusPageAPI.js +12 -2
  33. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  34. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +0 -2
  35. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  36. package/build/dist/Server/Services/IncidentService.js +47 -56
  37. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  38. package/build/dist/Server/Services/MonitorGroupService.js +15 -2
  39. package/build/dist/Server/Services/MonitorGroupService.js.map +1 -1
  40. package/build/dist/Server/Services/MonitorService.js +66 -3
  41. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  42. package/build/dist/Server/Services/MonitorStatusTimelineService.js +244 -102
  43. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  44. package/build/dist/Server/Services/StatusPageService.js +57 -33
  45. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  46. package/build/dist/Server/Utils/Monitor/MonitorResource.js +34 -12
  47. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  48. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +25 -8
  49. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  50. package/build/dist/Server/Utils/Monitor/MonitorStatusTimelineReconciler.js +268 -0
  51. package/build/dist/Server/Utils/Monitor/MonitorStatusTimelineReconciler.js.map +1 -0
  52. package/build/dist/Types/BaseDatabase/GreaterThan.js +8 -1
  53. package/build/dist/Types/BaseDatabase/GreaterThan.js.map +1 -1
  54. package/build/dist/Types/BaseDatabase/GreaterThanOrEqual.js +8 -1
  55. package/build/dist/Types/BaseDatabase/GreaterThanOrEqual.js.map +1 -1
  56. package/build/dist/Types/BaseDatabase/GreaterThanOrNull.js +9 -1
  57. package/build/dist/Types/BaseDatabase/GreaterThanOrNull.js.map +1 -1
  58. package/build/dist/Types/BaseDatabase/LessThan.js +8 -1
  59. package/build/dist/Types/BaseDatabase/LessThan.js.map +1 -1
  60. package/build/dist/Types/BaseDatabase/LessThanOrEqual.js +9 -1
  61. package/build/dist/Types/BaseDatabase/LessThanOrEqual.js.map +1 -1
  62. package/build/dist/Types/BaseDatabase/LessThanOrNull.js +8 -1
  63. package/build/dist/Types/BaseDatabase/LessThanOrNull.js.map +1 -1
  64. package/build/dist/Utils/StatusPage/ResourceUptime.js +4 -1
  65. package/build/dist/Utils/StatusPage/ResourceUptime.js.map +1 -1
  66. package/build/dist/Utils/Uptime/UptimeUtil.js +86 -33
  67. package/build/dist/Utils/Uptime/UptimeUtil.js.map +1 -1
  68. package/package.json +1 -1
  69. package/Server/Infrastructure/Postgres/SchemaMigrations/1784401962564-MigrationName.ts +0 -101
  70. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784401962564-MigrationName.js +0 -40
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784401962564-MigrationName.js.map +0 -1
@@ -0,0 +1,235 @@
1
+ import ObjectID from "../../../Types/ObjectID";
2
+
3
+ /*
4
+ * These tests pin the ownership of the per-monitor mutex in
5
+ * MonitorStatusTimelineService.create() - the prevention layer that stops new
6
+ * orphaned (endsAt = NULL) timeline rows at the source:
7
+ *
8
+ * - fail-closed: a lock that cannot be acquired refuses the write with the
9
+ * exact MONITOR_STATUS_TIMELINE_LOCK_ERROR_MESSAGE callers match on,
10
+ * instead of falling through and inserting unlocked (the original bug),
11
+ * - release-on-every-path: the mutex is released whether the create
12
+ * succeeds or throws (a leaked redis-semaphore mutex refreshes its own
13
+ * key for the life of the process, blocking every later create for the
14
+ * monitor until acquireTimeout),
15
+ * - the feed/workspace-notification side effects run only AFTER release,
16
+ * so third-party HTTP latency (Slack/Teams) can never extend the
17
+ * critical section,
18
+ * - the ignoreHooks and missing-monitorId paths skip locking entirely.
19
+ *
20
+ * Semaphore is mocked at the module boundary; DatabaseService.prototype.create
21
+ * is spied so super.create() is observable without a database.
22
+ */
23
+
24
+ const lockMock: jest.Mock = jest.fn();
25
+ const releaseMock: jest.Mock = jest.fn();
26
+
27
+ jest.mock("../../../Server/Infrastructure/Semaphore", () => {
28
+ return {
29
+ __esModule: true,
30
+ default: {
31
+ lock: (...args: Array<unknown>) => {
32
+ return lockMock(...args);
33
+ },
34
+ release: (...args: Array<unknown>) => {
35
+ return releaseMock(...args);
36
+ },
37
+ },
38
+ };
39
+ });
40
+
41
+ import MonitorStatusTimelineService, {
42
+ MONITOR_STATUS_TIMELINE_LOCK_ERROR_MESSAGE,
43
+ } from "../../../Server/Services/MonitorStatusTimelineService";
44
+ import DatabaseService from "../../../Server/Services/DatabaseService";
45
+ import MonitorStatusTimeline from "../../../Models/DatabaseModels/MonitorStatusTimeline";
46
+ import CreateBy from "../../../Server/Types/Database/CreateBy";
47
+ import ServerException from "../../../Types/Exception/ServerException";
48
+
49
+ const MONITOR_ID: ObjectID = new ObjectID(
50
+ "11111111-1111-4111-8111-111111111111",
51
+ );
52
+ const PROJECT_ID: ObjectID = new ObjectID(
53
+ "22222222-2222-4222-8222-222222222222",
54
+ );
55
+ const STATUS_ID: ObjectID = new ObjectID(
56
+ "33333333-3333-4333-8333-333333333333",
57
+ );
58
+
59
+ type MakeCreateByFunction = (data?: {
60
+ ignoreHooks?: boolean;
61
+ omitMonitorId?: boolean;
62
+ }) => CreateBy<MonitorStatusTimeline>;
63
+
64
+ const makeCreateBy: MakeCreateByFunction = (data?: {
65
+ ignoreHooks?: boolean;
66
+ omitMonitorId?: boolean;
67
+ }): CreateBy<MonitorStatusTimeline> => {
68
+ const timeline: MonitorStatusTimeline = new MonitorStatusTimeline();
69
+
70
+ if (!data?.omitMonitorId) {
71
+ timeline.monitorId = MONITOR_ID;
72
+ }
73
+
74
+ timeline.projectId = PROJECT_ID;
75
+ timeline.monitorStatusId = STATUS_ID;
76
+
77
+ return {
78
+ data: timeline,
79
+ props: {
80
+ isRoot: true,
81
+ ignoreHooks: data?.ignoreHooks || false,
82
+ },
83
+ };
84
+ };
85
+
86
+ describe("MonitorStatusTimelineService.create mutex ownership", () => {
87
+ let superCreateSpy: jest.SpyInstance;
88
+ let feedItemSpy: jest.SpyInstance;
89
+ let createdItem: MonitorStatusTimeline;
90
+
91
+ // a unique object standing in for the redis-semaphore mutex.
92
+ const fakeMutex: { id: string } = { id: "fake-mutex" };
93
+
94
+ beforeEach(() => {
95
+ lockMock.mockReset();
96
+ releaseMock.mockReset();
97
+
98
+ lockMock.mockResolvedValue(fakeMutex);
99
+ releaseMock.mockResolvedValue(undefined);
100
+
101
+ createdItem = new MonitorStatusTimeline();
102
+ createdItem.monitorId = MONITOR_ID;
103
+ createdItem.projectId = PROJECT_ID;
104
+ createdItem.monitorStatusId = STATUS_ID;
105
+
106
+ /*
107
+ * super.create - everything between lock and release (hooks, validation,
108
+ * the INSERT) is DatabaseService.create's responsibility and is not under
109
+ * test here.
110
+ */
111
+ superCreateSpy = jest
112
+ .spyOn(DatabaseService.prototype, "create")
113
+ .mockResolvedValue(createdItem);
114
+
115
+ /*
116
+ * The feed side effect does DB lookups and third-party HTTP; stub it and
117
+ * assert WHEN it runs relative to the release.
118
+ */
119
+ feedItemSpy = jest
120
+ .spyOn(
121
+ MonitorStatusTimelineService as unknown as {
122
+ createStatusChangeFeedItem: () => Promise<void>;
123
+ },
124
+ "createStatusChangeFeedItem",
125
+ )
126
+ .mockResolvedValue(undefined);
127
+ });
128
+
129
+ afterEach(() => {
130
+ superCreateSpy.mockRestore();
131
+ feedItemSpy.mockRestore();
132
+ });
133
+
134
+ it("acquires the per-monitor lock, creates, then releases", async () => {
135
+ const result: MonitorStatusTimeline =
136
+ await MonitorStatusTimelineService.create(makeCreateBy());
137
+
138
+ expect(result).toBe(createdItem);
139
+
140
+ expect(lockMock).toHaveBeenCalledTimes(1);
141
+ expect(lockMock).toHaveBeenCalledWith({
142
+ key: MONITOR_ID.toString(),
143
+ namespace: "MonitorStatusTimeline.create",
144
+ });
145
+
146
+ expect(superCreateSpy).toHaveBeenCalledTimes(1);
147
+ expect(releaseMock).toHaveBeenCalledTimes(1);
148
+ expect(releaseMock).toHaveBeenCalledWith(fakeMutex);
149
+
150
+ // lock -> create -> release, in that order.
151
+ expect(lockMock.mock.invocationCallOrder[0]!).toBeLessThan(
152
+ superCreateSpy.mock.invocationCallOrder[0]!,
153
+ );
154
+ expect(superCreateSpy.mock.invocationCallOrder[0]!).toBeLessThan(
155
+ releaseMock.mock.invocationCallOrder[0]!,
156
+ );
157
+ });
158
+
159
+ it("runs the feed side effect only after the mutex is released", async () => {
160
+ await MonitorStatusTimelineService.create(makeCreateBy());
161
+
162
+ expect(feedItemSpy).toHaveBeenCalledTimes(1);
163
+ expect(releaseMock.mock.invocationCallOrder[0]!).toBeLessThan(
164
+ feedItemSpy.mock.invocationCallOrder[0]!,
165
+ );
166
+ });
167
+
168
+ it("fails closed with the exact lock error message and never inserts unlocked", async () => {
169
+ lockMock.mockRejectedValue(new Error("redis unavailable"));
170
+
171
+ await expect(
172
+ MonitorStatusTimelineService.create(makeCreateBy()),
173
+ ).rejects.toThrow(MONITOR_STATUS_TIMELINE_LOCK_ERROR_MESSAGE);
174
+
175
+ await expect(
176
+ MonitorStatusTimelineService.create(makeCreateBy()),
177
+ ).rejects.toBeInstanceOf(ServerException);
178
+
179
+ /*
180
+ * The whole point of fail-closed: the write must NOT happen without the
181
+ * lock. The pre-fix code logged and fell through unlocked, which is what
182
+ * produced permanently orphaned endsAt = NULL rows.
183
+ */
184
+ expect(superCreateSpy).not.toHaveBeenCalled();
185
+ expect(releaseMock).not.toHaveBeenCalled();
186
+ });
187
+
188
+ it("releases the mutex when the create itself throws", async () => {
189
+ superCreateSpy.mockRejectedValue(new Error("insert failed"));
190
+
191
+ await expect(
192
+ MonitorStatusTimelineService.create(makeCreateBy()),
193
+ ).rejects.toThrow("insert failed");
194
+
195
+ // release must run on the throw path - a leaked mutex never expires.
196
+ expect(releaseMock).toHaveBeenCalledTimes(1);
197
+ expect(releaseMock).toHaveBeenCalledWith(fakeMutex);
198
+
199
+ // and the feed side effect must not run for a failed create.
200
+ expect(feedItemSpy).not.toHaveBeenCalled();
201
+ });
202
+
203
+ it("does not fail a successful create when the release itself fails", async () => {
204
+ releaseMock.mockRejectedValue(new Error("redis blip on release"));
205
+
206
+ const result: MonitorStatusTimeline =
207
+ await MonitorStatusTimelineService.create(makeCreateBy());
208
+
209
+ expect(result).toBe(createdItem);
210
+ expect(superCreateSpy).toHaveBeenCalledTimes(1);
211
+ });
212
+
213
+ it("skips locking entirely when ignoreHooks is set", async () => {
214
+ const result: MonitorStatusTimeline =
215
+ await MonitorStatusTimelineService.create(
216
+ makeCreateBy({ ignoreHooks: true }),
217
+ );
218
+
219
+ expect(result).toBe(createdItem);
220
+ expect(lockMock).not.toHaveBeenCalled();
221
+ expect(releaseMock).not.toHaveBeenCalled();
222
+ // no hooks -> no predecessor bookkeeping -> no feed side effect either.
223
+ expect(feedItemSpy).not.toHaveBeenCalled();
224
+ });
225
+
226
+ it("skips locking when monitorId is missing and lets onBeforeCreate reject it", async () => {
227
+ await MonitorStatusTimelineService.create(
228
+ makeCreateBy({ omitMonitorId: true }),
229
+ );
230
+
231
+ // super.create's onBeforeCreate throws BadDataException for this case.
232
+ expect(lockMock).not.toHaveBeenCalled();
233
+ expect(superCreateSpy).toHaveBeenCalledTimes(1);
234
+ });
235
+ });