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:
243
REDIS_SIGNALR_DEPLOYMENT.md
Normal file
243
REDIS_SIGNALR_DEPLOYMENT.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# Redis + SignalR Multi-Instance Deployment Guide
|
||||
|
||||
## Summary
|
||||
|
||||
The Managing API now supports **multiple instances** with **SignalR** (for LlmHub, BotHub, BacktestHub) using a **Redis backplane**.
|
||||
|
||||
This solves the "No Connection with that ID" error that occurs when:
|
||||
- `/llmhub/negotiate` hits instance A
|
||||
- WebSocket connection hits instance B (which doesn't know about the connection ID)
|
||||
|
||||
## What Was Added
|
||||
|
||||
### 1. Infrastructure Layer - Generic Redis Service
|
||||
|
||||
**Files Created:**
|
||||
- `src/Managing.Application.Abstractions/Services/IRedisConnectionService.cs` - Interface
|
||||
- `src/Managing.Infrastructure.Storage/RedisConnectionService.cs` - Implementation
|
||||
- `src/Managing.Infrastructure.Storage/README-REDIS.md` - Documentation
|
||||
|
||||
**Purpose:** Generic Redis connectivity that can be used for SignalR, caching, or any Redis needs.
|
||||
|
||||
### 2. SignalR Redis Backplane
|
||||
|
||||
**Files Modified:**
|
||||
- `src/Managing.Api/Program.cs` - Auto-configures SignalR with Redis when available
|
||||
- `src/Managing.Bootstrap/ApiBootstrap.cs` - Registers Redis service
|
||||
|
||||
**How It Works:**
|
||||
- Checks if Redis is configured
|
||||
- If yes: Adds Redis backplane to SignalR
|
||||
- If no: Runs in single-instance mode (graceful degradation)
|
||||
|
||||
### 3. Configuration
|
||||
|
||||
**Files Modified:**
|
||||
- `src/Managing.Api/appsettings.json` - Default config (empty, for local dev)
|
||||
- `src/Managing.Api/appsettings.Sandbox.json` - `srv-captain--redis:6379`
|
||||
- `src/Managing.Api/appsettings.Production.json` - `srv-captain--redis:6379`
|
||||
|
||||
### 4. NuGet Packages Added
|
||||
|
||||
- `Microsoft.AspNetCore.SignalR.StackExchangeRedis` (8.0.10) - SignalR backplane
|
||||
- `Microsoft.Extensions.Caching.StackExchangeRedis` (8.0.10) - Redis caching
|
||||
- `StackExchange.Redis` (2.8.16) - Redis client
|
||||
|
||||
## Deployment Steps for CapRover
|
||||
|
||||
### Step 1: Create Redis Service
|
||||
|
||||
1. In CapRover, go to **Apps**
|
||||
2. Click **One-Click Apps/Databases**
|
||||
3. Search for "Redis"
|
||||
4. Deploy Redis (or use existing one)
|
||||
5. Note the service name: `srv-captain--redis` (or your custom name)
|
||||
|
||||
### Step 2: Configure CapRover App
|
||||
|
||||
For `dev-managing-api` (Sandbox):
|
||||
|
||||
1. **Enable WebSocket Support**
|
||||
- Go to **HTTP Settings**
|
||||
- Toggle **"WebSocket Support"** to ON
|
||||
- Save
|
||||
|
||||
2. **Enable Sticky Sessions**
|
||||
- In **HTTP Settings**
|
||||
- Toggle **"Enable Sticky Sessions"** to ON
|
||||
- Save
|
||||
|
||||
3. **Verify Redis Connection String**
|
||||
- The connection string is already in `appsettings.Sandbox.json`
|
||||
- Default: `srv-captain--redis:6379`
|
||||
- If you used a different Redis service name, update via environment variable:
|
||||
```
|
||||
ConnectionStrings__Redis=srv-captain--your-redis-name:6379
|
||||
```
|
||||
- Or use the fallback:
|
||||
```
|
||||
REDIS_URL=srv-captain--your-redis-name:6379
|
||||
```
|
||||
|
||||
### Step 3: Deploy
|
||||
|
||||
1. Build and deploy the API:
|
||||
```bash
|
||||
cd src/Managing.Api
|
||||
# Your normal deployment process
|
||||
```
|
||||
|
||||
2. Watch the logs during startup. You should see:
|
||||
```
|
||||
✅ Configuring SignalR with Redis backplane: srv-captain--redis:6379
|
||||
✅ Redis connection established successfully
|
||||
```
|
||||
|
||||
### Step 4: Scale to Multiple Instances
|
||||
|
||||
1. In CapRover, go to your `dev-managing-api` app
|
||||
2. **App Configs** tab
|
||||
3. Set **"Number of app instances"** to `2` or `3`
|
||||
4. Click **Save & Update**
|
||||
|
||||
### Step 5: Test
|
||||
|
||||
1. Open the frontend (Kaigen Web UI)
|
||||
2. Open the AI Chat
|
||||
3. Send a message
|
||||
4. Should work without "No Connection with that ID" errors
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After deployment, verify:
|
||||
|
||||
- [ ] Redis service is running in CapRover
|
||||
- [ ] WebSocket support is enabled
|
||||
- [ ] Sticky sessions are enabled
|
||||
- [ ] API logs show Redis connection success
|
||||
- [ ] Multiple instances are running
|
||||
- [ ] AI Chat works without connection errors
|
||||
- [ ] Browser Network tab shows WebSocket upgrade successful
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: "No Connection with that ID" Still Appears
|
||||
|
||||
**Check:**
|
||||
1. Redis service is running: `redis-cli -h srv-captain--redis ping`
|
||||
2. API logs show Redis connected (not "Redis not configured")
|
||||
3. Sticky sessions are ON
|
||||
4. WebSocket support is ON
|
||||
|
||||
**Quick Test:**
|
||||
- Temporarily set instances to 1
|
||||
- If it works with 1 instance, the issue is multi-instance setup
|
||||
- If it fails with 1 instance, check WebSocket/proxy configuration
|
||||
|
||||
### Issue: Redis Connection Failed
|
||||
|
||||
**Check Logs For:**
|
||||
```
|
||||
⚠️ Failed to configure SignalR Redis backplane: <error>
|
||||
SignalR will work in single-instance mode only
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
1. Verify Redis service name matches configuration
|
||||
2. Ensure Redis is not password-protected (or add password to config)
|
||||
3. Check Redis service health in CapRover
|
||||
|
||||
### Issue: WebSocket Upgrade Failed
|
||||
|
||||
Not related to Redis. Check:
|
||||
1. CapRover WebSocket support is ON
|
||||
2. Nginx configuration allows upgrades
|
||||
3. Browser console for detailed error
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### Connection String Formats
|
||||
|
||||
**Simple (no password):**
|
||||
```
|
||||
srv-captain--redis:6379
|
||||
```
|
||||
|
||||
**With Password:**
|
||||
```
|
||||
srv-captain--redis:6379,password=your-password
|
||||
```
|
||||
|
||||
**Multiple Options:**
|
||||
```
|
||||
srv-captain--redis:6379,password=pwd,ssl=true,abortConnect=false
|
||||
```
|
||||
|
||||
### Configuration Priority
|
||||
|
||||
The app checks these in order:
|
||||
1. `ConnectionStrings:Redis` (appsettings.json or `ConnectionStrings__Redis` environment variable)
|
||||
2. `REDIS_URL` (fallback environment variable)
|
||||
|
||||
**Recommended**: Use `ConnectionStrings__Redis` environment variable to override appsettings without rebuilding.
|
||||
|
||||
## Architecture Benefits
|
||||
|
||||
### Before (Single Instance)
|
||||
|
||||
```
|
||||
Frontend → Nginx → API Instance
|
||||
- In-memory SignalR
|
||||
- Connection IDs stored locally
|
||||
❌ Scale limited to 1 instance
|
||||
```
|
||||
|
||||
### After (Multi-Instance with Redis)
|
||||
|
||||
```
|
||||
Frontend → Nginx (sticky) → API Instance 1 ┐
|
||||
→ API Instance 2 ├─→ Redis ← SignalR Backplane
|
||||
→ API Instance 3 ┘
|
||||
|
||||
- Connection IDs in Redis
|
||||
- Messages distributed via pub/sub
|
||||
- Any instance can handle any connection
|
||||
✅ Scale to N instances
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
After successful deployment:
|
||||
|
||||
1. **Monitor Performance**
|
||||
- Watch Redis memory usage
|
||||
- Check API response times
|
||||
- Monitor WebSocket connection stability
|
||||
|
||||
2. **Consider Redis Clustering**
|
||||
- For high availability
|
||||
- If scaling beyond 3-4 API instances
|
||||
|
||||
3. **Extend Redis Usage**
|
||||
- Distributed caching
|
||||
- Rate limiting
|
||||
- Session storage
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues occur:
|
||||
|
||||
1. **Immediate**: Set instances to 1
|
||||
2. **Environment Variable**: Set `REDIS_URL=` (empty) to disable Redis
|
||||
3. **Code Rollback**: Previous version still works (graceful degradation)
|
||||
|
||||
The implementation is backward-compatible and doesn't require Redis to function.
|
||||
|
||||
## Support
|
||||
|
||||
For issues:
|
||||
1. Check logs: `src/Managing.Infrastructure.Storage/README-REDIS.md`
|
||||
2. Review this guide
|
||||
3. Check CapRover app logs for Redis/SignalR messages
|
||||
4. Test with 1 instance first, then scale up
|
||||
|
||||
@@ -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