@oneuptime/common 7.0.4135 → 7.0.4142
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.
- package/Server/Services/AlertStateTimelineService.ts +196 -93
- package/Server/Services/IncidentStateTimelineService.ts +200 -87
- package/Server/Services/MonitorStatusTimelineService.ts +35 -1
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +192 -80
- package/Server/Utils/Monitor/MonitorResource.ts +17 -17
- package/build/dist/Server/Services/AlertStateTimelineService.js +165 -85
- package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +166 -81
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusTimelineService.js +21 -1
- package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +157 -72
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +19 -17
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/package.json +2 -2
|
@@ -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
|
-
|
|
68
|
-
createBy.data.startsAt = OneUptimeDate.getCurrentDate();
|
|
69
|
-
}
|
|
68
|
+
let mutex: SemaphoreMutex | null = null;
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
(createBy.data.
|
|
73
|
-
createBy.data.
|
|
74
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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 (
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
createBy.data.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
props: createBy.props,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
227
|
+
logger.debug("State After this");
|
|
228
|
+
logger.debug(stateAfterThis);
|
|
166
229
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
230
|
+
const internalNote: string | undefined = (
|
|
231
|
+
createBy.miscDataProps as JSONObject | undefined
|
|
232
|
+
)?.["internalNote"] as string | undefined;
|
|
170
233
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
66
|
-
throw new BadDataException("incidentId is null");
|
|
67
|
-
}
|
|
66
|
+
let mutex: SemaphoreMutex | null = null;
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
createBy.data.
|
|
71
|
-
|
|
68
|
+
try {
|
|
69
|
+
if (!createBy.data.incidentId) {
|
|
70
|
+
throw new BadDataException("incidentId is null");
|
|
71
|
+
}
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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.
|
|
82
|
-
|
|
82
|
+
if (!createBy.data.startsAt) {
|
|
83
|
+
createBy.data.startsAt = OneUptimeDate.getCurrentDate();
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
if (
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
createBy.data.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
|
|
239
|
+
logger.debug("State After this");
|
|
240
|
+
logger.debug(stateAfterThis);
|
|
151
241
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
242
|
+
const publicNote: string | undefined = (
|
|
243
|
+
createBy.miscDataProps as JSONObject | undefined
|
|
244
|
+
)?.["publicNote"] as string | undefined;
|
|
155
245
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|