Skip to content

Commit 126f33d

Browse files
committed
Merge branch 'develop' into feature/dotnet-10
2 parents 9981506 + b2a9596 commit 126f33d

11 files changed

Lines changed: 165 additions & 121 deletions

File tree

.github/actions/kubernetes-rollout-restart/action.yml

Lines changed: 0 additions & 46 deletions
This file was deleted.

.github/workflows/ci-build.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151

5252
steps:
5353
- name: Checkout
54-
uses: actions/checkout@v5
54+
uses: actions/checkout@v6
5555

5656
- uses: actions/setup-dotnet@v5
5757
with:
@@ -78,7 +78,7 @@ jobs:
7878

7979
steps:
8080
- name: Checkout
81-
uses: actions/checkout@v5
81+
uses: actions/checkout@v6
8282

8383
- name: Set up Docker Buildx
8484
uses: docker/setup-buildx-action@v3
@@ -114,7 +114,7 @@ jobs:
114114

115115
steps:
116116
- name: Checkout
117-
uses: actions/checkout@v5
117+
uses: actions/checkout@v6
118118

119119
- name: Log in to Container Registry
120120
uses: docker/login-action@v3
@@ -172,7 +172,7 @@ jobs:
172172
environment: development
173173

174174
steps:
175-
- uses: actions/checkout@v5
175+
- uses: actions/checkout@v6
176176
with:
177177
sparse-checkout: |
178178
.github

.github/workflows/codeql.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
contents: read
2828
steps:
2929
- name: Checkout repository
30-
uses: actions/checkout@v5
30+
uses: actions/checkout@v6
3131

3232
# Initializes the CodeQL tools for scanning.
3333
- name: Initialize CodeQL

.github/workflows/update-cloudflare-proxies.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
runs-on: ubuntu-latest
1414

1515
steps:
16-
- uses: actions/checkout@v5
16+
- uses: actions/checkout@v6
1717
with:
1818
ref: ${{ github.ref }}
1919

API/Realtime/RedisSubscriberService.cs

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,42 @@ public async Task StartAsync(CancellationToken cancellationToken)
5656

5757
private void HandleKeyExpired(RedisChannel _, RedisValue message)
5858
{
59-
if (!message.HasValue) return;
60-
if (message.ToString().Split(':', 2) is not [{ } guid, { } name]) return;
61-
62-
if (!Guid.TryParse(guid, out var id)) return;
63-
64-
if (typeof(DeviceOnline).FullName == name)
59+
if (!message.HasValue)
60+
{
61+
_logger.LogWarning("Received expired key with empty value for hub offline status");
62+
return;
63+
}
64+
65+
var messageString = (string?)message;
66+
if (messageString is null)
67+
{
68+
_logger.LogWarning("Received expired key that could not be converted to string for hub offline status. Raw value type: {ValueType}", message.GetType().FullName);
69+
return;
70+
}
71+
72+
var messageSpan = messageString.AsSpan();
73+
74+
// We always expect TypeName:GUID right now, if GUID is not present, something is really wrong
75+
var colonPos = messageSpan.IndexOf(':');
76+
if (colonPos < 0)
6577
{
78+
_logger.LogError("Received expired key with unexpected format (missing colon) for hub offline status. Value: {MessageValue}", messageString);
79+
return;
80+
}
81+
82+
// Data structure is TypeName:GUID
83+
var typeNameSpan = messageSpan[..colonPos];
84+
var guidSpan = messageSpan[(colonPos + 1)..];
85+
86+
// Check what type of expired key this is
87+
if (typeNameSpan.SequenceEqual(typeof(DeviceOnline).FullName))
88+
{
89+
if (!Guid.TryParse(guidSpan, out var id))
90+
{
91+
_logger.LogError("Received expired key with invalid GUID for hub offline status: {MessageValue}", messageString);
92+
return;
93+
}
94+
6695
OsTask.Run(() => LogicDeviceOnlineStatus(id));
6796
}
6897
}

