Skip to content

Commit ec25e7d

Browse files
authored
Add support for managing cluster workload groups (#113)
1 parent 74e7e17 commit ec25e7d

File tree

10 files changed

+1023
-40
lines changed

10 files changed

+1023
-40
lines changed

KustoSchemaTools.Tests/Changes/ClusterChangesTest.cs

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,315 @@ public void GenerateChanges_WithNullNewCapacityPolicy_ShouldNotGenerateChanges()
8989
Assert.Empty(changeSet.Scripts);
9090
}
9191

92+
#region Workload Group Tests
93+
94+
[Fact]
95+
public void GenerateChanges_WithNewWorkloadGroup_ShouldDetectCreation()
96+
{
97+
// Arrange
98+
var oldCluster = new Cluster { Name = "TestCluster", WorkloadGroups = new List<WorkloadGroup>() };
99+
var newCluster = new Cluster
100+
{
101+
Name = "TestCluster",
102+
WorkloadGroups = new List<WorkloadGroup>
103+
{
104+
CreateWorkloadGroup("test-group", maxMemoryPerQueryPerNode: 1024)
105+
}
106+
};
107+
108+
// Act
109+
var changeSet = ClusterChanges.GenerateChanges(oldCluster, newCluster, _loggerMock.Object);
110+
111+
// Assert
112+
Assert.NotNull(changeSet);
113+
Assert.NotEmpty(changeSet.Changes);
114+
Assert.NotEmpty(changeSet.Scripts);
115+
116+
var policyChange = Assert.Single(changeSet.Changes) as PolicyChange<WorkloadGroupPolicy>;
117+
Assert.NotNull(policyChange);
118+
Assert.Equal("test-group", policyChange.Entity);
119+
120+
var scriptContainer = Assert.Single(changeSet.Scripts);
121+
Assert.Contains(".create-or-alter workload_group test-group", scriptContainer.Script.Text);
122+
}
123+
124+
[Fact]
125+
public void GenerateChanges_WithUpdatedWorkloadGroup_ShouldDetectUpdate()
126+
{
127+
// Arrange
128+
var oldCluster = new Cluster
129+
{
130+
Name = "TestCluster",
131+
WorkloadGroups = new List<WorkloadGroup>
132+
{
133+
CreateWorkloadGroup("test-group", maxMemoryPerQueryPerNode: 1024)
134+
}
135+
};
136+
var newCluster = new Cluster
137+
{
138+
Name = "TestCluster",
139+
WorkloadGroups = new List<WorkloadGroup>
140+
{
141+
CreateWorkloadGroup("test-group", maxMemoryPerQueryPerNode: 2048)
142+
}
143+
};
144+
145+
// Act
146+
var changeSet = ClusterChanges.GenerateChanges(oldCluster, newCluster, _loggerMock.Object);
147+
148+
// Assert
149+
Assert.NotNull(changeSet);
150+
Assert.NotEmpty(changeSet.Changes);
151+
Assert.NotEmpty(changeSet.Scripts);
152+
153+
var policyChange = Assert.Single(changeSet.Changes) as PolicyChange<WorkloadGroupPolicy>;
154+
Assert.NotNull(policyChange);
155+
Assert.Equal("test-group", policyChange.Entity);
156+
157+
var scriptContainer = Assert.Single(changeSet.Scripts);
158+
Assert.Contains(".alter-merge workload_group test-group", scriptContainer.Script.Text);
159+
}
160+
161+
[Fact]
162+
public void GenerateChanges_WithIdenticalWorkloadGroups_ShouldDetectNoChanges()
163+
{
164+
// Arrange
165+
var workloadGroup = CreateWorkloadGroup("test-group", maxMemoryPerQueryPerNode: 1024);
166+
var oldCluster = new Cluster
167+
{
168+
Name = "TestCluster",
169+
WorkloadGroups = new List<WorkloadGroup> { workloadGroup }
170+
};
171+
var newCluster = new Cluster
172+
{
173+
Name = "TestCluster",
174+
WorkloadGroups = new List<WorkloadGroup>
175+
{
176+
CreateWorkloadGroup("test-group", maxMemoryPerQueryPerNode: 1024)
177+
}
178+
};
179+
180+
// Act
181+
var changeSet = ClusterChanges.GenerateChanges(oldCluster, newCluster, _loggerMock.Object);
182+
183+
// Assert
184+
Assert.NotNull(changeSet);
185+
Assert.Empty(changeSet.Changes);
186+
Assert.Empty(changeSet.Scripts);
187+
}
188+
189+
[Fact]
190+
public void GenerateChanges_WithWorkloadGroupDeletion_ShouldDetectDeletion()
191+
{
192+
// Arrange
193+
var oldCluster = new Cluster
194+
{
195+
Name = "TestCluster",
196+
WorkloadGroups = new List<WorkloadGroup>
197+
{
198+
CreateWorkloadGroup("test-group", maxMemoryPerQueryPerNode: 1024)
199+
}
200+
};
201+
var newCluster = new Cluster
202+
{
203+
Name = "TestCluster",
204+
WorkloadGroups = new List<WorkloadGroup>(),
205+
Deletions = new ClusterDeletions
206+
{
207+
WorkloadGroups = new List<string> { "test-group" }
208+
}
209+
};
210+
211+
// Act
212+
var changeSet = ClusterChanges.GenerateChanges(oldCluster, newCluster, _loggerMock.Object);
213+
214+
// Assert
215+
Assert.NotNull(changeSet);
216+
Assert.NotEmpty(changeSet.Changes);
217+
218+
var deletionChange = Assert.Single(changeSet.Changes) as DeletionChange;
219+
Assert.NotNull(deletionChange);
220+
Assert.Equal("test-group", deletionChange.Entity);
221+
Assert.Equal("workload_group", deletionChange.EntityType);
222+
Assert.Contains("Drop test-group", deletionChange.Markdown);
223+
}
224+
225+
[Fact]
226+
public void GenerateChanges_WithWorkloadGroupDeletionOfNonExistentGroup_ShouldNotCreateChange()
227+
{
228+
// Arrange
229+
var oldCluster = new Cluster { Name = "TestCluster", WorkloadGroups = new List<WorkloadGroup>() };
230+
var newCluster = new Cluster
231+
{
232+
Name = "TestCluster",
233+
WorkloadGroups = new List<WorkloadGroup>(),
234+
Deletions = new ClusterDeletions
235+
{
236+
WorkloadGroups = new List<string> { "non-existent-group" }
237+
}
238+
};
239+
240+
// Act
241+
var changeSet = ClusterChanges.GenerateChanges(oldCluster, newCluster, _loggerMock.Object);
242+
243+
// Assert
244+
Assert.NotNull(changeSet);
245+
Assert.Empty(changeSet.Changes);
246+
Assert.Empty(changeSet.Scripts);
247+
}
248+
249+
[Fact]
250+
public void GenerateChanges_WithWorkloadGroupMarkedForDeletionButAlsoInNewList_ShouldOnlyProcessDeletion()
251+
{
252+
// Arrange
253+
var oldCluster = new Cluster
254+
{
255+
Name = "TestCluster",
256+
WorkloadGroups = new List<WorkloadGroup>
257+
{
258+
CreateWorkloadGroup("test-group", maxMemoryPerQueryPerNode: 1024)
259+
}
260+
};
261+
var newCluster = new Cluster
262+
{
263+
Name = "TestCluster",
264+
WorkloadGroups = new List<WorkloadGroup>
265+
{
266+
CreateWorkloadGroup("test-group", maxMemoryPerQueryPerNode: 2048)
267+
},
268+
Deletions = new ClusterDeletions
269+
{
270+
WorkloadGroups = new List<string> { "test-group" }
271+
}
272+
};
273+
274+
// Act
275+
var changeSet = ClusterChanges.GenerateChanges(oldCluster, newCluster, _loggerMock.Object);
276+
277+
// Assert
278+
Assert.NotNull(changeSet);
279+
Assert.NotEmpty(changeSet.Changes);
280+
281+
var deletionChange = Assert.Single(changeSet.Changes) as DeletionChange;
282+
Assert.NotNull(deletionChange);
283+
Assert.Equal("test-group", deletionChange.Entity);
284+
Assert.Equal("workload_group", deletionChange.EntityType);
285+
}
286+
287+
[Fact]
288+
public void GenerateChanges_WithMultipleWorkloadGroupChanges_ShouldDetectAllChanges()
289+
{
290+
// Arrange
291+
var oldCluster = new Cluster
292+
{
293+
Name = "TestCluster",
294+
WorkloadGroups = new List<WorkloadGroup>
295+
{
296+
CreateWorkloadGroup("group1", maxMemoryPerQueryPerNode: 1024),
297+
CreateWorkloadGroup("group2", maxMemoryPerQueryPerNode: 2048),
298+
CreateWorkloadGroup("group3", maxMemoryPerQueryPerNode: 512)
299+
}
300+
};
301+
var newCluster = new Cluster
302+
{
303+
Name = "TestCluster",
304+
WorkloadGroups = new List<WorkloadGroup>
305+
{
306+
CreateWorkloadGroup("group1", maxMemoryPerQueryPerNode: 1024), // No change
307+
CreateWorkloadGroup("group2", maxMemoryPerQueryPerNode: 4096), // Update
308+
CreateWorkloadGroup("group4", maxMemoryPerQueryPerNode: 256) // New
309+
},
310+
Deletions = new ClusterDeletions
311+
{
312+
WorkloadGroups = new List<string> { "group3" } // Delete
313+
}
314+
};
315+
316+
// Act
317+
var changeSet = ClusterChanges.GenerateChanges(oldCluster, newCluster, _loggerMock.Object);
318+
319+
// Assert
320+
Assert.NotNull(changeSet);
321+
Assert.Equal(3, changeSet.Changes.Count); // 1 deletion + 1 update + 1 creation
322+
323+
// Check deletion
324+
var deletionChange = changeSet.Changes.OfType<DeletionChange>().Single();
325+
Assert.Equal("group3", deletionChange.Entity);
326+
327+
// Check updates/creations
328+
var policyChanges = changeSet.Changes.OfType<PolicyChange<WorkloadGroupPolicy>>().ToList();
329+
Assert.Equal(2, policyChanges.Count);
330+
331+
var group2Change = policyChanges.First(c => c.Entity == "group2");
332+
var group4Change = policyChanges.First(c => c.Entity == "group4");
333+
334+
Assert.NotNull(group2Change);
335+
Assert.NotNull(group4Change);
336+
337+
// Verify scripts
338+
Assert.Equal(3, changeSet.Scripts.Count);
339+
}
340+
341+
[Fact]
342+
public void GenerateChanges_WithWorkloadGroupHavingComplexPolicy_ShouldDetectChanges()
343+
{
344+
// Arrange
345+
var oldCluster = new Cluster
346+
{
347+
Name = "TestCluster",
348+
WorkloadGroups = new List<WorkloadGroup>
349+
{
350+
CreateComplexWorkloadGroup("complex-group", 1024, TimeSpan.FromMinutes(5))
351+
}
352+
};
353+
var newCluster = new Cluster
354+
{
355+
Name = "TestCluster",
356+
WorkloadGroups = new List<WorkloadGroup>
357+
{
358+
CreateComplexWorkloadGroup("complex-group", 2048, TimeSpan.FromMinutes(10))
359+
}
360+
};
361+
362+
// Act
363+
var changeSet = ClusterChanges.GenerateChanges(oldCluster, newCluster, _loggerMock.Object);
364+
365+
// Assert
366+
Assert.NotNull(changeSet);
367+
Assert.NotEmpty(changeSet.Changes);
368+
369+
var policyChange = Assert.Single(changeSet.Changes) as PolicyChange<WorkloadGroupPolicy>;
370+
Assert.NotNull(policyChange);
371+
Assert.Equal("complex-group", policyChange.Entity);
372+
Assert.Contains("MaxMemoryPerQueryPerNode", policyChange.Markdown);
373+
Assert.Contains("MaxExecutionTime", policyChange.Markdown);
374+
}
375+
376+
[Fact]
377+
public void GenerateChanges_WithWorkloadGroupHavingNullPolicy_ShouldNotCreateChange()
378+
{
379+
// Arrange
380+
var oldCluster = new Cluster { Name = "TestCluster", WorkloadGroups = new List<WorkloadGroup>() };
381+
var newCluster = new Cluster
382+
{
383+
Name = "TestCluster",
384+
WorkloadGroups = new List<WorkloadGroup>
385+
{
386+
new WorkloadGroup { WorkloadGroupName = "null-policy-group", WorkloadGroupPolicy = null }
387+
}
388+
};
389+
390+
// Act
391+
var changeSet = ClusterChanges.GenerateChanges(oldCluster, newCluster, _loggerMock.Object);
392+
393+
// Assert
394+
Assert.NotNull(changeSet);
395+
Assert.Empty(changeSet.Changes);
396+
Assert.Empty(changeSet.Scripts);
397+
}
398+
399+
#endregion
400+
92401
#region Helper Methods
93402
private Cluster CreateClusterWithPolicy(
94403
double? ingestionCapacityCoreUtilizationCoefficient = null,
@@ -112,6 +421,65 @@ private Cluster CreateClusterWithPolicy(
112421
}
113422
};
114423
}
424+
425+
private WorkloadGroup CreateWorkloadGroup(string name, long? maxMemoryPerQueryPerNode = null)
426+
{
427+
return new WorkloadGroup
428+
{
429+
WorkloadGroupName = name,
430+
WorkloadGroupPolicy = new WorkloadGroupPolicy
431+
{
432+
RequestLimitsPolicy = new RequestLimitsPolicy
433+
{
434+
MaxMemoryPerQueryPerNode = maxMemoryPerQueryPerNode.HasValue
435+
? new PolicyValue<long> { Value = maxMemoryPerQueryPerNode.Value, IsRelaxable = false }
436+
: null
437+
}
438+
}
439+
};
440+
}
441+
442+
private WorkloadGroup CreateComplexWorkloadGroup(string name, long maxMemoryPerQueryPerNode, TimeSpan maxExecutionTime)
443+
{
444+
return new WorkloadGroup
445+
{
446+
WorkloadGroupName = name,
447+
WorkloadGroupPolicy = new WorkloadGroupPolicy
448+
{
449+
RequestLimitsPolicy = new RequestLimitsPolicy
450+
{
451+
MaxMemoryPerQueryPerNode = new PolicyValue<long> { Value = maxMemoryPerQueryPerNode, IsRelaxable = false },
452+
MaxExecutionTime = new PolicyValue<TimeSpan> { Value = maxExecutionTime, IsRelaxable = true },
453+
MaxResultRecords = new PolicyValue<long> { Value = 10000, IsRelaxable = false }
454+
},
455+
RequestRateLimitPolicies = new List<RequestRateLimitPolicy>
456+
{
457+
new RequestRateLimitPolicy
458+
{
459+
IsEnabled = true,
460+
Scope = RateLimitScope.WorkloadGroup,
461+
LimitKind = RateLimitKind.ConcurrentRequests,
462+
Properties = new RateLimitProperties
463+
{
464+
MaxConcurrentRequests = 100
465+
}
466+
},
467+
new RequestRateLimitPolicy
468+
{
469+
IsEnabled = true,
470+
Scope = RateLimitScope.Principal,
471+
LimitKind = RateLimitKind.ResourceUtilization,
472+
Properties = new RateLimitProperties
473+
{
474+
ResourceKind = RateLimitResourceKind.TotalCpuSeconds,
475+
MaxUtilization = 0.8,
476+
TimeWindow = TimeSpan.FromMinutes(5)
477+
}
478+
}
479+
}
480+
}
481+
};
482+
}
115483
#endregion
116484
}
117485
}

0 commit comments

Comments
 (0)