@oneuptime/common 12.0.2 → 12.0.3
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/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/LogDropFilter.ts +9 -20
- package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
- package/Models/DatabaseModels/StatusPageResource.ts +73 -0
- package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
- package/Server/API/BaseAPI.ts +13 -2
- package/Server/API/DashboardAPI.ts +39 -0
- package/Server/EnvironmentConfig.ts +31 -1
- package/Server/Infrastructure/GlobalCache.ts +115 -0
- package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AlertStateTimelineService.ts +20 -0
- package/Server/Services/CephClusterService.ts +26 -86
- package/Server/Services/CloudResourceService.ts +23 -83
- package/Server/Services/DatabaseService.ts +111 -0
- package/Server/Services/DockerHostService.ts +19 -79
- package/Server/Services/DockerSwarmClusterService.ts +27 -88
- package/Server/Services/HostService.ts +46 -99
- package/Server/Services/IncidentStateTimelineService.ts +20 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/IoTFleetService.ts +19 -79
- package/Server/Services/KubernetesClusterService.ts +17 -78
- package/Server/Services/LabelService.ts +57 -10
- package/Server/Services/MetricTypeService.ts +84 -0
- package/Server/Services/MonitorService.ts +89 -14
- package/Server/Services/MonitorStatusTimelineService.ts +29 -0
- package/Server/Services/MonitorTemplateService.ts +65 -0
- package/Server/Services/PodmanHostService.ts +19 -79
- package/Server/Services/ProxmoxClusterService.ts +23 -83
- package/Server/Services/RumApplicationService.ts +19 -80
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
- package/Server/Services/ServerlessFunctionService.ts +24 -85
- package/Server/Services/ServiceService.ts +299 -73
- package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
- package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
- package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
- package/Server/Services/TelemetryEntityService.ts +1 -0
- package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
- package/Server/Utils/Monitor/MonitorResource.ts +63 -36
- package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
- package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
- package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
- package/Server/Utils/SingleFlight.ts +83 -0
- package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
- package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
- package/Server/Utils/Telemetry/Telemetry.ts +351 -122
- package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
- package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
- package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
- package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +226 -0
- package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
- package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
- package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
- package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
- package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
- package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
- package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
- package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
- package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
- package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
- package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
- package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
- package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
- package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
- package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
- package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
- package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
- package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
- package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
- package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
- package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
- package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
- package/Tests/Types/Decimal.test.ts +37 -0
- package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
- package/Tests/Types/Port.test.ts +38 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
- package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
- package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
- package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
- package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
- package/Types/Dashboard/DashboardComponentType.ts +1 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
- package/Types/Dashboard/DashboardTemplates.ts +397 -0
- package/Types/Database/BigIntColumnTransformer.ts +74 -0
- package/Types/Database/DatabaseProperty.ts +26 -0
- package/Types/Database/NumericColumnValue.ts +128 -0
- package/Types/Decimal.ts +16 -4
- package/Types/Permission.ts +48 -0
- package/Types/Port.ts +16 -4
- package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
- package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
- package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
- package/Utils/Dashboard/Components/Index.ts +7 -0
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
- package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
- package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
- package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +11 -1
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +39 -0
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +27 -1
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/GlobalCache.js +91 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
- package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +26 -76
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceService.js +23 -73
- package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +86 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +19 -69
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +58 -102
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetService.js +19 -69
- package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/LabelService.js +42 -7
- package/build/dist/Server/Services/LabelService.js.map +1 -1
- package/build/dist/Server/Services/MetricTypeService.js +74 -0
- package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +81 -9
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
- package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +19 -69
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +19 -70
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
- package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
- package/build/dist/Server/Services/ServiceService.js +236 -61
- package/build/dist/Server/Services/ServiceService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
- package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
- package/build/dist/Server/Utils/SingleFlight.js +76 -0
- package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
- package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
- package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
- package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
- package/build/dist/Types/Database/DatabaseProperty.js +25 -0
- package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
- package/build/dist/Types/Database/NumericColumnValue.js +97 -0
- package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
- package/build/dist/Types/Decimal.js +14 -3
- package/build/dist/Types/Decimal.js.map +1 -1
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Port.js +15 -5
- package/build/dist/Types/Port.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
- package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
- package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
import ProjectScopedReferenceValidator from "../../../../Server/Utils/Database/ProjectScopedReferenceValidator";
|
|
2
|
+
import DatabaseService from "../../../../Server/Services/DatabaseService";
|
|
2
3
|
import IncidentSeverityService from "../../../../Server/Services/IncidentSeverityService";
|
|
3
4
|
import IncidentStateService from "../../../../Server/Services/IncidentStateService";
|
|
4
5
|
import MonitorStatusService from "../../../../Server/Services/MonitorStatusService";
|
|
6
|
+
import UserService from "../../../../Server/Services/UserService";
|
|
7
|
+
import DatabaseBaseModel from "../../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
5
8
|
import IncidentSeverity from "../../../../Models/DatabaseModels/IncidentSeverity";
|
|
6
9
|
import IncidentState from "../../../../Models/DatabaseModels/IncidentState";
|
|
7
10
|
import MonitorStatus from "../../../../Models/DatabaseModels/MonitorStatus";
|
|
11
|
+
import User from "../../../../Models/DatabaseModels/User";
|
|
8
12
|
import BadDataException from "../../../../Types/Exception/BadDataException";
|
|
13
|
+
import Name from "../../../../Types/Name";
|
|
9
14
|
import ObjectID from "../../../../Types/ObjectID";
|
|
10
15
|
import { afterEach, describe, expect, it, jest } from "@jest/globals";
|
|
11
16
|
|
|
12
17
|
/*
|
|
13
18
|
* Contract under test: an incident (or alert, or scheduled maintenance event)
|
|
14
19
|
* stores the id of its state, its severity and the monitor status it switches
|
|
15
|
-
* monitors to.
|
|
16
|
-
*
|
|
17
|
-
* record, the row here still points at it, and the ON DELETE NO ACTION foreign
|
|
18
|
-
* key refuses the delete with 23503.
|
|
20
|
+
* monitors to. Two things can be wrong with one of those ids, and the guard
|
|
21
|
+
* refuses both.
|
|
19
22
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
+
* An id belonging to a *different* project is what leaves the referenced
|
|
24
|
+
* project undeletable — deleting that project cascades into the record, the row
|
|
25
|
+
* here still points at it, and the ON DELETE NO ACTION foreign key refuses the
|
|
26
|
+
* delete with 23503.
|
|
27
|
+
*
|
|
28
|
+
* An id that exists NOWHERE is issue #3039: the REST API took any uuid for
|
|
29
|
+
* these, and the write then failed as a raw foreign key violation deep in
|
|
30
|
+
* Postgres — or, for ids stored in a JSON blob with no foreign key behind them,
|
|
31
|
+
* saved fine and blew up much later inside the probe worker. Rejecting it here
|
|
32
|
+
* names the id instead. Callers whose reference is allowed to dangle opt out
|
|
33
|
+
* per reference with `mustExist: false`.
|
|
23
34
|
*/
|
|
24
35
|
|
|
25
36
|
const PROJECT_ID: ObjectID = new ObjectID(
|
|
@@ -34,6 +45,7 @@ const FOREIGN_SEVERITY_ID: string = "cfc2f04f-79cb-4344-8c54-dafe5e3a290c";
|
|
|
34
45
|
const FOREIGN_STATE_ID: string = "a2eb67d4-bd2e-4186-9187-dad799c9316c";
|
|
35
46
|
const FOREIGN_STATUS_ID: string = "1a1cf3f2-0e35-4a1e-98a1-3f0f8b5f7f9e";
|
|
36
47
|
const UNKNOWN_ID: string = "9c0ba0b3-2f8e-4c02-a8d5-6a4d2f5b9c11";
|
|
48
|
+
const OWN_USER_ID: string = "7c2f6b40-6a1b-4f18-9d0e-2c5ba2d3f0a7";
|
|
37
49
|
|
|
38
50
|
const incidentSeverity: (
|
|
39
51
|
id: string,
|
|
@@ -172,17 +184,200 @@ describe("ProjectScopedReferenceValidator", () => {
|
|
|
172
184
|
).resolves.toBeUndefined();
|
|
173
185
|
});
|
|
174
186
|
|
|
175
|
-
it("
|
|
187
|
+
it("rejects an id that matches no record at all", async () => {
|
|
188
|
+
/*
|
|
189
|
+
* Issue #3039. This used to be allowed through on purpose, and the write
|
|
190
|
+
* then either came back as an opaque foreign key violation from Postgres or
|
|
191
|
+
* — in a JSON column with no foreign key — saved and failed later, at run
|
|
192
|
+
* time, with nothing reported to whoever supplied the id.
|
|
193
|
+
*/
|
|
194
|
+
mockLookups({ incidentSeverities: [] });
|
|
195
|
+
|
|
196
|
+
await expect(
|
|
197
|
+
ProjectScopedReferenceValidator.validateReferencesBelongToProject({
|
|
198
|
+
projectId: PROJECT_ID,
|
|
199
|
+
subject: "incident",
|
|
200
|
+
references: [
|
|
201
|
+
{
|
|
202
|
+
modelName: "Incident Severity",
|
|
203
|
+
id: UNKNOWN_ID,
|
|
204
|
+
service: IncidentSeverityService,
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
}),
|
|
208
|
+
).rejects.toThrow(BadDataException);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("accepts an UPPERCASE uuid for a record Postgres reads back lower-cased", async () => {
|
|
212
|
+
/*
|
|
213
|
+
* ObjectID keeps whatever case it was handed — its validation regex is
|
|
214
|
+
* case-insensitive — and Postgres compares `uuid` by parsed value, so an
|
|
215
|
+
* uppercase id in the payload selects the row fine but reads back
|
|
216
|
+
* lower-cased. Comparing the two verbatim reported a record that plainly
|
|
217
|
+
* exists as missing, and the API answered "does not exist" for an id the
|
|
218
|
+
* caller had just read out of a GET response.
|
|
219
|
+
*/
|
|
220
|
+
mockLookups({
|
|
221
|
+
incidentSeverities: [incidentSeverity(OWN_SEVERITY_ID, PROJECT_ID)],
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await expect(
|
|
225
|
+
ProjectScopedReferenceValidator.validateReferencesBelongToProject({
|
|
226
|
+
projectId: PROJECT_ID,
|
|
227
|
+
references: [
|
|
228
|
+
{
|
|
229
|
+
modelName: "Incident Severity",
|
|
230
|
+
id: OWN_SEVERITY_ID.toUpperCase(),
|
|
231
|
+
service: IncidentSeverityService,
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
}),
|
|
235
|
+
).resolves.toBeUndefined();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("echoes the id as the caller wrote it when reporting it missing", async () => {
|
|
239
|
+
mockLookups({ incidentSeverities: [] });
|
|
240
|
+
|
|
241
|
+
await expect(
|
|
242
|
+
ProjectScopedReferenceValidator.validateReferencesBelongToProject({
|
|
243
|
+
projectId: PROJECT_ID,
|
|
244
|
+
references: [
|
|
245
|
+
{
|
|
246
|
+
modelName: "Incident Severity",
|
|
247
|
+
id: UNKNOWN_ID.toUpperCase(),
|
|
248
|
+
service: IncidentSeverityService,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
}),
|
|
252
|
+
).rejects.toThrow(`Incident Severity "${UNKNOWN_ID.toUpperCase()}"`);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("names the missing id in the rejection so the caller can find it", async () => {
|
|
256
|
+
mockLookups({ incidentSeverities: [] });
|
|
257
|
+
|
|
258
|
+
await expect(
|
|
259
|
+
ProjectScopedReferenceValidator.validateReferencesBelongToProject({
|
|
260
|
+
projectId: PROJECT_ID,
|
|
261
|
+
subject: "incident",
|
|
262
|
+
references: [
|
|
263
|
+
{
|
|
264
|
+
modelName: "Incident Severity",
|
|
265
|
+
id: UNKNOWN_ID,
|
|
266
|
+
service: IncidentSeverityService,
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
}),
|
|
270
|
+
).rejects.toThrow(
|
|
271
|
+
`This incident references records that do not exist: Incident Severity "${UNKNOWN_ID}". Please pick values that exist in this project and try again.`,
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("leaves a missing id alone when the caller opts out with mustExist false", async () => {
|
|
276
|
+
/*
|
|
277
|
+
* The opt-out exists for references with no foreign key behind them, where
|
|
278
|
+
* a stored id really can dangle (deleting a monitor status does not rewrite
|
|
279
|
+
* the monitor criteria that name it). Refusing those would stop a user
|
|
280
|
+
* saving their way out of a record that was already broken.
|
|
281
|
+
*/
|
|
282
|
+
mockLookups({ incidentSeverities: [] });
|
|
283
|
+
|
|
284
|
+
await expect(
|
|
285
|
+
ProjectScopedReferenceValidator.validateReferencesBelongToProject({
|
|
286
|
+
projectId: PROJECT_ID,
|
|
287
|
+
references: [
|
|
288
|
+
{
|
|
289
|
+
modelName: "Incident Severity",
|
|
290
|
+
id: UNKNOWN_ID,
|
|
291
|
+
service: IncidentSeverityService,
|
|
292
|
+
mustExist: false,
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
}),
|
|
296
|
+
).resolves.toBeUndefined();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("keeps the strictest requirement when the same id arrives twice", async () => {
|
|
176
300
|
/*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
301
|
+
* Two monitor criteria can name the same status, one operative and one not.
|
|
302
|
+
* They collapse to a single lookup, and the one that requires the record to
|
|
303
|
+
* exist must not be dropped by the one that does not.
|
|
180
304
|
*/
|
|
181
305
|
mockLookups({ incidentSeverities: [] });
|
|
182
306
|
|
|
183
307
|
await expect(
|
|
184
308
|
ProjectScopedReferenceValidator.validateReferencesBelongToProject({
|
|
185
309
|
projectId: PROJECT_ID,
|
|
310
|
+
references: [
|
|
311
|
+
{
|
|
312
|
+
modelName: "Incident Severity",
|
|
313
|
+
id: UNKNOWN_ID,
|
|
314
|
+
service: IncidentSeverityService,
|
|
315
|
+
mustExist: false,
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
modelName: "Incident Severity",
|
|
319
|
+
id: UNKNOWN_ID,
|
|
320
|
+
service: IncidentSeverityService,
|
|
321
|
+
mustExist: true,
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
}),
|
|
325
|
+
).rejects.toThrow("do not exist");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("reports a foreign reference and a missing one in the same message", async () => {
|
|
329
|
+
/*
|
|
330
|
+
* A payload with several bad ids should not have to be fixed one
|
|
331
|
+
* round-trip at a time.
|
|
332
|
+
*/
|
|
333
|
+
mockLookups({
|
|
334
|
+
incidentSeverities: [
|
|
335
|
+
incidentSeverity(FOREIGN_SEVERITY_ID, OTHER_PROJECT_ID),
|
|
336
|
+
],
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
let message: string = "";
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
await ProjectScopedReferenceValidator.validateReferencesBelongToProject({
|
|
343
|
+
projectId: PROJECT_ID,
|
|
344
|
+
subject: "incident",
|
|
345
|
+
references: [
|
|
346
|
+
{
|
|
347
|
+
modelName: "Incident Severity",
|
|
348
|
+
id: FOREIGN_SEVERITY_ID,
|
|
349
|
+
service: IncidentSeverityService,
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
modelName: "Incident Severity",
|
|
353
|
+
id: UNKNOWN_ID,
|
|
354
|
+
service: IncidentSeverityService,
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
});
|
|
358
|
+
} catch (error) {
|
|
359
|
+
message = (error as BadDataException).message;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
expect(message).toContain(
|
|
363
|
+
'belong to a different project: Incident Severity "Critical"',
|
|
364
|
+
);
|
|
365
|
+
expect(message).toContain(
|
|
366
|
+
`do not exist: Incident Severity "${UNKNOWN_ID}"`,
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("does not require a record to exist when its project is unknown", async () => {
|
|
371
|
+
/*
|
|
372
|
+
* Same reason the cross-project half is skipped: root and internal writes
|
|
373
|
+
* do not always carry a project, and the guard must stay out of the way
|
|
374
|
+
* rather than guess.
|
|
375
|
+
*/
|
|
376
|
+
const { severityFindBy } = mockLookups({ incidentSeverities: [] });
|
|
377
|
+
|
|
378
|
+
await expect(
|
|
379
|
+
ProjectScopedReferenceValidator.validateReferencesBelongToProject({
|
|
380
|
+
projectId: undefined,
|
|
186
381
|
references: [
|
|
187
382
|
{
|
|
188
383
|
modelName: "Incident Severity",
|
|
@@ -192,6 +387,52 @@ describe("ProjectScopedReferenceValidator", () => {
|
|
|
192
387
|
],
|
|
193
388
|
}),
|
|
194
389
|
).resolves.toBeUndefined();
|
|
390
|
+
|
|
391
|
+
expect(severityFindBy).not.toHaveBeenCalled();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("checks existence against a model that has no project of its own", async () => {
|
|
395
|
+
/*
|
|
396
|
+
* Monitor criteria can name owner *users*, and User has no tenant column.
|
|
397
|
+
* The cross-project comparison has to be skipped for those — comparing an
|
|
398
|
+
* absent projectId against the write's project would report every user as
|
|
399
|
+
* belonging to a different project — while the existence check still runs.
|
|
400
|
+
*/
|
|
401
|
+
const user: User = new User();
|
|
402
|
+
user._id = OWN_USER_ID;
|
|
403
|
+
user.name = new Name("Alice");
|
|
404
|
+
|
|
405
|
+
jest.spyOn(UserService, "findBy").mockResolvedValue([user] as never);
|
|
406
|
+
|
|
407
|
+
await expect(
|
|
408
|
+
ProjectScopedReferenceValidator.validateReferencesBelongToProject({
|
|
409
|
+
projectId: PROJECT_ID,
|
|
410
|
+
references: [
|
|
411
|
+
{
|
|
412
|
+
modelName: "User",
|
|
413
|
+
id: OWN_USER_ID,
|
|
414
|
+
service:
|
|
415
|
+
UserService as unknown as DatabaseService<DatabaseBaseModel>,
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
}),
|
|
419
|
+
).resolves.toBeUndefined();
|
|
420
|
+
|
|
421
|
+
jest.spyOn(UserService, "findBy").mockResolvedValue([] as never);
|
|
422
|
+
|
|
423
|
+
await expect(
|
|
424
|
+
ProjectScopedReferenceValidator.validateReferencesBelongToProject({
|
|
425
|
+
projectId: PROJECT_ID,
|
|
426
|
+
references: [
|
|
427
|
+
{
|
|
428
|
+
modelName: "User",
|
|
429
|
+
id: UNKNOWN_ID,
|
|
430
|
+
service:
|
|
431
|
+
UserService as unknown as DatabaseService<DatabaseBaseModel>,
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
}),
|
|
435
|
+
).rejects.toThrow(`User "${UNKNOWN_ID}"`);
|
|
195
436
|
});
|
|
196
437
|
|
|
197
438
|
it("reports every foreign reference in a single message", async () => {
|
|
@@ -272,9 +513,15 @@ describe("ProjectScopedReferenceValidator", () => {
|
|
|
272
513
|
it("looks each model up once, with every id it was asked about", async () => {
|
|
273
514
|
/*
|
|
274
515
|
* These run on the incident create path, so three references must not mean
|
|
275
|
-
* three round trips per model.
|
|
516
|
+
* three round trips per model. Both ids are returned so the existence check
|
|
517
|
+
* has nothing to complain about and the call reaches its end.
|
|
276
518
|
*/
|
|
277
|
-
const { severityFindBy } = mockLookups({
|
|
519
|
+
const { severityFindBy } = mockLookups({
|
|
520
|
+
incidentSeverities: [
|
|
521
|
+
incidentSeverity(OWN_SEVERITY_ID, PROJECT_ID),
|
|
522
|
+
incidentSeverity(FOREIGN_SEVERITY_ID, PROJECT_ID),
|
|
523
|
+
],
|
|
524
|
+
});
|
|
278
525
|
|
|
279
526
|
await ProjectScopedReferenceValidator.validateReferencesBelongToProject({
|
|
280
527
|
projectId: PROJECT_ID,
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import GlobalCache from "../../../Server/Infrastructure/GlobalCache";
|
|
2
|
+
import TelemetryEntityService from "../../../Server/Services/TelemetryEntityService";
|
|
3
|
+
import { reconcileByNaturalKey } from "../../../Server/Utils/Telemetry/EntityRegistry";
|
|
4
|
+
import TelemetryEntity from "../../../Models/DatabaseModels/TelemetryEntity";
|
|
5
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
6
|
+
import {
|
|
7
|
+
afterEach,
|
|
8
|
+
beforeEach,
|
|
9
|
+
describe,
|
|
10
|
+
expect,
|
|
11
|
+
jest,
|
|
12
|
+
test,
|
|
13
|
+
} from "@jest/globals";
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* The entity registry had the same defect as the Service heartbeat, one layer
|
|
17
|
+
* down and harder to see.
|
|
18
|
+
*
|
|
19
|
+
* Its fence keys on (project + the whole set of promoted entity keys), which
|
|
20
|
+
* in practice is unique per POD — a pod's own key is in the set. But the
|
|
21
|
+
* writes it gates are per ROW: the single TelemetryEntity row for a Kubernetes
|
|
22
|
+
* cluster receives one UPDATE per pod in that cluster per window, the
|
|
23
|
+
* namespace row one per pod in the namespace. When throttle granularity is
|
|
24
|
+
* finer than write granularity, the throttle does not bound the writes at all,
|
|
25
|
+
* and the mismatch factor is the pod count.
|
|
26
|
+
*
|
|
27
|
+
* So there are now two fences: the cheap set-level one, and a per-row one
|
|
28
|
+
* claimed atomically before the lookup — which also removes the redundant
|
|
29
|
+
* SELECT, not just the redundant UPDATE. The bump itself is the SKIP LOCKED
|
|
30
|
+
* write, so shared rows (a cluster, a namespace) cannot convoy the way Service
|
|
31
|
+
* did.
|
|
32
|
+
*
|
|
33
|
+
* Everything external is mocked — no Postgres, no Redis.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const PROJECT_ID: ObjectID = ObjectID.generate();
|
|
37
|
+
const ROW_ID: ObjectID = ObjectID.generate();
|
|
38
|
+
|
|
39
|
+
type BumpCall = { id: ObjectID; data: Record<string, unknown> };
|
|
40
|
+
|
|
41
|
+
let cache: Map<string, string>;
|
|
42
|
+
let deletedKeys: Array<string>;
|
|
43
|
+
let bumps: Array<BumpCall>;
|
|
44
|
+
let reads: number;
|
|
45
|
+
let existing: TelemetryEntity | null;
|
|
46
|
+
let bumpLands: boolean;
|
|
47
|
+
|
|
48
|
+
function existingRow(): TelemetryEntity {
|
|
49
|
+
const row: TelemetryEntity = new TelemetryEntity();
|
|
50
|
+
row.id = ROW_ID;
|
|
51
|
+
row._id = ROW_ID.toString();
|
|
52
|
+
return row;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function reconcile(rowFenceId: string): Promise<void> {
|
|
56
|
+
return reconcileByNaturalKey<TelemetryEntity>({
|
|
57
|
+
service: TelemetryEntityService,
|
|
58
|
+
query: { projectId: PROJECT_ID, entityKey: "k8s-cluster/prod" },
|
|
59
|
+
lastSeenAt: new Date(),
|
|
60
|
+
describe: "entity k8s-cluster/prod",
|
|
61
|
+
rowFenceId: rowFenceId,
|
|
62
|
+
buildModel: (): TelemetryEntity => {
|
|
63
|
+
return existingRow();
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
cache = new Map<string, string>();
|
|
70
|
+
deletedKeys = [];
|
|
71
|
+
bumps = [];
|
|
72
|
+
reads = 0;
|
|
73
|
+
existing = existingRow();
|
|
74
|
+
bumpLands = true;
|
|
75
|
+
|
|
76
|
+
jest
|
|
77
|
+
.spyOn(GlobalCache, "setStringIfNotExists")
|
|
78
|
+
.mockImplementation(async (namespace: string, key: string) => {
|
|
79
|
+
const full: string = `${namespace}:${key}`;
|
|
80
|
+
if (cache.has(full)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
cache.set(full, "1");
|
|
84
|
+
return true;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
jest
|
|
88
|
+
.spyOn(GlobalCache, "deleteKey")
|
|
89
|
+
.mockImplementation(async (namespace: string, key: string) => {
|
|
90
|
+
const full: string = `${namespace}:${key}`;
|
|
91
|
+
deletedKeys.push(full);
|
|
92
|
+
cache.delete(full);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
jest
|
|
96
|
+
.spyOn(TelemetryEntityService, "findOneBy")
|
|
97
|
+
.mockImplementation(async () => {
|
|
98
|
+
reads++;
|
|
99
|
+
return existing;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
jest
|
|
103
|
+
.spyOn(TelemetryEntityService, "updateColumnsByIdIfUnlockedWithoutHooks")
|
|
104
|
+
.mockImplementation(async (input: { id: ObjectID; data: unknown }) => {
|
|
105
|
+
bumps.push({
|
|
106
|
+
id: input.id,
|
|
107
|
+
data: { ...(input.data as Record<string, unknown>) },
|
|
108
|
+
});
|
|
109
|
+
return bumpLands;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
jest
|
|
113
|
+
.spyOn(TelemetryEntityService, "create")
|
|
114
|
+
.mockImplementation(async (input: { data: TelemetryEntity }) => {
|
|
115
|
+
return input.data;
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
afterEach(() => {
|
|
120
|
+
jest.restoreAllMocks();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("reconcileByNaturalKey — the per-row fence", () => {
|
|
124
|
+
test("bumps the row on first contact", async () => {
|
|
125
|
+
await reconcile("project:k8s-cluster:prod");
|
|
126
|
+
|
|
127
|
+
expect(bumps).toHaveLength(1);
|
|
128
|
+
expect(bumps[0]!.id.toString()).toBe(ROW_ID.toString());
|
|
129
|
+
expect(bumps[0]!.data["lastSeenAt"]).toBeInstanceOf(Date);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* The defect, reproduced: many pods, one shared row. Before the per-row
|
|
134
|
+
* fence, each pod's distinct set-level fence let it straight through to the
|
|
135
|
+
* same cluster row.
|
|
136
|
+
*/
|
|
137
|
+
test("many callers for the same row produce one bump per window", async () => {
|
|
138
|
+
for (let pod: number = 0; pod < 50; pod++) {
|
|
139
|
+
await reconcile("project:k8s-cluster:prod");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
expect(bumps).toHaveLength(1);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
/*
|
|
146
|
+
* Claimed BEFORE the lookup. The redundant SELECTs were as much of the pool
|
|
147
|
+
* pressure as the redundant UPDATEs.
|
|
148
|
+
*/
|
|
149
|
+
test("a fenced caller does not even read the row", async () => {
|
|
150
|
+
await reconcile("project:k8s-cluster:prod");
|
|
151
|
+
const readsAfterFirst: number = reads;
|
|
152
|
+
|
|
153
|
+
await reconcile("project:k8s-cluster:prod");
|
|
154
|
+
|
|
155
|
+
expect(reads).toBe(readsAfterFirst);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("different rows are fenced independently", async () => {
|
|
159
|
+
await reconcile("project:k8s-cluster:prod");
|
|
160
|
+
await reconcile("project:k8s-namespace:default");
|
|
161
|
+
|
|
162
|
+
expect(bumps).toHaveLength(2);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("claims the fence atomically, never read-then-write", async () => {
|
|
166
|
+
jest.spyOn(GlobalCache, "getString");
|
|
167
|
+
jest.spyOn(GlobalCache, "setString");
|
|
168
|
+
|
|
169
|
+
await reconcile("project:k8s-cluster:prod");
|
|
170
|
+
|
|
171
|
+
expect(GlobalCache.setStringIfNotExists).toHaveBeenCalled();
|
|
172
|
+
expect(GlobalCache.getString).not.toHaveBeenCalled();
|
|
173
|
+
expect(GlobalCache.setString).not.toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/*
|
|
177
|
+
* The fence id concatenates entity keys and is unbounded in length. Hashing
|
|
178
|
+
* keeps the Redis key fixed-width — a pod reporting hundreds of entities
|
|
179
|
+
* would otherwise produce a multi-kilobyte key.
|
|
180
|
+
*/
|
|
181
|
+
test("hashes the fence id to a fixed-width key", async () => {
|
|
182
|
+
const longId: string = `project:${"k".repeat(5000)}`;
|
|
183
|
+
|
|
184
|
+
await reconcile(longId);
|
|
185
|
+
|
|
186
|
+
const key: string = (GlobalCache.setStringIfNotExists as jest.Mock).mock
|
|
187
|
+
.calls[0]![1] as string;
|
|
188
|
+
|
|
189
|
+
expect(key.length).toBeLessThan(100);
|
|
190
|
+
expect(key).not.toContain("kkkk");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("a cache outage fails open so liveness still advances", async () => {
|
|
194
|
+
jest
|
|
195
|
+
.spyOn(GlobalCache, "setStringIfNotExists")
|
|
196
|
+
.mockRejectedValue(new Error("redis down"));
|
|
197
|
+
|
|
198
|
+
await reconcile("project:k8s-cluster:prod");
|
|
199
|
+
|
|
200
|
+
expect(bumps).toHaveLength(1);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("reconcileByNaturalKey — contention and failure re-open the window", () => {
|
|
205
|
+
test("uses the non-blocking write primitive", async () => {
|
|
206
|
+
await reconcile("project:k8s-cluster:prod");
|
|
207
|
+
|
|
208
|
+
expect(
|
|
209
|
+
TelemetryEntityService.updateColumnsByIdIfUnlockedWithoutHooks,
|
|
210
|
+
).toHaveBeenCalled();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
/*
|
|
214
|
+
* A skipped bump means the row was locked, so nothing was written — but the
|
|
215
|
+
* fence is already claimed and would suppress the next attempt for the whole
|
|
216
|
+
* window. Release it so the next batch retries; each attempt is
|
|
217
|
+
* non-blocking, so this cannot become a retry storm.
|
|
218
|
+
*/
|
|
219
|
+
test("a skipped bump releases the row fence", async () => {
|
|
220
|
+
bumpLands = false;
|
|
221
|
+
|
|
222
|
+
await reconcile("project:k8s-cluster:prod");
|
|
223
|
+
|
|
224
|
+
expect(deletedKeys).toHaveLength(1);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("the released fence actually lets the next attempt through", async () => {
|
|
228
|
+
bumpLands = false;
|
|
229
|
+
await reconcile("project:k8s-cluster:prod");
|
|
230
|
+
|
|
231
|
+
bumpLands = true;
|
|
232
|
+
await reconcile("project:k8s-cluster:prod");
|
|
233
|
+
|
|
234
|
+
expect(bumps).toHaveLength(2);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
/*
|
|
238
|
+
* An over-budget skip writes nothing, so holding the window would suppress
|
|
239
|
+
* the gate's own re-evaluation for a decision that had no effect.
|
|
240
|
+
*/
|
|
241
|
+
test("an over-budget create releases the row fence", async () => {
|
|
242
|
+
existing = null;
|
|
243
|
+
|
|
244
|
+
await reconcileByNaturalKey<TelemetryEntity>({
|
|
245
|
+
service: TelemetryEntityService,
|
|
246
|
+
query: { projectId: PROJECT_ID, entityKey: "k8s-cluster/prod" },
|
|
247
|
+
lastSeenAt: new Date(),
|
|
248
|
+
describe: "entity k8s-cluster/prod",
|
|
249
|
+
rowFenceId: "project:k8s-cluster:prod",
|
|
250
|
+
buildModel: (): TelemetryEntity => {
|
|
251
|
+
return existingRow();
|
|
252
|
+
},
|
|
253
|
+
beforeCreate: async (): Promise<boolean> => {
|
|
254
|
+
return false;
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
expect(deletedKeys).toHaveLength(1);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("an invalid create releases the row fence so the warning keeps surfacing", async () => {
|
|
262
|
+
existing = null;
|
|
263
|
+
|
|
264
|
+
jest
|
|
265
|
+
.spyOn(TelemetryEntityService, "create")
|
|
266
|
+
.mockRejectedValue(new Error("null value in column violates not-null"));
|
|
267
|
+
|
|
268
|
+
await reconcile("project:k8s-cluster:prod");
|
|
269
|
+
|
|
270
|
+
expect(deletedKeys).toHaveLength(1);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("a lost create race bumps the winner instead of failing", async () => {
|
|
274
|
+
existing = null;
|
|
275
|
+
|
|
276
|
+
jest
|
|
277
|
+
.spyOn(TelemetryEntityService, "create")
|
|
278
|
+
.mockRejectedValue(new Error("duplicate key value violates unique"));
|
|
279
|
+
|
|
280
|
+
jest
|
|
281
|
+
.spyOn(TelemetryEntityService, "findOneBy")
|
|
282
|
+
.mockImplementationOnce(async () => {
|
|
283
|
+
reads++;
|
|
284
|
+
return null;
|
|
285
|
+
})
|
|
286
|
+
.mockImplementation(async () => {
|
|
287
|
+
reads++;
|
|
288
|
+
return existingRow();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await reconcile("project:k8s-cluster:prod");
|
|
292
|
+
|
|
293
|
+
expect(bumps).toHaveLength(1);
|
|
294
|
+
expect(deletedKeys).toHaveLength(0);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("a successful bump leaves the fence claimed", async () => {
|
|
298
|
+
await reconcile("project:k8s-cluster:prod");
|
|
299
|
+
|
|
300
|
+
expect(deletedKeys).toHaveLength(0);
|
|
301
|
+
});
|
|
302
|
+
});
|