Common/OpenShockServiceHelper.cs

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ public static IServiceCollection AddOpenShockMemDB(this IServiceCollection servi
3838
{
3939
// <---- Redis ---->
4040
services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect(options));
41-
services.AddSingleton<IRedisConnectionProvider, RedisConnectionProvider>(serviceProvider => new RedisConnectionProvider(serviceProvider.GetRequiredService<IConnectionMultiplexer>()));
41+
services.AddSingleton<IRedisConnectionProvider, RedisConnectionProvider>(serviceProvider =>
42+
new RedisConnectionProvider(serviceProvider.GetRequiredService<IConnectionMultiplexer>()));
4243
services.AddSingleton<IRedisPubService, RedisPubService>();
4344

4445
return services;
@@ -47,7 +48,8 @@ public static IServiceCollection AddOpenShockMemDB(this IServiceCollection servi
4748
public static IServiceCollection AddOpenShockDB(this IServiceCollection services, DatabaseOptions options)
4849
{
4950
// <---- Postgres EF Core ---->
50-
services.AddDbContextPool<OpenShockContext>(builder => OpenShockContext.ConfigureOptionsBuilder(builder, options.Conn, options.Debug));
51+
services.AddDbContextPool<OpenShockContext>(builder =>
52+
OpenShockContext.ConfigureOptionsBuilder(builder, options.Conn, options.Debug));
5153
services.AddPooledDbContextFactory<OpenShockContext>(builder =>
5254
{
5355
builder.UseNpgsql(options.Conn);
@@ -67,7 +69,8 @@ public static IServiceCollection AddOpenShockDB(this IServiceCollection services
6769
/// <param name="services"></param>
6870
/// <param name="configureOptions"></param>
6971
/// <returns></returns>
70-
private static AuthenticationBuilder AddOpenShockAuthentication(this IServiceCollection services, Action<AuthenticationOptions> configureOptions)
72+
private static AuthenticationBuilder AddOpenShockAuthentication(this IServiceCollection services,
73+
Action<AuthenticationOptions> configureOptions)
7174
{
7275
ArgumentNullException.ThrowIfNull(services);
7376
ArgumentNullException.ThrowIfNull(configureOptions);
@@ -78,9 +81,9 @@ private static AuthenticationBuilder AddOpenShockAuthentication(this IServiceCol
7881
services.TryAddSingleton(TimeProvider.System);
7982
// services.TryAddSingleton<ISystemClock, SystemClock>(); // Exists in original AddAuthentication method
8083
// services.TryAddSingleton<IAuthenticationConfigurationProvider, DefaultAuthenticationConfigurationProvider>(); // Exists in original AddAuthentication method
81-
84+
8285
var builder = new AuthenticationBuilder(services);
83-
86+
8487
services.Configure(configureOptions);
8588

8689
return builder;
@@ -91,8 +94,10 @@ private static AuthenticationBuilder AddOpenShockAuthentication(this IServiceCol
9194
/// </summary>
9295
/// <param name="services"></param>
9396
/// <param name="configureAuth"></param>
97+
/// <param name="configureMetrics"></param>
9498
/// <returns></returns>
95-
public static IServiceCollection AddOpenShockServices(this IServiceCollection services, Action<AuthenticationBuilder>? configureAuth = null)
99+
public static IServiceCollection AddOpenShockServices(this IServiceCollection services,
100+
Action<AuthenticationBuilder>? configureAuth = null, Action<MeterProviderBuilder>? configureMetrics = null)
96101
{
97102
// <---- ASP.NET ---->
98103
services.AddExceptionHandler<OpenShockExceptionHandler>();
@@ -116,7 +121,8 @@ public static IServiceCollection AddOpenShockServices(this IServiceCollection se
116121
opt.DefaultScheme = OpenShockAuthSchemes.UserSessionCookie;
117122
opt.DefaultAuthenticateScheme = OpenShockAuthSchemes.UserSessionCookie;
118123
})
119-
.AddScheme<AuthenticationSchemeOptions, UserSessionAuthentication>(OpenShockAuthSchemes.UserSessionCookie, _ => { })
124+
.AddScheme<AuthenticationSchemeOptions, UserSessionAuthentication>(OpenShockAuthSchemes.UserSessionCookie,
125+
_ => { })
120126
.AddScheme<AuthenticationSchemeOptions, ApiTokenAuthentication>(OpenShockAuthSchemes.ApiToken, _ => { })
121127
.AddScheme<AuthenticationSchemeOptions, HubAuthentication>(OpenShockAuthSchemes.HubToken, _ => { });
122128

@@ -127,12 +133,12 @@ public static IServiceCollection AddOpenShockServices(this IServiceCollection se
127133
options.AddPolicy(OpenShockAuthPolicies.RankAdmin, policy => policy.RequireRole("Admin", "System"));
128134
// TODO: Add token permission policies
129135
});
130-
136+
131137
services.AddSingleton<IAuthorizationMiddlewareResultHandler, OpenShockAuthorizationMiddlewareResultHandler>();
132-
138+
133139
services.ConfigureHttpJsonOptions(opt => JsonOptions.ConfigureDefault(opt.SerializerOptions));
134140
services.AddControllers().AddJsonOptions(opt => JsonOptions.ConfigureDefault(opt.JsonSerializerOptions));
135-
141+
136142
var apiVersioningBuilder = services.AddApiVersioning(options =>
137143
{
138144
options.DefaultApiVersion = new ApiVersion(1, 0);
@@ -146,13 +152,13 @@ public static IServiceCollection AddOpenShockServices(this IServiceCollection se
146152
setup.DefaultApiVersion = new ApiVersion(1, 0);
147153
setup.AssumeDefaultVersionWhenUnspecified = true;
148154
});
149-
155+
150156
// generic ASP.NET stuff
151157
services.AddMemoryCache();
152158
services.AddHttpContextAccessor();
153159
services.AddWebEncoders();
154160
services.AddProblemDetails();
155-
161+
156162
services.AddCors(options =>
157163
{
158164
options.AddDefaultPolicy(builder =>
@@ -164,7 +170,7 @@ public static IServiceCollection AddOpenShockServices(this IServiceCollection se
164170
builder.SetPreflightMaxAge(TimeSpan.FromHours(24));
165171
});
166172
});
167-
173+
168174
// This needs to be at this position, earlier will break validation error responses
169175
services.Configure<ApiBehaviorOptions>(options =>
170176
{
@@ -174,16 +180,21 @@ public static IServiceCollection AddOpenShockServices(this IServiceCollection se
174180
return problemDetails.ToObjectResult(context.HttpContext);
175181
};
176182
});
177-
183+
178184
// OpenTelemetry
179185

180186
services.AddOpenTelemetry()
181-
.WithMetrics(metrics => metrics
182-
.AddRuntimeInstrumentation()
183-
.AddAspNetCoreInstrumentation()
184-
.AddHttpClientInstrumentation()
185-
.AddPrometheusExporter());
186-
187+
.WithMetrics(metrics =>
188+
{
189+
metrics
190+
.AddRuntimeInstrumentation()
191+
.AddAspNetCoreInstrumentation()
192+
.AddHttpClientInstrumentation()
193+
.AddPrometheusExporter();
194+
195+
configureMetrics?.Invoke(metrics);
196+
});
197+
187198
// <---- OpenShock Services ---->
188199

189200
services.AddScoped<IConfigurationService, ConfigurationService>();
@@ -233,16 +244,17 @@ await context.HttpContext.Response.WriteAsync("Too Many Requests. Please try aga
233244
var ip = context.GetRemoteIP();
234245
if (IPAddress.IsLoopback(ip)) return RateLimitPartition.GetNoLimiter("ip-loopback-nolimit");
235246

236-
return RateLimitPartition.GetSlidingWindowLimiter($"ip-{ip}", _ => new SlidingWindowRateLimiterOptions
237-
{
238-
PermitLimit = 1000,
239-
Window = TimeSpan.FromMinutes(1),
240-
SegmentsPerWindow = 6,
241-
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
242-
QueueLimit = 100
243-
});
247+
return RateLimitPartition.GetSlidingWindowLimiter($"ip-{ip}", _ =>
248+
new SlidingWindowRateLimiterOptions
249+
{
250+
PermitLimit = 1000,
251+
Window = TimeSpan.FromMinutes(1),
252+
SegmentsPerWindow = 6,
253+
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
254+
QueueLimit = 100
255+
});
244256
}
245-
257+
246258
if (user.HasClaim(claim => claim is { Type: ClaimTypes.Role, Value: "Admin" or "System" }))
247259
return RateLimitPartition.GetNoLimiter("privileged-nolimit");
248260

@@ -290,7 +302,8 @@ await context.HttpContext.Response.WriteAsync("Too Many Requests. Please try aga
290302
return services;
291303
}
292304

293-
public static IServiceCollection AddOpenShockSignalR(this IServiceCollection services, ConfigurationOptions redisConfig)
305+
public static IServiceCollection AddOpenShockSignalR(this IServiceCollection services,
306+
ConfigurationOptions redisConfig)
294307
{
295308
services.AddSignalR()
296309
.AddOpenShockStackExchangeRedis(options => { options.Configuration = redisConfig; })
@@ -299,7 +312,7 @@ public static IServiceCollection AddOpenShockSignalR(this IServiceCollection ser
299312
options.PayloadSerializerOptions.PropertyNameCaseInsensitive = true;
300313
options.PayloadSerializerOptions.Converters.Add(new SemVersionJsonConverter());
301314
});
302-
315+
303316
return services;
304317
}
305318
}

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<PropertyGroup>
33
<Company>OpenShock</Company>
44
<Product>$(Company).$(MSBuildProjectName)</Product>
5-
<Version>3.15.1</Version>
5+
<Version>3.15.2</Version>
66

77
<Title>$(Product)</Title>
88
<Authors>OpenShock</Authors>

LiveControlGateway/LcgKeepAlive.cs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ public sealed class LcgKeepAlive : IHostedService
1515
private readonly LcgOptions _options;
1616
private readonly ILogger<LcgKeepAlive> _logger;
1717

18-
private const uint KeepAliveInterval = 35; // 35 seconds
18+
private uint _errorsInRow;
19+
20+
private static readonly TimeSpan KeepAliveKeyTTL = TimeSpan.FromSeconds(35); // 35 seconds
21+
private static readonly TimeSpan KeepAliveInterval = TimeSpan.FromSeconds(15); // 15 seconds
1922

2023
/// <summary>
2124
/// DI Constructor
@@ -45,7 +48,7 @@ await lcgNodes.InsertAsync(new LcgNode
4548
Country = _options.CountryCode,
4649
Load = 0,
4750
Environment = _env.EnvironmentName
48-
}, TimeSpan.FromSeconds(35));
51+
}, KeepAliveKeyTTL);
4952
return;
5053
}
5154

@@ -65,7 +68,7 @@ await lcgNodes.InsertAsync(new LcgNode
6568
}
6669

6770
await _redisConnectionProvider.Connection.ExecuteAsync("EXPIRE",
68-
$"{typeof(LcgNode).FullName}:{_options.Fqdn}", KeepAliveInterval);
71+
$"{typeof(LcgNode).FullName}:{_options.Fqdn}", (int)KeepAliveKeyTTL.TotalSeconds);
6972
}
7073

7174
private async Task Loop()
@@ -76,11 +79,21 @@ private async Task Loop()
7679
{
7780
_logger.LogDebug("Sending keep alive...");
7881
await SelfOnline();
79-
await Task.Delay(15_000);
82+
_logger.LogDebug("Sent keep alive!");
83+
_errorsInRow = 0;
84+
await Task.Delay(KeepAliveInterval);
8085
}
8186
catch (Exception e)
8287
{
83-
_logger.LogError(e, "Error in loop");
88+
++_errorsInRow;
89+
_logger.LogError(e, "Error sending gateway keep alive {Attempt}", _errorsInRow);
90+
if(_errorsInRow >= 10)
91+
{
92+
_logger.LogCritical("Too many errors in a row sending keep alive, terminating process");
93+
Environment.Exit(1001);
94+
}
95+
96+
await Task.Delay(KeepAliveInterval);
8497
}
8598
}
8699
// ReSharper disable once FunctionNeverReturns

0 commit comments

Comments
 (0)