@oneuptime/common 14.0.5 → 14.0.7

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 (192) hide show
  1. package/Models/DatabaseModels/InventoryItem.ts +23 -12
  2. package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  4. package/Server/Services/AlertService.ts +32 -36
  5. package/Server/Services/IncidentAlertService.ts +609 -2
  6. package/Server/Services/IncidentService.ts +134 -44
  7. package/Server/Services/InventoryItemService.ts +38 -0
  8. package/Server/Services/IoTDeviceCredentialService.ts +13 -4
  9. package/Server/Services/MetricService.ts +1 -1
  10. package/Server/Services/ScheduledMaintenanceService.ts +50 -46
  11. package/Server/Services/UserNotificationRuleService.ts +55 -53
  12. package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
  13. package/Server/Types/Database/QueryHelper.ts +54 -1
  14. package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
  15. package/Server/Utils/AI/Toolbox/ResourceTools.ts +12 -1
  16. package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
  17. package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
  18. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
  19. package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
  20. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
  21. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +4 -3
  22. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +3 -1
  23. package/Server/Utils/Response.ts +31 -0
  24. package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
  25. package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
  26. package/Server/Utils/Topology/TopologyRequest.ts +463 -0
  27. package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
  28. package/Server/Utils/Topology/TopologySql.ts +1024 -0
  29. package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
  30. package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
  31. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
  32. package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
  33. package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
  34. package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
  35. package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
  36. package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
  37. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
  38. package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
  39. package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
  40. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +292 -1
  41. package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
  42. package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
  43. package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
  44. package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
  45. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
  46. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
  47. package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
  48. package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
  49. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
  50. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
  51. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
  52. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
  53. package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
  54. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
  55. package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
  56. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
  57. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
  58. package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
  59. package/Tests/Server/Services/DomainService.test.ts +525 -0
  60. package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
  61. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
  62. package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
  63. package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
  64. package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
  65. package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
  66. package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
  67. package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
  68. package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
  69. package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
  70. package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
  71. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
  72. package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
  73. package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
  74. package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
  75. package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
  76. package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
  77. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
  78. package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
  79. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +96 -0
  80. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
  81. package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
  82. package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
  83. package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
  84. package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
  85. package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
  86. package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
  87. package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
  88. package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
  89. package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
  90. package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
  91. package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
  92. package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
  93. package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
  94. package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
  95. package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
  96. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +8 -14
  97. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +4 -3
  98. package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +27 -6
  99. package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
  100. package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
  101. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
  102. package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
  103. package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
  104. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
  105. package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
  106. package/Types/BaseDatabase/AggregationType.ts +7 -0
  107. package/Types/Dashboard/DashboardTemplates.ts +20 -7
  108. package/Types/Incident/IncidentAlertLink.ts +19 -0
  109. package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
  110. package/Types/Rum/WebVitals.ts +13 -0
  111. package/Types/Telemetry/InventoryLiveness.ts +26 -0
  112. package/Types/Topology/TopologyApi.ts +428 -0
  113. package/Types/Topology/TopologyTypeRules.ts +191 -0
  114. package/UI/Components/Input/Input.tsx +11 -0
  115. package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
  116. package/Utils/Incident/IncidentFromAlerts.ts +78 -0
  117. package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
  118. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js.map +1 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  123. package/build/dist/Server/Services/AlertService.js +21 -33
  124. package/build/dist/Server/Services/AlertService.js.map +1 -1
  125. package/build/dist/Server/Services/IncidentAlertService.js +457 -3
  126. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
  127. package/build/dist/Server/Services/IncidentService.js +96 -42
  128. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  129. package/build/dist/Server/Services/InventoryItemService.js +31 -0
  130. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  131. package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
  132. package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
  133. package/build/dist/Server/Services/MetricService.js +1 -1
  134. package/build/dist/Server/Services/ScheduledMaintenanceService.js +37 -40
  135. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  136. package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
  137. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  138. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
  139. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
  140. package/build/dist/Server/Types/Database/QueryHelper.js +47 -1
  141. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  142. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
  143. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
  144. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +12 -1
  145. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  146. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
  147. package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
  148. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
  149. package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
  150. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
  151. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
  152. package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
  153. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
  154. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  155. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  156. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  157. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1 -1
  158. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  159. package/build/dist/Server/Utils/Response.js +30 -0
  160. package/build/dist/Server/Utils/Response.js.map +1 -1
  161. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
  162. package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
  163. package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
  164. package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
  165. package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
  166. package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
  167. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
  168. package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
  169. package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
  170. package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
  171. package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
  172. package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
  173. package/build/dist/Types/Dashboard/DashboardTemplates.js +20 -7
  174. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  175. package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
  176. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
  177. package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
  178. package/build/dist/Types/Rum/WebVitals.js +12 -0
  179. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  180. package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
  181. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
  182. package/build/dist/Types/Topology/TopologyApi.js +55 -0
  183. package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
  184. package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
  185. package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
  186. package/build/dist/UI/Components/Input/Input.js +1 -1
  187. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  188. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
  189. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
  190. package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
  191. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
  192. package/package.json +1 -1
