Skip to content

Garnet evaluation#2009

Open
mSunberg wants to merge 1 commit into
mainfrom
Feature/garnet-redis-evaluation
Open

Garnet evaluation#2009
mSunberg wants to merge 1 commit into
mainfrom
Feature/garnet-redis-evaluation

Conversation

@mSunberg

@mSunberg mSunberg commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Description

Related Issue(s)

  • #{issue number}

Verification

  • Your code builds clean without any errors or warnings
  • Manual testing done (required)
  • Relevant automated test added (if you find this hard, leave it and we'll help out)
  • All tests run green
  • If pre- or post-deploy actions (including database migrations) are needed, add a description, include a "Pre/Post-deploy actions" section below, and mark the PR title with ⚠️

Documentation

  • User documentation is updated with a separate linked PR in altinn-studio-docs. (if applicable)

Summary by CodeRabbit

  • Chores
    • Simplified local development setup by eliminating the need for a separate Redis Docker container. The API now features an embedded, in-process cache server that provides complete Redis compatibility for development and testing environments, reducing infrastructure overhead and streamlining the setup process.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The standalone Redis container is removed from docker-compose.yml and replaced with an in-process Microsoft.Garnet server. A new EmbeddedGarnetServer class starts a GarnetServer singleton in the Development environment using the existing Redis connection string, with readiness probing. Program.cs conditionally invokes it before Redis cache setup.

Changes

Embedded Garnet cache server

Layer / File(s) Summary
EmbeddedGarnetServer implementation
src/Altinn.Correspondence.API/Altinn.Correspondence.API.csproj, src/Altinn.Correspondence.API/Caching/EmbeddedGarnetServer.cs
Adds the Microsoft.Garnet 1.1.10 package reference and introduces the EmbeddedGarnetServer static class: thread-safe singleton initialization, Redis connection string parsing, Garnet --bind argument construction (with localhost127.0.0.1 mapping), optional password auth, GarnetServer startup, bounded readiness probing via ConnectionMultiplexer.Connect + PING, and retryable exception classification.
Wiring and docker-compose cleanup
src/Altinn.Correspondence.API/Program.cs, docker-compose.yml
Program.cs adds the caching namespace import and a Development-only block calling EmbeddedGarnetServer.EnsureStarted before Redis cache and Data Protection configuration. docker-compose.yml removes the redis service and adds comments pointing to the embedded Garnet server.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is completely empty except for the template structure; no actual content describing the changes, rationale, or verification status has been provided. Fill in the Description section explaining what Garnet replaces and why, confirm verification checkboxes, and link any related issues.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Garnet evaluation' is vague and doesn't clearly describe the main change; it lacks specificity about what the implementation actually accomplishes. Use a more descriptive title such as 'Replace standalone Redis with embedded Garnet server for local development' to clearly convey the primary change.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch Feature/garnet-redis-evaluation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Altinn.Correspondence.API/Altinn.Correspondence.API.csproj`:
- Line 37: Update the PackageReference for Microsoft.Garnet to a version that
brings Snappier 1.3.1 or higher to address the high-severity Snappier
vulnerability (GHSA-pggp-6c3x-2xmx). Additionally, locate and update the
OpenTelemetry.Api PackageReference to version 1.15.3 or higher to resolve the
moderate-severity memory exhaustion vulnerability (GHSA-g94r-2vxg-569j) in
version 1.14.0. Both updates should be made in the project file to ensure all
transitive and direct dependencies are patched.

In `@src/Altinn.Correspondence.API/Caching/EmbeddedGarnetServer.cs`:
- Around line 42-43: The code does not validate that the EndPoints collection
from parsed ConfigurationOptions contains elements before calling
FirstOrDefault(), which returns null when the collection is empty and masks
misconfiguration. Add validation to check that options.EndPoints is not empty
after parsing the redisConnectionString, and throw or log an appropriate error
if no endpoints are found instead of allowing the null value to silently pass to
ResolveEndpoint. This ensures invalid or empty connection strings fail
explicitly rather than falling back to hardcoded defaults without warning.
- Line 82: The IsStartupProbeException method currently only checks for
RedisConnectionException, RedisTimeoutException, and RedisServerException, but
other transient exceptions like SocketException, IOException, and
TimeoutException can occur during startup when the TCP listener is not yet
ready. Expand the IsStartupProbeException method to also include checks for
these additional exception types so they are properly caught and retried by the
exception filter on line 82 instead of causing immediate failure.
- Line 28: Add parameter validation to the EnsureStarted method to ensure the
redisConnectionString parameter is neither null nor empty before it is used. At
the beginning of the method, before any other logic, check that
redisConnectionString meets these requirements and throw an appropriate
exception (such as ArgumentException or ArgumentNullException) if validation
fails, providing a meaningful error message to help with debugging.
- Around line 61-64: The GarnetServer instance created in the initialization
block is started with server.Start() but not assigned to the _server field until
after WaitUntilAcceptingConnections() is called. If the readiness probe throws
an exception, the server assignment never occurs, leaving the started server
instance unreachable and preventing proper cleanup or disposal. Fix this by
assigning _server = server before calling WaitUntilAcceptingConnections(), so
that any exception during the readiness probe will still allow the server to be
properly stopped and disposed through _server, rather than leaving it running
with the port bound.
- Around line 21-68: The EmbeddedGarnetServer class creates a GarnetServer
instance and stores it in the static _server field, but never disposes it when
the application shuts down, causing resource leaks. Create an IHostedService
implementation that calls _server?.Dispose() in its StopAsync method to ensure
proper cleanup of network handlers and background tasks when the application
stops. Register this service in the dependency injection container so it gets
invoked during application shutdown.

In `@src/Altinn.Correspondence.API/Program.cs`:
- Around line 134-139: The `generalSettings` object may be null if the
configuration section binding fails, which will cause a NullReferenceException
when accessing `generalSettings.RedisConnectionString` in the
`EmbeddedGarnetServer.EnsureStarted()` call. Add a null check for
`generalSettings` before the IsDevelopment() block (or within it) to validate
that the configuration was bound successfully. If it is null, log an error using
`bootstrapLogger` and either throw an exception or skip the Garnet
initialization with an appropriate warning message to ensure the application
startup fails gracefully with a clear error message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0ed1595e-eded-4f55-944e-4d65810dcb12

📥 Commits

Reviewing files that changed from the base of the PR and between 3f272a2 and 3fb1b96.

📒 Files selected for processing (4)
  • docker-compose.yml
  • src/Altinn.Correspondence.API/Altinn.Correspondence.API.csproj
  • src/Altinn.Correspondence.API/Caching/EmbeddedGarnetServer.cs
  • src/Altinn.Correspondence.API/Program.cs

</PackageReference>
<PackageReference Include="StackExchange.Redis" Version="2.9.32" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.0" />
<PackageReference Include="Microsoft.Garnet" Version="1.1.10" />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Identify Snappier transitive dependency and check for Garnet updates.

# Check which package references Snappier
dotnet list src/Altinn.Correspondence.API/Altinn.Correspondence.API.csproj package --include-transitive | grep -i snappier -B 5

# Check latest Microsoft.Garnet version
echo "Checking latest Microsoft.Garnet version..."
dotnet list src/Altinn.Correspondence.API/Altinn.Correspondence.API.csproj package --outdated | grep -i garnet

Repository: Altinn/altinn-correspondence

Length of output: 224


🌐 Web query:

Microsoft.Garnet latest version and Snappier dependency version

💡 Result:

As of June 15, 2026, the latest stable version of Microsoft Garnet is v1.1.10, released on May 28, 2026 [1]. There is also a newer preview version, v2.0.1-beta.3, which was released on June 9, 2026 [2]. Regarding the Snappier dependency, while Garnet's centralized dependency file (Directory.Packages.props) does not list Snappier directly in its public repository main branch [3], Snappier has been identified as a dependency often used within the.NET ecosystem [4][5]. The Snappier package version 1.3.1 was released to address a high-severity vulnerability (GHSA-pggp-6c3x-2xmx) that affected all versions up to and including 1.3.0 [4][5]. If your Garnet project relies on Snappier, it is recommended to ensure you are using version 1.3.1 or higher to mitigate this security issue [4][6][5].

Citations:


🌐 Web query:

GHSA-pggp-6c3x-2xmx Snappier vulnerability details and patched version

💡 Result:

The vulnerability GHSA-pggp-6c3x-2xmx (CVE-2026-44302) in the Snappier library involves an uncatchable infinite loop that occurs when the SnappyStream component attempts to decompress malformed framed-format Snappy input as small as 15 bytes [1][2][3]. This loop consumes CPU resources, causing a denial-of-service condition that cannot be handled via standard try/catch blocks because no exception is thrown [1][3]. The issue affects all versions of Snappier up to and including version 1.3.0 [1][4]. It has been patched in version 1.3.1 [5][3][4]. Users are advised to upgrade to version 1.3.1 or later to resolve this vulnerability [3].

Citations:


🌐 Web query:

GHSA-g94r-2vxg-569j OpenTelemetry.Api vulnerability details and patched version

💡 Result:

The vulnerability GHSA-g94r-2vxg-569j (CVE-2026-40894) is a memory exhaustion issue (CWE-789) affecting the OpenTelemetry.NET ecosystem [1][2]. Details: The vulnerability exists in the baggage, B3, and Jaeger propagation header processing logic within the OpenTelemetry.Api and OpenTelemetry.Extensions.Propagators NuGet packages [1][3]. Specifically, these libraries were found to eagerly allocate intermediate arrays when parsing propagation headers, particularly when dealing with degenerate or malformed inputs containing large numbers of delimiter characters [1]. An attacker can exploit this by sending specially crafted, excessively large propagation headers, leading to excessive memory allocation and a potential denial of service (DoS) in the consuming application [1][4][3]. Affected Versions: - OpenTelemetry.Api: versions 0.5.0-beta.2 through 1.15.2 [1][2] - OpenTelemetry.Extensions.Propagators: versions 1.3.1 through 1.15.2 [2][3] Patched Version: The issue was addressed in version 1.15.3 [1][2][3]. Users are advised to upgrade to version 1.15.3 or higher [4][3]. Workarounds: If an immediate upgrade is not possible, the vulnerability can be mitigated by configuring appropriate HTTP request header size limits at the infrastructure level (e.g., in IIS or Nginx) or by disabling baggage and/or trace propagation [1][4].

Citations:


Update package versions to address high-severity Snappier and moderate-severity OpenTelemetry.Api vulnerabilities.

Microsoft.Garnet 1.1.10 likely brings Snappier as a transitive dependency. The Snappier vulnerability (GHSA-pggp-6c3x-2xmx) affects versions up to 1.3.0 and is patched in 1.3.1. Additionally, OpenTelemetry.Api 1.14.0 has a memory exhaustion vulnerability (GHSA-g94r-2vxg-569j) that should be upgraded to 1.15.3 or higher.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Altinn.Correspondence.API/Altinn.Correspondence.API.csproj` at line 37,
Update the PackageReference for Microsoft.Garnet to a version that brings
Snappier 1.3.1 or higher to address the high-severity Snappier vulnerability
(GHSA-pggp-6c3x-2xmx). Additionally, locate and update the OpenTelemetry.Api
PackageReference to version 1.15.3 or higher to resolve the moderate-severity
memory exhaustion vulnerability (GHSA-g94r-2vxg-569j) in version 1.14.0. Both
updates should be made in the project file to ensure all transitive and direct
dependencies are patched.

Comment on lines +21 to +68
internal static class EmbeddedGarnetServer
{
private static readonly object _lock = new();
private static readonly TimeSpan StartupProbeDelay = TimeSpan.FromMilliseconds(100);
private const int MaxStartupProbeAttempts = 50;
private static GarnetServer? _server;

public static void EnsureStarted(string redisConnectionString, ILogger logger)
{
if (_server is not null)
{
return;
}

lock (_lock)
{
if (_server is not null)
{
return;
}

var options = ConfigurationOptions.Parse(redisConnectionString);
var (host, port) = ResolveEndpoint(options.EndPoints.FirstOrDefault());

var serverArgs = new List<string>
{
"--bind", host,
"--port", port.ToString(),
};

if (!string.IsNullOrEmpty(options.Password))
{
serverArgs.Add("--auth");
serverArgs.Add("Password");
serverArgs.Add("--password");
serverArgs.Add(options.Password);
}

logger.LogInformation("Starting embedded Garnet server on {Host}:{Port}", host, port);

var server = new GarnetServer(serverArgs.ToArray());
server.Start();
WaitUntilAcceptingConnections(redisConnectionString);
_server = server;

logger.LogInformation("Embedded Garnet server started");
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if GarnetServer implements IDisposable and requires cleanup.

# Search for GarnetServer class definition and IDisposable implementation
ast-grep --pattern $'class GarnetServer : $$$, IDisposable, $$$'

# Also check for Dispose method
ast-grep --pattern $'class GarnetServer {
  $$$
  Dispose($$$) {
    $$$
  }
  $$$
}'

Repository: Altinn/altinn-correspondence

Length of output: 54


🏁 Script executed:

# Find imports and usages of GarnetServer
rg "GarnetServer" --type cs -B 2 -A 2

Repository: Altinn/altinn-correspondence

Length of output: 2861


🏁 Script executed:

# Check for project file to see Garnet package dependency
fd -e ".*proj" -type f | xargs grep -l "Garnet" 2>/dev/null

Repository: Altinn/altinn-correspondence

Length of output: 245


🏁 Script executed:

# Search for any existing disposal/shutdown patterns in the codebase
rg "IDisposable|Dispose\(\)|IHostedService" --type cs -l

Repository: Altinn/altinn-correspondence

Length of output: 574


🏁 Script executed:

# Find .csproj files and search for Garnet package
find . -name "*.csproj" -type f | xargs grep -l "Garnet" 2>/dev/null

Repository: Altinn/altinn-correspondence

Length of output: 137


🏁 Script executed:

# Also check .sln and other config files
find . -maxdepth 3 -type f \( -name "*.csproj" -o -name "nuget.config" -o -name "packages.config" \) | head -20

Repository: Altinn/altinn-correspondence

Length of output: 704


🏁 Script executed:

# Check the specific project file for Garnet reference
cat src/Altinn.Correspondence.API/Altinn.Correspondence.API.csproj 2>/dev/null | grep -A 5 -B 5 "Garnet"

Repository: Altinn/altinn-correspondence

Length of output: 697


🌐 Web query:

GarnetServer IDisposable cleanup requirements documentation

💡 Result:

To properly clean up a GarnetServer instance, you should follow standard.NET IDisposable patterns while leveraging the server's specific graceful shutdown capabilities. Basic Disposal As GarnetServer implements IDisposable, you can manage its lifecycle using the using statement or by manually calling Dispose [1][2]. Disposing the server instance ensures that internal resources, such as network handlers, storage wrappers, and background tasks, are properly released [3][4][5]. Graceful Shutdown For a more controlled shutdown—particularly in production environments like Kubernetes—it is recommended to use the GarnetServer.ShutdownAsync method [6]. This orchestration method is designed to: 1. Stop accepting new connections (via StopListening) [6]. 2. Allow active connections to drain within a specified timeout [6]. 3. Commit final state, including AOF (Append-Only File) logging and checkpoints [6]. Implementation Example The standard pattern for hosting GarnetServer involves initializing the server and ensuring cleanup occurs upon termination signals (such as SIGINT or SIGTERM) [6][1][7]. // Conceptual example of graceful shutdown integration using var server = new GarnetServer(args); server.Start; // In a real-world scenario (e.g., service or console app), // trigger this on shutdown signals: await server.ShutdownAsync; server.Dispose; Key Considerations - Resource Cleanup: Internally, disposing of GarnetServer triggers the disposal of components like StoreWrapper, cluster providers, and task managers [3]. - Socket Handling: Recent improvements ensure that server disposal and network handler cleanup are handled more effectively, moving away from forced TCP resets toward graceful FIN-based connection termination to avoid client-side issues like SYN flooding [8][4]. - Background Tasks: Using ShutdownAsync helps prevent issues where background tasks might otherwise be abruptly cancelled, ensuring data integrity during persistence operations [6].

Citations:


🏁 Script executed:

# Read Program.cs to understand the app initialization pattern
cat src/Altinn.Correspondence.API/Program.cs

Repository: Altinn/altinn-correspondence

Length of output: 9080


🏁 Script executed:

# Search for IHostedService implementations in the codebase
rg "IHostedService|BackgroundService" --type cs

Repository: Altinn/altinn-correspondence

Length of output: 685


Add IHostedService to dispose of GarnetServer on app shutdown.

GarnetServer implements IDisposable and requires cleanup of network handlers, storage wrappers, and background tasks on shutdown. The current code creates the singleton but never disposes it, which prevents graceful cleanup in test scenarios and graceful application shutdown.

Create an IHostedService (similar to the existing EdDsaSecurityKeysCacheService pattern) to call _server?.Dispose() when the application stops, or register a shutdown callback with the host.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Altinn.Correspondence.API/Caching/EmbeddedGarnetServer.cs` around lines
21 - 68, The EmbeddedGarnetServer class creates a GarnetServer instance and
stores it in the static _server field, but never disposes it when the
application shuts down, causing resource leaks. Create an IHostedService
implementation that calls _server?.Dispose() in its StopAsync method to ensure
proper cleanup of network handlers and background tasks when the application
stops. Register this service in the dependency injection container so it gets
invoked during application shutdown.

private const int MaxStartupProbeAttempts = 50;
private static GarnetServer? _server;

public static void EnsureStarted(string redisConnectionString, ILogger logger)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate redisConnectionString parameter.

The method does not validate that redisConnectionString is non-null and non-empty before passing it to ConfigurationOptions.Parse. A null or empty string could cause ArgumentException or unexpected parse results.

🛡️ Proposed fix to add validation
 public static void EnsureStarted(string redisConnectionString, ILogger logger)
 {
+    if (string.IsNullOrWhiteSpace(redisConnectionString))
+    {
+        throw new ArgumentException("Redis connection string cannot be null or empty.", nameof(redisConnectionString));
+    }
+
     if (_server is not null)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Altinn.Correspondence.API/Caching/EmbeddedGarnetServer.cs` at line 28,
Add parameter validation to the EnsureStarted method to ensure the
redisConnectionString parameter is neither null nor empty before it is used. At
the beginning of the method, before any other logic, check that
redisConnectionString meets these requirements and throw an appropriate
exception (such as ArgumentException or ArgumentNullException) if validation
fails, providing a meaningful error message to help with debugging.

Comment on lines +42 to +43
var options = ConfigurationOptions.Parse(redisConnectionString);
var (host, port) = ResolveEndpoint(options.EndPoints.FirstOrDefault());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Null-reference hazard: EndPoints collection may be empty.

options.EndPoints.FirstOrDefault() returns null if the collection is empty. The downstream ResolveEndpoint has a fallback, but this silently masks misconfiguration. If the connection string is invalid or contains no endpoints, the server binds to a hardcoded default (127.0.0.1:6379) without warning.

🛡️ Proposed fix to validate endpoints
     var options = ConfigurationOptions.Parse(redisConnectionString);
-    var (host, port) = ResolveEndpoint(options.EndPoints.FirstOrDefault());
+    var endpoint = options.EndPoints.FirstOrDefault();
+    if (endpoint is null)
+    {
+        throw new InvalidOperationException($"Redis connection string '{redisConnectionString}' contains no endpoints.");
+    }
+    var (host, port) = ResolveEndpoint(endpoint);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var options = ConfigurationOptions.Parse(redisConnectionString);
var (host, port) = ResolveEndpoint(options.EndPoints.FirstOrDefault());
var options = ConfigurationOptions.Parse(redisConnectionString);
var endpoint = options.EndPoints.FirstOrDefault();
if (endpoint is null)
{
throw new InvalidOperationException($"Redis connection string '{redisConnectionString}' contains no endpoints.");
}
var (host, port) = ResolveEndpoint(endpoint);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Altinn.Correspondence.API/Caching/EmbeddedGarnetServer.cs` around lines
42 - 43, The code does not validate that the EndPoints collection from parsed
ConfigurationOptions contains elements before calling FirstOrDefault(), which
returns null when the collection is empty and masks misconfiguration. Add
validation to check that options.EndPoints is not empty after parsing the
redisConnectionString, and throw or log an appropriate error if no endpoints are
found instead of allowing the null value to silently pass to ResolveEndpoint.
This ensures invalid or empty connection strings fail explicitly rather than
falling back to hardcoded defaults without warning.

Comment on lines +61 to +64
var server = new GarnetServer(serverArgs.ToArray());
server.Start();
WaitUntilAcceptingConnections(redisConnectionString);
_server = server;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Resource leak: GarnetServer started but not assigned if readiness probe fails.

server.Start() is called on line 62, but _server = server assignment happens on line 64, after WaitUntilAcceptingConnections. If the readiness probe throws, the lock is released but the started server instance is lost, with no way to stop or dispose it. This leaves the server running and the TCP port bound.

🔒 Proposed fix to assign before probing or wrap in try-catch

Option 1: Assign _server before probing so cleanup is possible on failure:

     var server = new GarnetServer(serverArgs.ToArray());
     server.Start();
+    _server = server;
     WaitUntilAcceptingConnections(redisConnectionString);
-    _server = server;

Option 2: Wrap in try-catch and dispose server on probe failure:

     var server = new GarnetServer(serverArgs.ToArray());
-    server.Start();
-    WaitUntilAcceptingConnections(redisConnectionString);
-    _server = server;
+    try
+    {
+        server.Start();
+        WaitUntilAcceptingConnections(redisConnectionString);
+        _server = server;
+    }
+    catch
+    {
+        server.Dispose();
+        throw;
+    }

Note: Verify that GarnetServer implements IDisposable for option 2.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var server = new GarnetServer(serverArgs.ToArray());
server.Start();
WaitUntilAcceptingConnections(redisConnectionString);
_server = server;
var server = new GarnetServer(serverArgs.ToArray());
server.Start();
_server = server;
WaitUntilAcceptingConnections(redisConnectionString);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Altinn.Correspondence.API/Caching/EmbeddedGarnetServer.cs` around lines
61 - 64, The GarnetServer instance created in the initialization block is
started with server.Start() but not assigned to the _server field until after
WaitUntilAcceptingConnections() is called. If the readiness probe throws an
exception, the server assignment never occurs, leaving the started server
instance unreachable and preventing proper cleanup or disposal. Fix this by
assigning _server = server before calling WaitUntilAcceptingConnections(), so
that any exception during the readiness probe will still allow the server to be
properly stopped and disposed through _server, rather than leaving it running
with the port bound.

connection.GetDatabase().Ping();
return;
}
catch (Exception ex) when (IsStartupProbeException(ex))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Startup probe exception filter may be incomplete.

The filter on line 82 only retries RedisConnectionException, RedisTimeoutException, and RedisServerException. Other transient exceptions like SocketException, IOException, or TimeoutException will cause immediate failure without retries. This could lead to false negatives if the server is starting but the TCP listener is not yet ready.

🛡️ Proposed fix to widen exception filter
 private static bool IsStartupProbeException(Exception exception)
 {
-    return exception is RedisConnectionException or RedisTimeoutException or RedisServerException;
+    return exception is RedisConnectionException 
+        or RedisTimeoutException 
+        or RedisServerException 
+        or SocketException 
+        or IOException 
+        or TimeoutException;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Altinn.Correspondence.API/Caching/EmbeddedGarnetServer.cs` at line 82,
The IsStartupProbeException method currently only checks for
RedisConnectionException, RedisTimeoutException, and RedisServerException, but
other transient exceptions like SocketException, IOException, and
TimeoutException can occur during startup when the TCP listener is not yet
ready. Expand the IsStartupProbeException method to also include checks for
these additional exception types so they are properly caught and retried by the
exception filter on line 82 instead of causing immediate failure.

Comment on lines +134 to +139
if (hostEnvironment.IsDevelopment())
{
// Run Garnet in-process for local development and tests instead of a Docker Redis container.
// The StackExchange.Redis clients below connect to it via RedisConnectionString unchanged.
EmbeddedGarnetServer.EnsureStarted(generalSettings.RedisConnectionString, bootstrapLogger);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Null-reference hazard: generalSettings may be null.

Line 133 calls config.GetSection(nameof(GeneralSettings)).Bind(generalSettings), but if the configuration section is missing or binding fails, generalSettings.RedisConnectionString on line 138 will throw NullReferenceException. The app startup will crash with an unclear error.

🛡️ Proposed fix to validate configuration
     var generalSettings = new GeneralSettings();
     config.GetSection(nameof(GeneralSettings)).Bind(generalSettings);
+    if (string.IsNullOrWhiteSpace(generalSettings.RedisConnectionString))
+    {
+        throw new InvalidOperationException("GeneralSettings:RedisConnectionString is required but was not found in configuration.");
+    }
     if (hostEnvironment.IsDevelopment())
     {
         // Run Garnet in-process for local development and tests instead of a Docker Redis container.
         // The StackExchange.Redis clients below connect to it via RedisConnectionString unchanged.
         EmbeddedGarnetServer.EnsureStarted(generalSettings.RedisConnectionString, bootstrapLogger);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Altinn.Correspondence.API/Program.cs` around lines 134 - 139, The
`generalSettings` object may be null if the configuration section binding fails,
which will cause a NullReferenceException when accessing
`generalSettings.RedisConnectionString` in the
`EmbeddedGarnetServer.EnsureStarted()` call. Add a null check for
`generalSettings` before the IsDevelopment() block (or within it) to validate
that the configuration was bound successfully. If it is null, log an error using
`bootstrapLogger` and either throw an exception or skip the Garnet
initialization with an appropriate warning message to ensure the application
startup fails gracefully with a clear error message.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant