@oneuptime/common 7.0.4137 → 7.0.4144

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.
@@ -23,6 +23,7 @@ import AlertFeedService from "./AlertFeedService";
23
23
  import { AlertFeedEventType } from "../../Models/DatabaseModels/AlertFeed";
24
24
  import WorkspaceNotificationRuleService from "./WorkspaceNotificationRuleService";
25
25
  import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
26
+ import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
26
27
 
27
28
  export class Service extends DatabaseService<AlertStateTimeline> {
28
29
  public constructor() {
@@ -64,118 +65,210 @@ export class Service extends DatabaseService<AlertStateTimeline> {
64
65
  throw new BadDataException("alertId is null");
65
66
  }
66
67
 
67
- if (!createBy.data.startsAt) {
68
- createBy.data.startsAt = OneUptimeDate.getCurrentDate();
69
- }
68
+ let mutex: SemaphoreMutex | null = null;
70
69
 
71
- if (
72
- (createBy.data.createdByUserId ||
73
- createBy.data.createdByUser ||
74
- createBy.props.userId) &&
75
- !createBy.data.rootCause
76
- ) {
77
- let userId: ObjectID | undefined = createBy.data.createdByUserId;
70
+ try {
71
+ if (!createBy.data.startsAt) {
72
+ createBy.data.startsAt = OneUptimeDate.getCurrentDate();
73
+ }
78
74
 
79
- if (createBy.props.userId) {
80
- userId = createBy.props.userId;
75
+ try {
76
+ mutex = await Semaphore.lock({
77
+ key: createBy.data.alertId.toString(),
78
+ namespace: "AlertStateTimeline.create",
79
+ });
80
+ } catch (err) {
81
+ logger.error(err);
81
82
  }
82
83
 
83
- if (createBy.data.createdByUser && createBy.data.createdByUser.id) {
84
- userId = createBy.data.createdByUser.id;
84
+ if (
85
+ (createBy.data.createdByUserId ||
86
+ createBy.data.createdByUser ||
87
+ createBy.props.userId) &&
88
+ !createBy.data.rootCause
89
+ ) {
90
+ let userId: ObjectID | undefined = createBy.data.createdByUserId;
91
+
92
+ if (createBy.props.userId) {
93
+ userId = createBy.props.userId;
94
+ }
95
+
96
+ if (createBy.data.createdByUser && createBy.data.createdByUser.id) {
97
+ userId = createBy.data.createdByUser.id;
98
+ }
99
+
100
+ if (userId) {
101
+ createBy.data.rootCause = `Alert state created by ${await UserService.getUserMarkdownString(
102
+ {
103
+ userId: userId!,
104
+ projectId: createBy.data.projectId || createBy.props.tenantId!,
105
+ },
106
+ )}`;
107
+ }
85
108
  }
86
109
 
87
- if (userId) {
88
- createBy.data.rootCause = `Alert state created by ${await UserService.getUserMarkdownString(
89
- {
90
- userId: userId!,
91
- projectId: createBy.data.projectId || createBy.props.tenantId!,
110
+ const alertStateId: ObjectID | undefined | null =
111
+ createBy.data.alertStateId || createBy.data.alertState?.id;
112
+
113
+ if (!alertStateId) {
114
+ throw new BadDataException("alertStateId is null");
115
+ }
116
+
117
+ const stateBeforeThis: AlertStateTimeline | null = await this.findOneBy({
118
+ query: {
119
+ alertId: createBy.data.alertId,
120
+ startsAt: QueryHelper.lessThanEqualTo(createBy.data.startsAt),
121
+ },
122
+ sort: {
123
+ startsAt: SortOrder.Descending,
124
+ },
125
+ props: {
126
+ isRoot: true,
127
+ },
128
+ select: {
129
+ alertStateId: true,
130
+ alertState: {
131
+ order: true,
132
+ name: true,
92
133
  },
93
- )}`;
134
+ startsAt: true,
135
+ endsAt: true,
136
+ },
137
+ });
138
+
139
+ logger.debug("State Before this");
140
+ logger.debug(stateBeforeThis);
141
+
142
+ // If this is the first state, then do not notify the owner.
143
+ if (!stateBeforeThis) {
144
+ // since this is the first status, do not notify the owner.
145
+ createBy.data.isOwnerNotified = true;
94
146
  }
95
- }
96
147
 
97
- const stateBeforeThis: AlertStateTimeline | null = await this.findOneBy({
98
- query: {
99
- alertId: createBy.data.alertId,
100
- startsAt: QueryHelper.lessThanEqualTo(createBy.data.startsAt),
101
- },
102
- sort: {
103
- startsAt: SortOrder.Descending,
104
- },
105
- props: {
106
- isRoot: true,
107
- },
108
- select: {
109
- alertStateId: true,
110
- startsAt: true,
111
- endsAt: true,
112
- },
113
- });
148
+ // check if this new state and the previous state are same.
149
+ // if yes, then throw bad data exception.
114
150
 
115
- logger.debug("State Before this");
116
- logger.debug(stateBeforeThis);
151
+ if (stateBeforeThis && stateBeforeThis.alertStateId && alertStateId) {
152
+ if (
153
+ stateBeforeThis.alertStateId.toString() === alertStateId.toString()
154
+ ) {
155
+ throw new BadDataException(
156
+ "Alert state cannot be same as previous state.",
157
+ );
158
+ }
159
+ }
117
160
 
118
- // If this is the first state, then do not notify the owner.
119
- if (!stateBeforeThis) {
120
- // since this is the first status, do not notify the owner.
121
- createBy.data.isOwnerNotified = true;
122
- }
161
+ if (stateBeforeThis && stateBeforeThis.alertState?.order) {
162
+ const newAlertState: AlertState | null =
163
+ await AlertStateService.findOneBy({
164
+ query: {
165
+ _id: alertStateId,
166
+ },
167
+ select: {
168
+ order: true,
169
+ name: true,
170
+ },
171
+ props: {
172
+ isRoot: true,
173
+ },
174
+ });
123
175
 
124
- const stateAfterThis: AlertStateTimeline | null = await this.findOneBy({
125
- query: {
126
- alertId: createBy.data.alertId,
127
- startsAt: QueryHelper.greaterThan(createBy.data.startsAt),
128
- },
129
- sort: {
130
- startsAt: SortOrder.Ascending,
131
- },
132
- props: {
133
- isRoot: true,
134
- },
135
- select: {
136
- alertStateId: true,
137
- startsAt: true,
138
- endsAt: true,
139
- },
140
- });
176
+ if (newAlertState && newAlertState.order) {
177
+ // check if the new alert state is in order is greater than the previous state order
178
+ if (
179
+ stateBeforeThis &&
180
+ stateBeforeThis.alertState &&
181
+ stateBeforeThis.alertState.order &&
182
+ newAlertState.order <= stateBeforeThis.alertState.order
183
+ ) {
184
+ throw new BadDataException(
185
+ `Alert cannot transition to ${newAlertState.name} state from ${stateBeforeThis.alertState.name} state because ${newAlertState.name} is before ${stateBeforeThis.alertState.name} in the order of alert states.`,
186
+ );
187
+ }
188
+ }
189
+ }
141
190
 
142
- // compute ends at. It's the start of the next status.
143
- if (stateAfterThis && stateAfterThis.startsAt) {
144
- createBy.data.endsAt = stateAfterThis.startsAt;
145
- }
191
+ const stateAfterThis: AlertStateTimeline | null = await this.findOneBy({
192
+ query: {
193
+ alertId: createBy.data.alertId,
194
+ startsAt: QueryHelper.greaterThan(createBy.data.startsAt),
195
+ },
196
+ sort: {
197
+ startsAt: SortOrder.Ascending,
198
+ },
199
+ props: {
200
+ isRoot: true,
201
+ },
202
+ select: {
203
+ alertStateId: true,
204
+ startsAt: true,
205
+ endsAt: true,
206
+ },
207
+ });
146
208
 
147
- logger.debug("State After this");
148
- logger.debug(stateAfterThis);
209
+ // compute ends at. It's the start of the next status.
210
+ if (stateAfterThis && stateAfterThis.startsAt) {
211
+ createBy.data.endsAt = stateAfterThis.startsAt;
212
+ }
149
213
 
150
- const internalNote: string | undefined = (
151
- createBy.miscDataProps as JSONObject | undefined
152
- )?.["internalNote"] as string | undefined;
214
+ // check if this new state and the previous state are same.
215
+ // if yes, then throw bad data exception.
153
216
 
154
- if (internalNote) {
155
- const alertNote: AlertInternalNote = new AlertInternalNote();
156
- alertNote.alertId = createBy.data.alertId;
157
- alertNote.note = internalNote;
158
- alertNote.createdAt = createBy.data.startsAt;
159
- alertNote.projectId = createBy.data.projectId!;
217
+ if (stateAfterThis && stateAfterThis.alertStateId && alertStateId) {
218
+ if (
219
+ stateAfterThis.alertStateId.toString() === alertStateId.toString()
220
+ ) {
221
+ throw new BadDataException(
222
+ "Alert state cannot be same as next state.",
223
+ );
224
+ }
225
+ }
160
226
 
161
- await AlertInternalNoteService.create({
162
- data: alertNote,
163
- props: createBy.props,
164
- });
165
- }
227
+ logger.debug("State After this");
228
+ logger.debug(stateAfterThis);
166
229
 
167
- const privateNote: string | undefined = (
168
- createBy.miscDataProps as JSONObject | undefined
169
- )?.["privateNote"] as string | undefined;
230
+ const internalNote: string | undefined = (
231
+ createBy.miscDataProps as JSONObject | undefined
232
+ )?.["internalNote"] as string | undefined;
170
233
 
171
- return {
172
- createBy,
173
- carryForward: {
174
- statusTimelineBeforeThisStatus: stateBeforeThis || null,
175
- statusTimelineAfterThisStatus: stateAfterThis || null,
176
- privateNote: privateNote,
177
- },
178
- };
234
+ if (internalNote) {
235
+ const alertNote: AlertInternalNote = new AlertInternalNote();
236
+ alertNote.alertId = createBy.data.alertId;
237
+ alertNote.note = internalNote;
238
+ alertNote.createdAt = createBy.data.startsAt;
239
+ alertNote.projectId = createBy.data.projectId!;
240
+
241
+ await AlertInternalNoteService.create({
242
+ data: alertNote,
243
+ props: createBy.props,
244
+ });
245
+ }
246
+
247
+ const privateNote: string | undefined = (
248
+ createBy.miscDataProps as JSONObject | undefined
249
+ )?.["privateNote"] as string | undefined;
250
+
251
+ return {
252
+ createBy,
253
+ carryForward: {
254
+ statusTimelineBeforeThisStatus: stateBeforeThis || null,
255
+ statusTimelineAfterThisStatus: stateAfterThis || null,
256
+ privateNote: privateNote,
257
+ mutex: mutex,
258
+ },
259
+ };
260
+ } catch (error) {
261
+ // release the mutex if it was acquired.
262
+ if (mutex) {
263
+ try {
264
+ await Semaphore.release(mutex);
265
+ } catch (err) {
266
+ logger.error(err);
267
+ }
268
+ }
269
+
270
+ throw error;
271
+ }
179
272
  }
180
273
 
181
274
  @CaptureSpan()
@@ -187,6 +280,8 @@ export class Service extends DatabaseService<AlertStateTimeline> {
187
280
  throw new BadDataException("alertId is null");
188
281
  }
189
282
 
283
+ const mutex: SemaphoreMutex | null = onCreate.carryForward.mutex;
284
+
190
285
  if (!createdItem.alertStateId) {
191
286
  throw new BadDataException("alertStateId is null");
192
287
  }
@@ -256,6 +351,14 @@ export class Service extends DatabaseService<AlertStateTimeline> {
256
351
  });