@@ -0,0 +1,534 @@
1
+ import TopologyConcurrencyLimiterInstance, {
2
+ TOPOLOGY_BUSY_MESSAGE,
3
+ TOPOLOGY_BUSY_RETRY_AFTER_SECONDS,
4
+ TOPOLOGY_CONCURRENCY_LIMITS,
5
+ TopologyConcurrencyLimiter,
6
+ TopologyConcurrencyLimits,
7
+ TopologyRequestAbandonedError,
8
+ } from "../../../../Server/Utils/Topology/TopologyConcurrencyLimiter";
9
+ import ExceptionCode from "../../../../Types/Exception/ExceptionCode";
10
+ import TooManyRequestsException from "../../../../Types/Exception/TooManyRequestsException";
11
+ import { describe, expect, test } from "@jest/globals";
12
+
13
+ /*
14
+ * The Topology API's per-process limiter: how many reads run at once, per
15
+ * process and per project; that waiters start in arrival order without a
16
+ * project at its limit holding up another; that a full queue or a long wait
17
+ * is a 429 without running anything; and that a slot is always given back,
18
+ * however the work ends.
19
+ */
20
+
21
+ const LIMITS: TopologyConcurrencyLimits = {
22
+ maxRunning: 4,
23
+ maxRunningPerProject: 2,
24
+ maxWaiting: 64,
25
+ maxWaitingPerProject: 16,
26
+ maxWaitMs: 60_000,
27
+ };
28
+
29
+ interface Job {
30
+ label: string;
31
+ started: boolean;
32
+ settled: boolean;
33
+ result: Promise<string>;
34
+ finish: () => void;
35
+ fail: (error: Error) => void;
36
+ }
37
+
38
+ /* Lets queued promise callbacks (a slot handed over, work starting) run. */
39
+ async function settle(): Promise<void> {
40
+ for (let round: number = 0; round < 3; round++) {
41
+ await new Promise<void>((resolve: () => void): void => {
42
+ setTimeout(resolve, 0);
43
+ });
44
+ }
45
+ }
46
+
47
+ /*
48
+ * A unit of work under the limiter that runs until the test finishes or
49
+ * fails it. `started` records whether the limiter let it run; `order` the
50
+ * order in which jobs started.
51
+ */
52
+ function launch(
53
+ limiter: TopologyConcurrencyLimiter,
54
+ projectId: string,
55
+ label: string,
56
+ order: Array<string>,
57
+ signal?: AbortSignal,
58
+ ): Job {
59
+ let finish: () => void = (): void => {
60
+ return undefined;
61
+ };
62
+ let fail: (error: Error) => void = (): void => {
63
+ return undefined;
64
+ };
65
+ const job: Job = {
66
+ label,
67
+ started: false,
68
+ settled: false,
69
+ result: Promise.resolve(""),
70
+ finish: (): void => {
71
+ finish();
72
+ },
73
+ fail: (error: Error): void => {
74
+ fail(error);
75
+ },
76
+ };
77
+ job.result = limiter.run(
78
+ projectId,
79
+ (): Promise<string> => {
80
+ job.started = true;
81
+ order.push(label);
82
+ return new Promise<string>(
83
+ (resolve: (value: string) => void, reject: (error: Error) => void) => {
84
+ finish = (): void => {
85
+ resolve(label);
86
+ };
87
+ fail = reject;
88
+ },
89
+ );
90
+ },
91
+ signal,
92
+ );
93
+ job.result.then(
94
+ (): void => {
95
+ job.settled = true;
96
+ },
97
+ (): void => {
98
+ job.settled = true;
99
+ },
100
+ );
101
+ return job;
102
+ }
103
+
104
+ async function refusal(job: Job): Promise<unknown> {
105
+ try {
106
+ await job.result;
107
+ } catch (error) {
108
+ return error;
109
+ }
110
+ return null;
111
+ }
112
+
113
+ describe("TopologyConcurrencyLimiter", () => {
114
+ test("defaults: 4 at once per process, 2 per project, 64 waiting (16 per project), 30 s wait", () => {
115
+ expect(TOPOLOGY_CONCURRENCY_LIMITS).toEqual({
116
+ maxRunning: 4,
117
+ maxRunningPerProject: 2,
118
+ maxWaiting: 64,
119
+ maxWaitingPerProject: 16,
120
+ maxWaitMs: 30_000,
121
+ });
122
+ expect(TopologyConcurrencyLimiterInstance).toBeInstanceOf(
123
+ TopologyConcurrencyLimiter,
124
+ );
125
+ expect(TOPOLOGY_BUSY_RETRY_AFTER_SECONDS).toBeGreaterThan(0);
126
+ });
127
+
128
+ test("runs the work and hands back its result, then frees the slot", async () => {
129
+ const limiter: TopologyConcurrencyLimiter = new TopologyConcurrencyLimiter(
130
+ LIMITS,
131
+ );
132
+ expect(
133
+ await limiter.run("p", async (): Promise<number> => {
134
+ expect(limiter.runningCount()).toBe(1);
135
+ expect(limiter.runningCount("p")).toBe(1);
136
+ return 42;
137
+ }),
138
+ ).toBe(42);
139
+ expect(limiter.runningCount()).toBe(0);
140
+ expect(limiter.runningCount("p")).toBe(0);
141
+ });
142
+
143
+ test("never more than the process limit at once, and waiters start as slots free up", async () => {
144
+ const limiter: TopologyConcurrencyLimiter = new TopologyConcurrencyLimiter(
145
+ LIMITS,
146
+ );
147
+ const order: Array<string> = [];
148
+ const jobs: Array<Job> = ["a", "b", "c", "d", "e", "f"].map(
149
+ (project: string): Job => {
150
+ return launch(limiter, project, project, order);
151
+ },
152
+ );
153
+ await settle();
154
+ expect(order).toEqual(["a", "b", "c", "d"]);
155
+ expect(limiter.runningCount()).toBe(4);
156
+ expect(limiter.waitingCount()).toBe(2);
157
+
158
+ jobs[1]!.finish();
159
+ await settle();
160
+ expect(order).toEqual(["a", "b", "c", "d", "e"]);
161
+ expect(limiter.runningCount()).toBe(4);
162
+
163
+ for (const job of jobs) {
164
+ job.finish();
165
+ }
166
+ await settle();
167
+ expect(order).toEqual(["a", "b", "c", "d", "e", "f"]);
168
+ for (const job of jobs) {
169
+ job.finish();
170
+ }
171
+ await Promise.all(
172
+ jobs.map((job: Job): Promise<string> => {
173
+ return job.result;
174
+ }),
175
+ );
176
+ expect(limiter.runningCount()).toBe(0);
177
+ expect(limiter.waitingCount()).toBe(0);
178
+ });
179
+
180
+ test("one project gets at most its share; another project is not held up behind it", async () => {
181
+ const limiter: TopologyConcurrencyLimiter = new TopologyConcurrencyLimiter(
182
+ LIMITS,
183
+ );
184
+ const order: Array<string> = [];
185
+ const busy: Array<Job> = [1, 2, 3, 4, 5].map((index: number): Job => {
186
+ return launch(limiter, "busy", `busy-${index}`, order);
187
+ });
188
+ await settle();
189
+ expect(order).toEqual(["busy-1", "busy-2"]);
190
+ expect(limiter.runningCount("busy")).toBe(2);
191
+ expect(limiter.waitingCount("busy")).toBe(3);
192
+
193
+ // Queued after busy's waiters, yet starts at once: the process has room.
194
+ const quiet: Job = launch(limiter, "quiet", "quiet-1", order);
195
+ await settle();
196
+ expect(quiet.started).toBe(true);
197
+ expect(order).toEqual(["busy-1", "busy-2", "quiet-1"]);
198
+
199
+ // busy's own waiters go in arrival order, one per slot busy gives back.
200
+ busy[0]!.finish();
201
+ await settle();
202
+ expect(order).toEqual(["busy-1", "busy-2", "quiet-1", "busy-3"]);
203
+ expect(busy[3]!.started).toBe(false);
204
+
205
+ for (const job of [...busy, quiet]) {
206
+ job.finish();
207
+ }
208
+ await settle();
209
+ for (const job of busy) {
210
+ job.finish();
211
+ }
212
+ await Promise.all(
213
+ [...busy, quiet].map((job: Job): Promise<string> => {
214
+ return job.result;
215
+ }),
216
+ );
217
+ expect(order).toEqual([
218
+ "busy-1",
219
+ "busy-2",
220
+ "quiet-1",
221
+ "busy-3",
222
+ "busy-4",
223
+ "busy-5",
224
+ ]);
225
+ });
226
+
227
+ test("a freed slot goes to the first waiter that may use it, skipping projects at their limit", async () => {
228
+ const limiter: TopologyConcurrencyLimiter = new TopologyConcurrencyLimiter({
229
+ ...LIMITS,
230
+ maxRunning: 2,
231
+ maxRunningPerProject: 1,
232
+ });
233
+ const order: Array<string> = [];
234
+ const a1: Job = launch(limiter, "a", "a1", order);
235
+ const b1: Job = launch(limiter, "b", "b1", order);
236
+ const a2: Job = launch(limiter, "a", "a2", order);
237
+ const a3: Job = launch(limiter, "a", "a3", order);
238
+ const b2: Job = launch(limiter, "b", "b2", order);
239
+ const c1: Job = launch(limiter, "c", "c1", order);
240
+ await settle();
241
+ expect(order).toEqual(["a1", "b1"]);
242
+
243
+ // b's slot: a2 and a3 are first in line but a is at its limit.
244
+ b1.finish();
245
+ await settle();
246
+ expect(order).toEqual(["a1", "b1", "b2"]);
247
+
248
+ // a's slot: a2, the first in line that may start now.
249
+ a1.finish();
250
+ await settle();
251
+ expect(order).toEqual(["a1", "b1", "b2", "a2"]);
252
+
253
+ b2.finish();
254
+ await settle();
255
+ expect(order).toEqual(["a1", "b1", "b2", "a2", "c1"]);
256
+
257
+ for (const job of [a2, c1]) {
258
+ job.finish();
259
+ }
260
+ await settle();
261
+ a3.finish();
262
+ await Promise.all(
263
+ [a1, b1, a2, a3, b2, c1].map((job: Job): Promise<string> => {
264
+ return job.result;
265
+ }),
266
+ );
267
+ expect(order).toEqual(["a1", "b1", "b2", "a2", "c1", "a3"]);
268
+ });
269
+
270
+ test("a full queue is a 429 at once, and the refused work never runs", async () => {
271
+ const limiter: TopologyConcurrencyLimiter = new TopologyConcurrencyLimiter({
272
+ ...LIMITS,
273
+ maxRunning: 1,
274
+ maxRunningPerProject: 1,
275
+ maxWaiting: 2,
276
+ });
277
+ const order: Array<string> = [];
278
+ const running: Job = launch(limiter, "a", "running", order);
279
+ const waiting: Array<Job> = [
280
+ launch(limiter, "b", "waiting-1", order),
281
+ launch(limiter, "c", "waiting-2", order),
282
+ ];
283
+ const refused: Job = launch(limiter, "d", "refused", order);
284
+ await settle();
285
+
286
+ const error: unknown = await refusal(refused);
287
+ expect(error).toBeInstanceOf(TooManyRequestsException);
288
+ expect((error as TooManyRequestsException).code).toBe(
289
+ ExceptionCode.TooManyRequestsException,
290
+ );
291
+ expect((error as Error).message).toBe(TOPOLOGY_BUSY_MESSAGE);
292
+ expect(limiter.waitingCount()).toBe(2);
293
+
294
+ running.finish();
295
+ for (const job of waiting) {
296
+ await settle();
297
+ job.finish();
298
+ }
299
+ await Promise.all(
300
+ [running, ...waiting].map((job: Job): Promise<string> => {
301
+ return job.result;
302
+ }),
303
+ );
304
+ expect(order).toEqual(["running", "waiting-1", "waiting-2"]);
305
+ expect(refused.started).toBe(false);
306
+ });
307
+
308
+ test("one project cannot fill the queue: past its own share it is refused, others still queue", async () => {
309
+ const limiter: TopologyConcurrencyLimiter = new TopologyConcurrencyLimiter({
310
+ ...LIMITS,
311
+ maxRunning: 1,
312
+ maxRunningPerProject: 1,
313
+ maxWaiting: 10,
314
+ maxWaitingPerProject: 2,
315
+ });
316
+ const order: Array<string> = [];
317
+ const jobs: Array<Job> = [
318
+ launch(limiter, "a", "a-running", order),
319
+ launch(limiter, "a", "a-waiting-1", order),
320
+ launch(limiter, "a", "a-waiting-2", order),
321
+ ];
322
+ const refused: Job = launch(limiter, "a", "a-refused", order);
323
+ const other: Job = launch(limiter, "b", "b-waiting", order);
324
+ await settle();
325
+ expect(await refusal(refused)).toBeInstanceOf(TooManyRequestsException);
326
+ expect(limiter.waitingCount("a")).toBe(2);
327
+ expect(limiter.waitingCount("b")).toBe(1);
328
+
329
+ for (const job of [...jobs, other]) {
330
+ job.finish();
331
+ await settle();
332
+ }
333
+ for (const job of [...jobs, other]) {
334
+ job.finish();
335
+ }
336
+ await Promise.all(
337
+ [...jobs, other].map((job: Job): Promise<string> => {
338
+ return job.result;
339
+ }),
340
+ );
341
+ expect(refused.started).toBe(false);
342
+ });
343
+
344
+ test("a slot is given back when the work fails or throws, and the next waiter runs", async () => {
345
+ const limiter: TopologyConcurrencyLimiter = new TopologyConcurrencyLimiter({
346
+ ...LIMITS,
347
+ maxRunning: 1,
348
+ });
349
+ const order: Array<string> = [];
350
+ const failing: Job = launch(limiter, "a", "failing", order);
351
+ const next: Job = launch(limiter, "b", "next", order);
352
+ await settle();
353
+ expect(next.started).toBe(false);
354
+
355
+ failing.fail(new Error("statement timeout"));
356
+ await expect(failing.result).rejects.toThrow("statement timeout");
357
+ await settle();
358
+ expect(next.started).toBe(true);
359
+ expect(limiter.runningCount("a")).toBe(0);
360
+
361
+ next.finish();
362
+ await next.result;
363
+
364
+ // Work that throws before returning a promise frees its slot too.
365
+ await expect(
366
+ limiter.run("c", (): Promise<string> => {
367
+ throw new Error("boom");
368
+ }),
369
+ ).rejects.toThrow("boom");
370
+ expect(limiter.runningCount()).toBe(0);
371
+ expect(
372
+ await limiter.run("c", async (): Promise<string> => {
373
+ return "still works";
374
+ }),
375
+ ).toBe("still works");
376
+ });
377
+
378
+ test("a request that waits past the limit is a 429, leaves the queue and never runs", async () => {
379
+ const limiter: TopologyConcurrencyLimiter = new TopologyConcurrencyLimiter({
380
+ ...LIMITS,
381
+ maxRunning: 1,
382
+ maxWaitMs: 25,
383
+ });
384
+ const order: Array<string> = [];
385
+ const running: Job = launch(limiter, "a", "running", order);
386
+ const waiting: Job = launch(limiter, "b", "waiting", order);
387
+ await settle();
388
+ expect(limiter.waitingCount()).toBe(1);
389
+
390
+ const error: unknown = await refusal(waiting);
391
+ expect(error).toBeInstanceOf(TooManyRequestsException);
392
+ expect(limiter.waitingCount()).toBe(0);
393
+
394
+ running.finish();
395
+ await running.result;
396
+ await settle();
397
+ expect(waiting.started).toBe(false);
398
+ expect(order).toEqual(["running"]);
399
+ expect(limiter.runningCount()).toBe(0);
400
+ });
401
+
402
+ test("a waiter that gets its slot in time is not refused later", async () => {
403
+ const limiter: TopologyConcurrencyLimiter = new TopologyConcurrencyLimiter({
404
+ ...LIMITS,
405
+ maxRunning: 1,
406
+ maxWaitMs: 40,
407
+ });
408
+ const order: Array<string> = [];
409
+ const running: Job = launch(limiter, "a", "running", order);
410
+ const waiting: Job = launch(limiter, "b", "waiting", order);
411
+ await settle();
412
+ running.finish();
413
+ await settle();
414
+ expect(waiting.started).toBe(true);
415
+ // Past the wait limit while running: the timer was cleared.
416
+ await new Promise<void>((resolve: () => void): void => {
417
+ setTimeout(resolve, 60);
418
+ });
419
+ expect(waiting.settled).toBe(false);
420
+ waiting.finish();
421
+ expect(await waiting.result).toBe("waiting");
422
+ });
423
+ /*
424
+ * The drawer aborts the previous entity's request on every click. A queued
425
+ * request whose client has gone must give its place back at once and never
426
+ * run, or one person clicking through the drawer fills their project's
427
+ * queue with dead reads ahead of the one they are waiting for.
428
+ */
429
+ describe("a request whose client has gone", () => {
430
+ test("leaves the queue at once, never runs, and frees its place for others", async () => {
431
+ const limiter: TopologyConcurrencyLimiter =
432
+ new TopologyConcurrencyLimiter({
433
+ ...LIMITS,
434
+ maxRunning: 1,
435
+ maxWaitingPerProject: 1,
436
+ });
437
+ const order: Array<string> = [];
438
+ const running: Job = launch(limiter, "a", "running", order);
439
+ const controller: AbortController = new AbortController();
440
+ const abandoned: Job = launch(
441
+ limiter,
442
+ "a",
443
+ "abandoned",
444
+ order,
445
+ controller.signal,
446
+ );
447
+ await settle();
448
+ expect(limiter.waitingCount("a")).toBe(1);
449
+
450
+ controller.abort();
451
+ expect(await refusal(abandoned)).toBeInstanceOf(
452
+ TopologyRequestAbandonedError,
453
+ );
454
+ expect(limiter.waitingCount("a")).toBe(0);
455
+
456
+ // Its place is free again: the project's next request queues, not 429.
457
+ const next: Job = launch(limiter, "a", "next", order);
458
+ await settle();
459
+ expect(next.settled).toBe(false);
460
+ expect(limiter.waitingCount("a")).toBe(1);
461
+
462
+ running.finish();
463
+ await settle();
464
+ next.finish();
465
+ expect(await next.result).toBe("next");
466
+ expect(order).toEqual(["running", "next"]);
467
+ expect(abandoned.started).toBe(false);
468
+ expect(limiter.runningCount()).toBe(0);
469
+ });
470
+
471
+ test("an already-abandoned request takes no slot and never runs", async () => {
472
+ const limiter: TopologyConcurrencyLimiter =
473
+ new TopologyConcurrencyLimiter(LIMITS);
474
+ const controller: AbortController = new AbortController();
475
+ controller.abort();
476
+ const order: Array<string> = [];
477
+ const job: Job = launch(limiter, "a", "gone", order, controller.signal);
478
+ expect(await refusal(job)).toBeInstanceOf(TopologyRequestAbandonedError);
479
+ expect(job.started).toBe(false);
480
+ expect(limiter.runningCount()).toBe(0);
481
+ expect(limiter.waitingCount()).toBe(0);
482
+ });
483
+
484
+ test("abandoned while its slot is being handed over: the slot goes back unused", async () => {
485
+ const limiter: TopologyConcurrencyLimiter =
486
+ new TopologyConcurrencyLimiter({ ...LIMITS, maxRunning: 1 });
487
+ const order: Array<string> = [];
488
+ const running: Job = launch(limiter, "a", "running", order);
489
+ const controller: AbortController = new AbortController();
490
+ const handedOver: Job = launch(
491
+ limiter,
492
+ "b",
493
+ "handed-over",
494
+ order,
495
+ controller.signal,
496
+ );
497
+ const after: Job = launch(limiter, "c", "after", order);
498
+ await settle();
499
+
500
+ // The slot is granted synchronously; the abort lands before work runs.
501
+ running.finish();
502
+ controller.abort();
503
+ expect(await refusal(handedOver)).toBeInstanceOf(
504
+ TopologyRequestAbandonedError,
505
+ );
506
+ await settle();
507
+ expect(handedOver.started).toBe(false);
508
+ expect(after.started).toBe(true);
509
+ after.finish();
510
+ await after.result;
511
+ expect(limiter.runningCount()).toBe(0);
512
+ });
513
+
514
+ test("a read that already started is not interrupted by its client leaving", async () => {
515
+ const limiter: TopologyConcurrencyLimiter =
516
+ new TopologyConcurrencyLimiter(LIMITS);
517
+ const controller: AbortController = new AbortController();
518
+ const order: Array<string> = [];
519
+ const job: Job = launch(
520
+ limiter,
521
+ "a",
522
+ "started",
523
+ order,
524
+ controller.signal,
525
+ );
526
+ await settle();
527
+ expect(job.started).toBe(true);
528
+ controller.abort();
529
+ job.finish();
530
+ expect(await job.result).toBe("started");
531
+ expect(limiter.runningCount()).toBe(0);
532
+ });
533
+ });
534
+ });