feat(server): adds minimum keepalive and headroom, chore: updates packages to fix vulnerabilities
Build & Deploy / build (push) Failing after 1m45s

This commit is contained in:
2026-07-29 13:18:19 +02:00
parent 94bf0b554b
commit 3ae9123597
12 changed files with 181 additions and 22 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.10" />
</ItemGroup>
<PropertyGroup>
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ngino.Client.Tests")]
[assembly: InternalsVisibleTo("ReverseLlama.Client.Tests")]
+12 -2
View File
@@ -369,7 +369,14 @@ internal static class AdminEndpoints
try
{
var member = store.AddGroupClient(id, request.ClientId, request.Model, request.ClientPattern);
var member = store.AddGroupClient(
id,
request.ClientId,
request.Model,
request.ClientPattern,
request.KeepaliveInstancesToKeepAlive,
request.KeepaliveMaxParallelismPerClient,
request.KeepaliveParallelismHeadroom);
return Results.Json(member);
}
catch (ArgumentException exception)
@@ -889,7 +896,10 @@ internal sealed record UpdateGroupRequest(string Name);
internal sealed record AddGroupClientRequest(
string? ClientId,
string? Model,
string? ClientPattern);
string? ClientPattern,
int? KeepaliveInstancesToKeepAlive,
int? KeepaliveMaxParallelismPerClient,
int? KeepaliveParallelismHeadroom);
internal sealed record SetUserKeyGroupsRequest(IReadOnlyList<string>? GroupIds);
+100 -8
View File
@@ -824,7 +824,10 @@ internal sealed class ManagementStore
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT id, group_id, client_id, model, client_pattern
SELECT id, group_id, client_id, model, client_pattern,
keepalive_instances_to_keep_alive,
keepalive_max_parallelism_per_client,
keepalive_parallelism_headroom
FROM group_members
WHERE group_id = $group_id
ORDER BY client_id, model, client_pattern
@@ -840,14 +843,22 @@ internal sealed class ManagementStore
reader.GetString(1),
reader.IsDBNull(2) ? null : reader.GetString(2),
reader.IsDBNull(3) ? null : reader.GetString(3),
reader.IsDBNull(4) ? null : reader.GetString(4)));
reader.IsDBNull(4) ? null : reader.GetString(4),
ReadKeepalivePolicy(reader, 5, 6, 7)));
}
return result;
}
}
public GroupClientInfo AddGroupClient(string groupId, string? clientId, string? model, string? clientPattern)
public GroupClientInfo AddGroupClient(
string groupId,
string? clientId,
string? model,
string? clientPattern,
int? keepaliveInstancesToKeepAlive,
int? keepaliveMaxParallelismPerClient,
int? keepaliveParallelismHeadroom)
{
EnsureAvailable();
@@ -873,24 +884,46 @@ internal sealed class ManagementStore
}
}
var policy = NormalizeKeepalivePolicy(
keepaliveInstancesToKeepAlive,
keepaliveMaxParallelismPerClient,
keepaliveParallelismHeadroom);
lock (_lock)
{
using var connection = OpenConnection();
using var command = connection.CreateCommand();
command.CommandText = """
INSERT INTO group_members (group_id, client_id, model, client_pattern)
VALUES ($group_id, $client_id, $model, $client_pattern)
INSERT INTO group_members (
group_id,
client_id,
model,
client_pattern,
keepalive_instances_to_keep_alive,
keepalive_max_parallelism_per_client,
keepalive_parallelism_headroom)
VALUES (
$group_id,
$client_id,
$model,
$client_pattern,
$keepalive_instances_to_keep_alive,
$keepalive_max_parallelism_per_client,
$keepalive_parallelism_headroom)
""";
command.Parameters.AddWithValue("$group_id", groupId);
command.Parameters.AddWithValue("$client_id", string.IsNullOrWhiteSpace(clientId) ? DBNull.Value : clientId);
command.Parameters.AddWithValue("$model", string.IsNullOrWhiteSpace(model) ? DBNull.Value : model);
command.Parameters.AddWithValue("$client_pattern", string.IsNullOrWhiteSpace(clientPattern) ? DBNull.Value : clientPattern);
command.Parameters.AddWithValue("$keepalive_instances_to_keep_alive", policy.InstancesToKeepAlive);
command.Parameters.AddWithValue("$keepalive_max_parallelism_per_client", policy.MaxParallelismPerClient);
command.Parameters.AddWithValue("$keepalive_parallelism_headroom", policy.ParallelismHeadroom);
command.ExecuteNonQuery();
using var idCommand = connection.CreateCommand();
idCommand.CommandText = "SELECT last_insert_rowid()";
var insertedId = (long)idCommand.ExecuteScalar()!;
return new GroupClientInfo(insertedId, groupId, clientId, model, clientPattern);
return new GroupClientInfo(insertedId, groupId, clientId, model, clientPattern, policy);
}
}
@@ -1987,7 +2020,10 @@ internal sealed class ManagementStore
group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
client_id TEXT NULL,
model TEXT NULL,
client_pattern TEXT NULL
client_pattern TEXT NULL,
keepalive_instances_to_keep_alive INTEGER NOT NULL DEFAULT 1,
keepalive_max_parallelism_per_client INTEGER NOT NULL DEFAULT 1,
keepalive_parallelism_headroom INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX IF NOT EXISTS idx_group_members_group_id
@@ -2010,6 +2046,29 @@ internal sealed class ManagementStore
""";
command.ExecuteNonQuery();
using (var migrate = connection.CreateCommand())
{
migrate.CommandText = """
SELECT COUNT(*) FROM pragma_table_info('group_members') WHERE name = 'keepalive_instances_to_keep_alive'
""";
var hasKeepaliveInstancesColumn = (long)migrate.ExecuteScalar()! > 0;
if (!hasKeepaliveInstancesColumn)
{
using var addKeepaliveInstances = connection.CreateCommand();
addKeepaliveInstances.CommandText = "ALTER TABLE group_members ADD COLUMN keepalive_instances_to_keep_alive INTEGER NOT NULL DEFAULT 1";
addKeepaliveInstances.ExecuteNonQuery();
using var addKeepaliveMaxParallelism = connection.CreateCommand();
addKeepaliveMaxParallelism.CommandText = "ALTER TABLE group_members ADD COLUMN keepalive_max_parallelism_per_client INTEGER NOT NULL DEFAULT 1";
addKeepaliveMaxParallelism.ExecuteNonQuery();
using var addKeepaliveHeadroom = connection.CreateCommand();
addKeepaliveHeadroom.CommandText = "ALTER TABLE group_members ADD COLUMN keepalive_parallelism_headroom INTEGER NOT NULL DEFAULT 1";
addKeepaliveHeadroom.ExecuteNonQuery();
}
}
using (var migrate = connection.CreateCommand())
{
migrate.CommandText = """
@@ -2231,6 +2290,30 @@ internal sealed class ManagementStore
return connection;
}
private static GroupClientKeepalivePolicy? ReadKeepalivePolicy(SqliteDataReader reader, int instancesOrdinal, int maxParallelismOrdinal, int headroomOrdinal)
{
if (reader.IsDBNull(instancesOrdinal) || reader.IsDBNull(maxParallelismOrdinal) || reader.IsDBNull(headroomOrdinal))
{
return null;
}
return NormalizeKeepalivePolicy(
reader.GetInt32(instancesOrdinal),
reader.GetInt32(maxParallelismOrdinal),
reader.GetInt32(headroomOrdinal));
}
private static GroupClientKeepalivePolicy NormalizeKeepalivePolicy(
int? instancesToKeepAlive,
int? maxParallelismPerClient,
int? parallelismHeadroom)
{
return new GroupClientKeepalivePolicy(
Math.Max(1, instancesToKeepAlive ?? GroupClientKeepalivePolicy.Default.InstancesToKeepAlive),
Math.Max(1, maxParallelismPerClient ?? GroupClientKeepalivePolicy.Default.MaxParallelismPerClient),
Math.Max(1, parallelismHeadroom ?? GroupClientKeepalivePolicy.Default.ParallelismHeadroom));
}
private void EnsureAvailable()
{
if (!_isAvailable)
@@ -2420,7 +2503,16 @@ internal sealed record GroupClientInfo(
string GroupId,
string? ClientId,
string? Model,
string? ClientPattern);
string? ClientPattern,
GroupClientKeepalivePolicy? KeepalivePolicy = null);
internal sealed record GroupClientKeepalivePolicy(
int InstancesToKeepAlive,
int MaxParallelismPerClient,
int ParallelismHeadroom)
{
public static GroupClientKeepalivePolicy Default { get; } = new(1, 1, 1);
}
internal sealed record UserKeyGroupInfo(
string UserKeyId,
+13 -7
View File
@@ -3,14 +3,20 @@
<ItemGroup>
<PackageReference Include="ElmahCore" Version="2.1.2" />
<PackageReference Include="ElmahCore.MySql" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.28" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.28" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.3" />
<PackageReference Include="System.Text.Encodings.Web" Version="8.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.10" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
<PackageReference Include="SSH.NET" Version="2024.1.0" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.5" />
<PackageReference Include="System.Data.SqlClient" Version="4.9.1" />
<PackageReference Include="System.Drawing.Common" Version="9.0.0" />
<PackageReference Include="System.Text.Encodings.Web" Version="10.0.10" />
<PackageReference Include="System.Text.Json" Version="10.0.10" />
<ProjectReference Include="..\Ngino.Protocol\Ngino.Protocol.csproj" />
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Ngino.Client.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<PropertyGroup>
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("ReverseLlama.Client.Tests")]
+29 -1
View File
@@ -228,6 +228,9 @@ content.addEventListener("submit", async (event) => {
if (data.clientPattern && data.clientPattern.trim()) {
body.clientPattern = data.clientPattern.trim();
}
body.keepaliveInstancesToKeepAlive = parseInt(data.keepaliveInstancesToKeepAlive, 10) || 1;
body.keepaliveMaxParallelismPerClient = parseInt(data.keepaliveMaxParallelismPerClient, 10) || 1;
body.keepaliveParallelismHeadroom = parseInt(data.keepaliveParallelismHeadroom, 10) || 1;
if (!body.clientId && !body.clientPattern) {
throw new Error("Either Client ID or Client pattern is required.");
@@ -981,9 +984,21 @@ function renderGroupDetail() {
<label for="addClientPattern">Client pattern (regex, optional)</label>
<input class="input" id="addClientPattern" name="clientPattern" placeholder="GPU_[0-9]*">
</div>
<div class="field">
<label for="addClientKeepaliveInstances">Keepalive instances</label>
<input class="input" id="addClientKeepaliveInstances" name="keepaliveInstancesToKeepAlive" type="number" min="1" step="1" value="1">
</div>
<div class="field">
<label for="addClientKeepaliveMaxParallelism">Max parallelism per client</label>
<input class="input" id="addClientKeepaliveMaxParallelism" name="keepaliveMaxParallelismPerClient" type="number" min="1" step="1" value="1">
</div>
<div class="field">
<label for="addClientKeepaliveHeadroom">Parallelism headroom</label>
<input class="input" id="addClientKeepaliveHeadroom" name="keepaliveParallelismHeadroom" type="number" min="1" step="1" value="1">
</div>
<button class="button" type="submit">Add</button>
</form>
<div class="cell-sub" style="margin-top:8px">Provide either a Client ID (for explicit client access) or a Client pattern (for regex-based matching). Model is optional to restrict to a specific model.</div>
<div class="cell-sub" style="margin-top:8px">Provide either a Client ID (for explicit client access) or a Client pattern (for regex-based matching). Model is optional to restrict to a specific model. Keepalive defaults to 1 instance, 1 parallelism, and 1 headroom.</div>
</div>
</div>
<div class="panel">
@@ -1096,6 +1111,17 @@ function renderGroupDetail() {
`);
}
function formatKeepalivePolicy(policy) {
if (!policy) {
return `<div class="cell-sub">Default (1 / 1 / 1)</div>`;
}
return `
<div class="cell-main">${policy.instancesToKeepAlive} instance${policy.instancesToKeepAlive === 1 ? "" : "s"}</div>
<div class="cell-sub">Parallelism ${policy.maxParallelismPerClient} · headroom ${policy.parallelismHeadroom}</div>
`;
}
function groupClientsTable(clients, groupId) {
const rows = clients.map((client) => `
<tr>
@@ -1117,6 +1143,7 @@ function groupClientsTable(clients, groupId) {
: `<div class="cell-sub">-</div>`
}
</td>
<td>${formatKeepalivePolicy(client.keepalivePolicy)}</td>
<td>
<button class="button danger" data-action="remove-client" data-group-id="${escapeAttr(groupId)}" data-member-id="${client.id}">Remove</button>
</td>
@@ -1130,6 +1157,7 @@ function groupClientsTable(clients, groupId) {
<th>Client ID</th>
<th>Model</th>
<th>Client pattern</th>
<th>Keepalive policy</th>
<th></th>
</tr>
</thead>
@@ -0,0 +1,18 @@
using Ngino.Server;
using Xunit;
namespace ReverseLlama.Client.Tests;
public class GroupKeepalivePolicyTests
{
[Fact]
public void KeepalivePolicy_IsRoundTripped_ThroughGroupClientInfo()
{
var policy = new GroupClientKeepalivePolicy(2, 3, 4);
var info = new GroupClientInfo(1, "group-1", "client-1", "model-a", "pattern", policy);
Assert.Equal(2, info.KeepalivePolicy?.InstancesToKeepAlive);
Assert.Equal(3, info.KeepalivePolicy?.MaxParallelismPerClient);
Assert.Equal(4, info.KeepalivePolicy?.ParallelismHeadroom);
}
}
@@ -9,9 +9,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@@ -19,6 +19,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Ngino.Client\Ngino.Client.csproj" />
<ProjectReference Include="..\..\src\Ngino.Server\Ngino.Server.csproj" />
</ItemGroup>
</Project>