257
352
  }
258
353
 
354
+ if (mutex) {
355
+ try {
356
+ await Semaphore.release(mutex);
357
+ } catch (err) {
358
+ logger.error(err);
359
+ }
360
+ }
361
+
259
362
  const alertState: AlertState | null = await AlertStateService.findOneBy({
260
363
  query: {
261
364
  _id: createdItem.alertStateId.toString()!,
@@ -24,6 +24,7 @@ import { IncidentFeedEventType } from "../../Models/DatabaseModels/IncidentFeed"
24
24
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
25
25
  import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
26
26
  import WorkspaceNotificationRuleService from "./WorkspaceNotificationRuleService";
27
+ import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
27
28
 
28
29
  export class Service extends DatabaseService<IncidentStateTimeline> {
29
30
  public constructor() {
@@ -62,112 +63,214 @@ export class Service extends DatabaseService<IncidentStateTimeline> {
62
63
  protected override async onBeforeCreate(
63
64
  createBy: CreateBy<IncidentStateTimeline>,
64
65
  ): Promise<OnCreate<IncidentStateTimeline>> {
65
- if (!createBy.data.incidentId) {
66
- throw new BadDataException("incidentId is null");
67
- }
66
+ let mutex: SemaphoreMutex | null = null;
68
67
 
69
- if (!createBy.data.startsAt) {
70
- createBy.data.startsAt = OneUptimeDate.getCurrentDate();
71
- }
68
+ try {
69
+ if (!createBy.data.incidentId) {
70
+ throw new BadDataException("incidentId is null");
71
+ }
72
72
 
73
- if (
74
- (createBy.data.createdByUserId ||
75
- createBy.data.createdByUser ||
76
- createBy.props.userId) &&
77
- !createBy.data.rootCause
78
- ) {
79
- let userId: ObjectID | undefined = createBy.data.createdByUserId;
73
+ try {
74
+ mutex = await Semaphore.lock({
75
+ key: createBy.data.incidentId.toString(),
76
+ namespace: "IncidentStateTimeline.create",
77
+ });
78
+ } catch (err) {
79
+ logger.error(err);
80
+ }
80
81
 
81
- if (createBy.props.userId) {
82
- userId = createBy.props.userId;
82
+ if (!createBy.data.startsAt) {
83
+ createBy.data.startsAt = OneUptimeDate.getCurrentDate();
83
84
  }
84
85
 
85
- if (createBy.data.createdByUser && createBy.data.createdByUser.id) {
86
- userId = createBy.data.createdByUser.id;
86
+ if (
87
+ (createBy.data.createdByUserId ||
88
+ createBy.data.createdByUser ||
89
+ createBy.props.userId) &&
90
+ !createBy.data.rootCause
91
+ ) {
92
+ let userId: ObjectID | undefined = createBy.data.createdByUserId;
93
+
94
+ if (createBy.props.userId) {
95
+ userId = createBy.props.userId;
96
+ }
97
+
98
+ if (createBy.data.createdByUser && createBy.data.createdByUser.id) {
99
+ userId = createBy.data.createdByUser.id;
100
+ }
101
+
102
+ if (userId) {
103
+ createBy.data.rootCause = `Incident state created by ${await UserService.getUserMarkdownString(
104
+ {
105
+ userId: userId!,
106
+ projectId: createBy.data.projectId || createBy.props.tenantId!,
107
+ },
108
+ )}`;
109
+ }
87
110
  }
88
111
 
89
- if (userId) {
90
- createBy.data.rootCause = `Incident state created by ${await UserService.getUserMarkdownString(
91
- {
92
- userId: userId!,
93
- projectId: createBy.data.projectId || createBy.props.tenantId!,
112
+ const incidentStateId: ObjectID | undefined | null =
113
+ createBy.data.incidentStateId || createBy.data.incidentState?.id;
114
+
115
+ if (!incidentStateId) {
116
+ throw new BadDataException("incidentStateId is null");
117
+ }
118
+
119
+ const stateBeforeThis: IncidentStateTimeline | null =
120
+ await this.findOneBy({
121
+ query: {
122
+ incidentId: createBy.data.incidentId,
123
+ startsAt: QueryHelper.lessThanEqualTo(createBy.data.startsAt),
124
+ },
125
+ sort: {
126
+ startsAt: SortOrder.Descending,
127
+ },
128
+ props: {
129
+ isRoot: true,
130
+ },
131
+ select: {
132
+ incidentStateId: true,
133
+ incidentState: {
134
+ _id: true,
135
+ order: true,
136
+ name: true,
137
+ },
138
+ startsAt: true,
139
+ endsAt: true,
94
140
  },
95
- )}`;
141
+ });
142
+
143
+ logger.debug("State Before this");
144
+ logger.debug(stateBeforeThis);
145
+
146
+ // If this is the first state, then do not notify the owner.
147
+ if (!stateBeforeThis) {
148
+ // since this is the first status, do not notify the owner.
149
+ createBy.data.isOwnerNotified = true;
96
150
  }
97
- }
98
151
 
99
- const stateBeforeThis: IncidentStateTimeline | null = await this.findOneBy({
100
- query: {
101
- incidentId: createBy.data.incidentId,
102
- startsAt: QueryHelper.lessThanEqualTo(createBy.data.startsAt),
103
- },
104
- sort: {
105
- startsAt: SortOrder.Descending,
106
- },
107
- props: {
108
- isRoot: true,
109
- },
110
- select: {
111
- incidentStateId: true,
112
- startsAt: true,
113
- endsAt: true,
114
- },
115
- });
152
+ // check if this new state and the previous state are same.
153
+ // if yes, then throw bad data exception.
154
+
155
+ if (
156
+ stateBeforeThis &&
157
+ stateBeforeThis.incidentStateId &&
158
+ incidentStateId
159
+ ) {
160
+ if (
161
+ stateBeforeThis.incidentStateId.toString() ===
162
+ incidentStateId.toString()
163
+ ) {
164
+ throw new BadDataException(
165
+ "Incident state cannot be same as previous state.",
166
+ );
167
+ }
168
+ }
169
+
170
+ if (stateBeforeThis && stateBeforeThis.incidentState?.order) {
171
+ const newIncidentState: IncidentState | null =
172
+ await IncidentStateService.findOneBy({
173
+ query: {
174
+ _id: incidentStateId,
175
+ },
176
+ select: {
177
+ order: true,
178
+ name: true,
179
+ },
180
+ props: {
181
+ isRoot: true,
182
+ },
183
+ });
116
184
 
117
- logger.debug("State Before this");
118
- logger.debug(stateBeforeThis);
185
+ if (newIncidentState && newIncidentState.order) {
186
+ // check if the new incident state is in order is greater than the previous state order
187
+ if (
188
+ stateBeforeThis &&
189
+ stateBeforeThis.incidentState &&
190
+ stateBeforeThis.incidentState.order &&
191
+ newIncidentState.order <= stateBeforeThis.incidentState.order
192
+ ) {
193
+ throw new BadDataException(
194
+ `Incident cannot transition to ${newIncidentState.name} state from ${stateBeforeThis.incidentState.name} state because ${newIncidentState.name} is before ${stateBeforeThis.incidentState.name} in the order of incident states.`,
195
+ );
196
+ }
197
+ }
198
+ }
119
199
 
120
- // If this is the first state, then do not notify the owner.
121
- if (!stateBeforeThis) {
122
- // since this is the first status, do not notify the owner.
123
- createBy.data.isOwnerNotified = true;
124
- }
200
+ const stateAfterThis: IncidentStateTimeline | null = await this.findOneBy(
201
+ {
202
+ query: {
203
+ incidentId: createBy.data.incidentId,
204
+ startsAt: QueryHelper.greaterThan(createBy.data.startsAt),
205
+ },
206
+ sort: {
207
+ startsAt: SortOrder.Ascending,
208
+ },
209
+ props: {
210
+ isRoot: true,
211
+ },
212
+ select: {
213
+ incidentStateId: true,
214
+ startsAt: true,
215
+ endsAt: true,
216
+ },
217
+ },
218
+ );
125
219
 
126
- const stateAfterThis: IncidentStateTimeline | null = await this.findOneBy({
127
- query: {
128
- incidentId: createBy.data.incidentId,
129
- startsAt: QueryHelper.greaterThan(createBy.data.startsAt),
130
- },
131
- sort: {
132
- startsAt: SortOrder.Ascending,
133
- },
134
- props: {
135
- isRoot: true,
136
- },
137
- select: {
138
- incidentStateId: true,
139
- startsAt: true,
140
- endsAt: true,
141
- },
142
- });
220
+ // compute ends at. It's the start of the next status.
221
+ if (stateAfterThis && stateAfterThis.startsAt) {
222
+ createBy.data.endsAt = stateAfterThis.startsAt;
223
+ }
143
224
 
144
- // compute ends at. It's the start of the next status.
145
- if (stateAfterThis && stateAfterThis.startsAt) {
146
- createBy.data.endsAt = stateAfterThis.startsAt;
147
- }
225
+ // check if this new state and the previous state are same.
226
+ // if yes, then throw bad data exception.
227
+
228
+ if (stateAfterThis && stateAfterThis.incidentStateId && incidentStateId) {
229
+ if (
230
+ stateAfterThis.incidentStateId.toString() ===
231
+ incidentStateId.toString()
232
+ ) {
233
+ throw new BadDataException(
234
+ "Incident state cannot be same as next state.",
235
+ );
236
+ }
237
+ }
148
238
 
149
- logger.debug("State After this");
150
- logger.debug(stateAfterThis);
239
+ logger.debug("State After this");
240
+ logger.debug(stateAfterThis);
151
241
 
152
- const publicNote: string | undefined = (
153
- createBy.miscDataProps as JSONObject | undefined
154
- )?.["publicNote"] as string | undefined;
242
+ const publicNote: string | undefined = (
243
+ createBy.miscDataProps as JSONObject | undefined
244
+ )?.["publicNote"] as string | undefined;
155
245
 
156
- if (publicNote) {
157
- // mark status page subscribers as notified for this state change because we dont want to send duplicate (two) emails one for public note and one for state change.
158
- if (createBy.data.shouldStatusPageSubscribersBeNotified) {
159
- createBy.data.isStatusPageSubscribersNotified = true;
246
+ if (publicNote) {
247
+ // mark status page subscribers as notified for this state change because we dont want to send duplicate (two) emails one for public note and one for state change.
248
+ if (createBy.data.shouldStatusPageSubscribersBeNotified) {
249
+ createBy.data.isStatusPageSubscribersNotified = true;
250
+ }
160
251
  }
161
- }
162
252
 
163
- return {
164
- createBy,
165
- carryForward: {
166
- statusTimelineBeforeThisStatus: stateBeforeThis || null,
167
- statusTimelineAfterThisStatus: stateAfterThis || null,
168
- publicNote: publicNote,
169
- },
170
- };
253
+ return {
254
+ createBy,
255
+ carryForward: {
256
+ statusTimelineBeforeThisStatus: stateBeforeThis || null,
257
+ statusTimelineAfterThisStatus: stateAfterThis || null,
258
+ publicNote: publicNote,
259
+ mutex: mutex,
260
+ },
261
+ };
262
+ } catch (err) {
263
+ // release the mutex if it was acquired.
264
+ if (mutex) {
265
+ try {
266
+ await Semaphore.release(mutex);
267
+ } catch (err) {
268
+ logger.error(err);
269
+ }
270
+ }
271
+
272
+ throw err;
273
+ }
171
274
  }
172
275
 
173
276
  @CaptureSpan()
@@ -175,6 +278,8 @@ export class Service extends DatabaseService<IncidentStateTimeline> {
175
278
  onCreate: OnCreate<IncidentStateTimeline>,
176
279
  createdItem: IncidentStateTimeline,
177
280
  ): Promise<IncidentStateTimeline> {
281
+ const mutex: SemaphoreMutex | null = onCreate.carryForward.mutex;
282
+
178
283
  if (!createdItem.incidentId) {
179
284
  throw new BadDataException("incidentId is null");
180
285
  }
@@ -267,6 +372,14 @@ export class Service extends DatabaseService<IncidentStateTimeline> {
267
372
  },
268
373
  });
269
374
 
375
+ if (mutex) {
376
+ try {
377
+ await Semaphore.release(mutex);
378
+ } catch (err) {
379
+ logger.error(err);
380
+ }
381
+ }
382
+
270
383
  const stateName: string = incidentState?.name || "";
271
384
  let stateEmoji: string = "➡️";
272
385