Add Redis support for SignalR backplane and caching
- Introduced Redis configuration in appsettings.json to enable SignalR backplane functionality. - Updated Program.cs to conditionally configure SignalR with Redis if a connection string is provided. - Added Redis connection service registration in ApiBootstrap for distributed scenarios. - Included necessary package references for StackExchange.Redis and Microsoft.Extensions.Caching.StackExchangeRedis in project files. - Implemented password masking for Redis connection strings to enhance security.
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
||||
<PackageReference Include="Essential.LoggerProvider.Elasticsearch" Version="1.3.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.Orleans.Core" Version="9.2.1" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
|
||||
<PackageReference Include="NSwag.AspNetCore" Version="14.0.7" />
|
||||
|
||||
@@ -461,7 +461,32 @@ builder.Services.AddCors(options =>
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddSignalR().AddJsonProtocol();
|
||||
// Configure SignalR with Redis backplane if available
|
||||
var signalRBuilder = builder.Services.AddSignalR().AddJsonProtocol();
|
||||
|
||||
// Check if Redis is configured for SignalR backplane
|
||||
// Priority: ConnectionStrings:Redis (can be set via ConnectionStrings__Redis env var) > REDIS_URL env var
|
||||
var redisConnectionString = builder.Configuration.GetConnectionString("Redis")
|
||||
?? builder.Configuration["REDIS_URL"];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(redisConnectionString))
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"✅ Configuring SignalR with Redis backplane");
|
||||
signalRBuilder.AddStackExchangeRedis(redisConnectionString, options =>
|
||||
{
|
||||
// Configure channel prefix for SignalR messages
|
||||
options.Configuration.ChannelPrefix = "managing-signalr";
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"⚠️ Failed to configure SignalR Redis backplane: {ex.Message}");
|
||||
Console.WriteLine("SignalR will work in single-instance mode only");
|
||||
}
|
||||
}
|
||||
|
||||
builder.Services.AddScoped<IJwtUtils, JwtUtils>();
|
||||
|
||||
builder.Services.RegisterApiDependencies(builder.Configuration);
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
"DebitEndpoint": "/api/credits/debit",
|
||||
"RefundEndpoint": "/api/credits/refund"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"Redis": ""
|
||||
},
|
||||
"Flagsmith": {
|
||||
"ApiUrl": "https://flag.kaigen.ai/api/v1/"
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="9.2.1"/>
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.16"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace Managing.Application.Abstractions.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing Redis connections in a generic way across the application.
|
||||
/// This service provides access to the underlying Redis connection multiplexer
|
||||
/// which can be used for various purposes including SignalR backplane, caching, etc.
|
||||
/// </summary>
|
||||
public interface IRedisConnectionService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the Redis connection multiplexer instance.
|
||||
/// Returns null if Redis is not configured or connection failed.
|
||||
/// </summary>
|
||||
IConnectionMultiplexer? GetConnection();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Redis database instance.
|
||||
/// </summary>
|
||||
/// <param name="db">Database number (default: -1 for default database)</param>
|
||||
/// <returns>IDatabase instance or null if not connected</returns>
|
||||
IDatabase? GetDatabase(int db = -1);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if Redis is connected and available.
|
||||
/// </summary>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Redis connection string being used.
|
||||
/// </summary>
|
||||
string? ConnectionString { get; }
|
||||
}
|
||||
|
||||
@@ -510,9 +510,56 @@ public static class ApiBootstrap
|
||||
services.AddTransient<ICacheService, CacheService>();
|
||||
services.AddSingleton<ITaskCache, TaskCache>();
|
||||
|
||||
// Redis (for SignalR backplane and other distributed scenarios)
|
||||
services.AddRedis(configuration);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddRedis(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// Check if Redis is configured
|
||||
// Priority: ConnectionStrings:Redis (can be set via ConnectionStrings__Redis env var) > REDIS_URL env var
|
||||
var redisConnectionString = configuration.GetConnectionString("Redis")
|
||||
?? configuration["REDIS_URL"];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(redisConnectionString))
|
||||
{
|
||||
Console.WriteLine($"✅ Redis configured: {MaskRedisPassword(redisConnectionString)}");
|
||||
|
||||
// Register generic Redis connection service for various use cases
|
||||
// (SignalR backplane, distributed caching, pub/sub, etc.)
|
||||
services.AddSingleton<IRedisConnectionService, RedisConnectionService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("ℹ️ Redis not configured - running in single-instance mode");
|
||||
|
||||
// Register a no-op Redis service that returns null connections
|
||||
services.AddSingleton<IRedisConnectionService>(sp =>
|
||||
new RedisConnectionService(configuration, sp.GetRequiredService<ILogger<RedisConnectionService>>()));
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static string MaskRedisPassword(string connectionString)
|
||||
{
|
||||
if (connectionString.Contains("password=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var parts = connectionString.Split(',');
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
if (parts[i].Trim().StartsWith("password=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
parts[i] = "password=***";
|
||||
}
|
||||
}
|
||||
return string.Join(",", parts);
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddWorkers(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
if (configuration.GetValue<bool>("WorkerNotifyBundleBacktest", false))
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.10"/>
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.16"/>
|
||||
<PackageReference Include="System.Runtime.Caching" Version="8.0.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
329
src/Managing.Infrastructure.Storage/README-REDIS.md
Normal file
329
src/Managing.Infrastructure.Storage/README-REDIS.md
Normal file
@@ -0,0 +1,329 @@
|
||||
# Redis Integration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The Managing platform now includes a generic Redis integration that supports multiple use cases:
|
||||
|
||||
1. **SignalR Backplane** - Enables multi-instance SignalR hubs (BotHub, BacktestHub, LlmHub)
|
||||
2. **Distributed Caching** - Can be extended for distributed caching scenarios
|
||||
3. **Other Redis Use Cases** - Generic connection service available for any Redis operations
|
||||
|
||||
## Architecture
|
||||
|
||||
### Components
|
||||
|
||||
- **`IRedisConnectionService`** - Interface for Redis connectivity (in `Managing.Application.Abstractions`)
|
||||
- **`RedisConnectionService`** - Implementation managing Redis connections (in `Managing.Infrastructure.Storage`)
|
||||
- **SignalR Backplane** - Automatically configured when Redis is available (in `Managing.Api/Program.cs`)
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **Generic and Reusable** - The Redis service is in the Infrastructure layer and can be used by any part of the application
|
||||
2. **Graceful Degradation** - If Redis is not configured, the application runs normally in single-instance mode
|
||||
3. **Automatic Configuration** - SignalR automatically uses Redis backplane when available
|
||||
4. **Connection Management** - Single connection multiplexer shared across the application
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Redis can be configured using any of these methods (in order of precedence):
|
||||
|
||||
1. **ConnectionStrings:Redis** (recommended) - Set via `ConnectionStrings__Redis` environment variable
|
||||
2. **REDIS_URL** (fallback environment variable)
|
||||
|
||||
### Configuration Examples
|
||||
|
||||
#### appsettings.json
|
||||
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Redis": "srv-captain--redis:6379"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### With Password
|
||||
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Redis": "srv-captain--redis:6379,password=your-password-here"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Environment Variable (Recommended for CapRover/Docker)
|
||||
|
||||
**.NET Standard Format:**
|
||||
```bash
|
||||
export ConnectionStrings__Redis="srv-captain--redis:6379"
|
||||
# or with password
|
||||
export ConnectionStrings__Redis="srv-captain--redis:6379,password=your-password-here"
|
||||
```
|
||||
|
||||
**Fallback Format:**
|
||||
```bash
|
||||
export REDIS_URL="srv-captain--redis:6379"
|
||||
# or with password
|
||||
export REDIS_URL="srv-captain--redis:6379,password=your-password-here"
|
||||
```
|
||||
|
||||
### Current Configuration
|
||||
|
||||
- **Sandbox** (`appsettings.Sandbox.json`): `srv-captain--redis:6379`
|
||||
- **Production** (`appsettings.Production.json`): `srv-captain--redis:6379`
|
||||
- **Local Development** (`appsettings.json`): Not configured (single-instance mode)
|
||||
|
||||
## Deployment
|
||||
|
||||
### CapRover Requirements
|
||||
|
||||
For multi-instance SignalR to work properly on CapRover:
|
||||
|
||||
1. **Enable WebSocket Support**
|
||||
- Go to App Settings
|
||||
- Enable "WebSocket Support"
|
||||
- Save and redeploy
|
||||
|
||||
2. **Enable Sticky Sessions**
|
||||
- Go to HTTP Settings
|
||||
- Enable "Sticky Sessions"
|
||||
- Save
|
||||
|
||||
3. **Setup Redis**
|
||||
- Create a Redis app in CapRover (or use external Redis)
|
||||
- Note the service name (e.g., `srv-captain--redis`)
|
||||
- Configure connection string in appsettings
|
||||
|
||||
4. **Set Instance Count**
|
||||
- With Redis and sticky sessions, you can safely scale to multiple instances
|
||||
- Recommended: 2-3 instances for high availability
|
||||
|
||||
### Docker/Docker Compose
|
||||
|
||||
If using Docker Compose, add a Redis service:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
```
|
||||
|
||||
Then set the connection string to `redis:6379`.
|
||||
|
||||
## SignalR Backplane
|
||||
|
||||
### How It Works
|
||||
|
||||
When Redis is configured:
|
||||
|
||||
1. All SignalR connections register in Redis
|
||||
2. Messages sent to a hub are distributed via Redis pub/sub
|
||||
3. Each instance receives messages for its connected clients
|
||||
4. Connection IDs are shared across instances
|
||||
|
||||
### Benefits
|
||||
|
||||
- **Scale Out**: Run multiple API instances
|
||||
- **High Availability**: If one instance dies, others continue working
|
||||
- **Load Balancing**: Distribute WebSocket connections across instances
|
||||
- **No "Connection ID Not Found" Errors**: All instances know about all connections
|
||||
|
||||
### Verification
|
||||
|
||||
Check the application logs on startup:
|
||||
|
||||
```
|
||||
✅ Configuring SignalR with Redis backplane: srv-captain--redis:6379
|
||||
✅ Redis connection established successfully
|
||||
```
|
||||
|
||||
If Redis is not configured:
|
||||
|
||||
```
|
||||
ℹ️ Redis not configured - SignalR running in single-instance mode
|
||||
```
|
||||
|
||||
If Redis connection fails:
|
||||
|
||||
```
|
||||
⚠️ Failed to configure SignalR Redis backplane: <error message>
|
||||
SignalR will work in single-instance mode only
|
||||
```
|
||||
|
||||
## Using Redis in Your Code
|
||||
|
||||
### Accessing Redis Connection
|
||||
|
||||
```csharp
|
||||
public class MyService
|
||||
{
|
||||
private readonly IRedisConnectionService _redisService;
|
||||
|
||||
public MyService(IRedisConnectionService redisService)
|
||||
{
|
||||
_redisService = redisService;
|
||||
}
|
||||
|
||||
public async Task<string?> GetValueAsync(string key)
|
||||
{
|
||||
if (!_redisService.IsConnected)
|
||||
{
|
||||
// Redis not available, handle gracefully
|
||||
return null;
|
||||
}
|
||||
|
||||
var db = _redisService.GetDatabase();
|
||||
return await db.StringGetAsync(key);
|
||||
}
|
||||
|
||||
public async Task SetValueAsync(string key, string value, TimeSpan? expiry = null)
|
||||
{
|
||||
if (!_redisService.IsConnected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var db = _redisService.GetDatabase();
|
||||
await db.StringSetAsync(key, value, expiry);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
```csharp
|
||||
// Get the connection multiplexer for advanced scenarios
|
||||
var connection = _redisService.GetConnection();
|
||||
if (connection != null)
|
||||
{
|
||||
// Pub/Sub
|
||||
var subscriber = connection.GetSubscriber();
|
||||
await subscriber.SubscribeAsync("my-channel", (channel, message) =>
|
||||
{
|
||||
Console.WriteLine($"Received: {message}");
|
||||
});
|
||||
|
||||
// Multiple databases
|
||||
var db0 = _redisService.GetDatabase(0);
|
||||
var db1 = _redisService.GetDatabase(1);
|
||||
|
||||
// Server operations
|
||||
var server = connection.GetServer(connection.GetEndPoints().First());
|
||||
var keys = server.Keys();
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health Checks
|
||||
|
||||
The Redis connection service automatically:
|
||||
|
||||
- Reconnects on connection loss
|
||||
- Logs connection events
|
||||
- Provides connection status via `IsConnected` property
|
||||
|
||||
### Logging
|
||||
|
||||
Key log messages to watch:
|
||||
|
||||
- `✅ Redis connection established successfully` - Initial connection succeeded
|
||||
- `✅ Redis connection restored` - Reconnected after failure
|
||||
- `❌ Redis connection failed: <error>` - Connection problem
|
||||
- `⚠️ Redis connection lost, attempting to reconnect...` - Transient failure
|
||||
|
||||
### CapRover Monitoring
|
||||
|
||||
In CapRover App Logs, search for:
|
||||
|
||||
- "Redis" - All Redis-related messages
|
||||
- "SignalR" - SignalR configuration
|
||||
- "No Connection with that ID" - SignalR connection issues (should not appear with Redis)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### SignalR "No Connection with that ID" Error
|
||||
|
||||
**Symptoms**: Frontend gets 404 with "No Connection with that ID" after negotiation
|
||||
|
||||
**Causes**:
|
||||
1. Multiple instances without Redis backplane
|
||||
2. Sticky sessions not enabled
|
||||
3. Redis not configured correctly
|
||||
|
||||
**Solutions**:
|
||||
1. Enable Redis (see Configuration section)
|
||||
2. Enable sticky sessions in CapRover
|
||||
3. Verify Redis connection in logs
|
||||
4. Temporarily set instances to 1 to test
|
||||
|
||||
### Redis Connection Failures
|
||||
|
||||
**Check**:
|
||||
1. Redis service is running in CapRover
|
||||
2. Connection string is correct (service name, port)
|
||||
3. No firewall blocking the connection
|
||||
4. Redis service is healthy (check its logs)
|
||||
|
||||
**Test Connection**:
|
||||
```bash
|
||||
# In CapRover terminal or SSH
|
||||
redis-cli -h srv-captain--redis ping
|
||||
# Should return: PONG
|
||||
```
|
||||
|
||||
### Single Instance Still Shows Errors
|
||||
|
||||
If running single instance and getting SignalR errors:
|
||||
|
||||
1. Check WebSocket support is enabled in CapRover
|
||||
2. Verify no proxy/load balancer issues
|
||||
3. Check browser console for actual WebSocket error
|
||||
4. Review CapRover Nginx configuration
|
||||
|
||||
## Performance
|
||||
|
||||
### Connection Pooling
|
||||
|
||||
The Redis connection service uses a single `ConnectionMultiplexer` instance shared across the application. This is the recommended approach for StackExchange.Redis.
|
||||
|
||||
### SignalR Message Volume
|
||||
|
||||
Redis pub/sub is used for SignalR messages:
|
||||
|
||||
- Low overhead for typical SignalR usage
|
||||
- Efficient binary protocol
|
||||
- Automatic message routing
|
||||
|
||||
### Scaling Considerations
|
||||
|
||||
- **2-3 instances**: Optimal for most workloads
|
||||
- **4+ instances**: Consider Redis clustering for high availability
|
||||
- Monitor Redis memory usage if using for caching
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
The generic Redis service can be extended for:
|
||||
|
||||
1. **Distributed Caching**: Replace `IDistributedCache` implementation
|
||||
2. **Session Storage**: Store user sessions in Redis
|
||||
3. **Rate Limiting**: Use Redis for distributed rate limiting
|
||||
4. **Pub/Sub**: Implement event-driven architecture
|
||||
5. **Job Queues**: Background job processing with Redis
|
||||
|
||||
## References
|
||||
|
||||
- [StackExchange.Redis Documentation](https://stackexchange.github.io/StackExchange.Redis/)
|
||||
- [SignalR Scale-out with Redis](https://learn.microsoft.com/en-us/aspnet/core/signalr/scale)
|
||||
- [Redis Best Practices](https://redis.io/docs/manual/patterns/)
|
||||
|
||||
160
src/Managing.Infrastructure.Storage/RedisConnectionService.cs
Normal file
160
src/Managing.Infrastructure.Storage/RedisConnectionService.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace Managing.Infrastructure.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// Generic Redis connection service that manages a single Redis connection multiplexer
|
||||
/// to be shared across the application for various purposes (SignalR, caching, etc.)
|
||||
/// </summary>
|
||||
public class RedisConnectionService : IRedisConnectionService, IDisposable
|
||||
{
|
||||
private readonly ILogger<RedisConnectionService> _logger;
|
||||
private readonly string? _connectionString;
|
||||
private IConnectionMultiplexer? _connection;
|
||||
private readonly object _lock = new object();
|
||||
private bool _disposed;
|
||||
|
||||
public RedisConnectionService(IConfiguration configuration, ILogger<RedisConnectionService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
// Try configuration keys in priority order
|
||||
// Priority: ConnectionStrings:Redis (can be set via ConnectionStrings__Redis env var) > REDIS_URL env var
|
||||
_connectionString = configuration.GetConnectionString("Redis")
|
||||
?? configuration["REDIS_URL"];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_connectionString))
|
||||
{
|
||||
_logger.LogWarning("Redis connection string not configured. Redis features will be unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
InitializeConnection();
|
||||
}
|
||||
|
||||
private void InitializeConnection()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_connectionString))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Initializing Redis connection to: {ConnectionString}",
|
||||
MaskConnectionString(_connectionString));
|
||||
|
||||
var options = ConfigurationOptions.Parse(_connectionString);
|
||||
|
||||
// Configure connection options
|
||||
options.AbortOnConnectFail = false; // Don't fail the app if Redis is down
|
||||
options.ConnectTimeout = 5000; // 5 second timeout
|
||||
options.SyncTimeout = 5000;
|
||||
options.AsyncTimeout = 5000;
|
||||
options.ConnectRetry = 3;
|
||||
options.KeepAlive = 60; // Send keepalive every 60 seconds
|
||||
|
||||
_connection = ConnectionMultiplexer.Connect(options);
|
||||
|
||||
_connection.ConnectionFailed += OnConnectionFailed;
|
||||
_connection.ConnectionRestored += OnConnectionRestored;
|
||||
_connection.ErrorMessage += OnErrorMessage;
|
||||
|
||||
_logger.LogInformation("✅ Redis connection established successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to connect to Redis. Redis features will be unavailable.");
|
||||
_connection = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConnectionFailed(object? sender, ConnectionFailedEventArgs e)
|
||||
{
|
||||
_logger.LogError("❌ Redis connection failed: {Exception}", e.Exception?.Message ?? "Unknown error");
|
||||
}
|
||||
|
||||
private void OnConnectionRestored(object? sender, ConnectionFailedEventArgs e)
|
||||
{
|
||||
_logger.LogInformation("✅ Redis connection restored");
|
||||
}
|
||||
|
||||
private void OnErrorMessage(object? sender, RedisErrorEventArgs e)
|
||||
{
|
||||
_logger.LogError("Redis error: {Message}", e.Message);
|
||||
}
|
||||
|
||||
public IConnectionMultiplexer? GetConnection()
|
||||
{
|
||||
if (_connection != null && _connection.IsConnected)
|
||||
{
|
||||
return _connection;
|
||||
}
|
||||
|
||||
// Try to reconnect if connection was lost
|
||||
lock (_lock)
|
||||
{
|
||||
if (_connection == null || !_connection.IsConnected)
|
||||
{
|
||||
_logger.LogWarning("Redis connection lost, attempting to reconnect...");
|
||||
_connection?.Dispose();
|
||||
_connection = null;
|
||||
InitializeConnection();
|
||||
}
|
||||
}
|
||||
|
||||
return _connection;
|
||||
}
|
||||
|
||||
public IDatabase? GetDatabase(int db = -1)
|
||||
{
|
||||
var connection = GetConnection();
|
||||
return connection?.GetDatabase(db);
|
||||
}
|
||||
|
||||
public bool IsConnected => _connection != null && _connection.IsConnected;
|
||||
|
||||
public string? ConnectionString => _connectionString;
|
||||
|
||||
private static string MaskConnectionString(string connectionString)
|
||||
{
|
||||
// Mask password in connection string for logging
|
||||
if (connectionString.Contains("password=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var parts = connectionString.Split(',');
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
if (parts[i].Trim().StartsWith("password=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
parts[i] = "password=***";
|
||||
}
|
||||
}
|
||||
return string.Join(",", parts);
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
if (_connection != null)
|
||||
{
|
||||
_logger.LogInformation("Disposing Redis connection");
|
||||
_connection.ConnectionFailed -= OnConnectionFailed;
|
||||
_connection.ConnectionRestored -= OnConnectionRestored;
|
||||
_connection.ErrorMessage -= OnErrorMessage;
|
||||
_connection.Dispose();
|
||||
_connection = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user