11 KiB
11 KiB
Chat Service Architecture Documentation
Detailed architecture for Chat Service with SignalR, scalability patterns, and AI integration.
Architecture Overview
graph TB
subgraph "Clients"
WEB[Web App]
MOB[Mobile App]
BOT[Bot Client]
end
subgraph "Load Balancer"
LB[NGINX/Traefik]
SS[Sticky Sessions]
end
subgraph "Chat Service Instances"
CS1[Instance 1<br/>SignalR Hub]
CS2[Instance 2<br/>SignalR Hub]
CS3[Instance 3<br/>SignalR Hub]
end
subgraph "Backplane"
RD[(Redis<br/>Pub/Sub)]
end
subgraph "Storage"
PG[(PostgreSQL<br/>Messages)]
RC[(Redis<br/>Cache)]
end
subgraph "AI Service"
AI[OpenAI<br/>GPT-4]
end
WEB & MOB & BOT --> LB
LB --> SS --> CS1 & CS2 & CS3
CS1 & CS2 & CS3 <--> RD
CS1 & CS2 & CS3 --> PG
CS1 & CS2 & CS3 --> RC
CS1 & CS2 & CS3 --> AI
style LB fill:#3498db,stroke:#2980b9,color:#fff
style RD fill:#e74c3c,stroke:#c0392b,color:#fff
style PG fill:#27ae60,stroke:#1e8449,color:#fff
style AI fill:#9b59b6,stroke:#7d3c98,color:#fff
SignalR Hub Architecture
Connection Lifecycle
sequenceDiagram
participant Client
participant Hub as ChatHub
participant Groups
participant DB as Database
participant Redis
Client->>Hub: Connect (JWT Token)
Hub->>Hub: OnConnectedAsync()
Hub->>DB: Load user rooms
Hub->>Groups: AddToGroupAsync(roomId)
Hub->>Redis: Publish(UserOnline)
Hub-->>Client: Connected
Note over Client,Hub: User is now online
Client->>Hub: SendMessage(roomId, content)
Hub->>DB: Save message
Hub->>Redis: Publish(NewMessage)
Redis-->>Hub: Broadcast to all instances
Hub->>Groups: Clients.Group(roomId)
Hub-->>Client: ReceiveMessage
Hub Implementation
public class ChatHub : Hub<IChatClient>
{
private readonly IChatRoomRepository _roomRepository;
private readonly IMessageRepository _messageRepository;
private readonly IAIService _aiService;
public async Task JoinRoom(Guid roomId)
{
var userId = Context.UserIdentifier;
// Add to SignalR Group
await Groups.AddToGroupAsync(Context.ConnectionId, roomId.ToString());
// Notify other members
await Clients.Group(roomId.ToString())
.UserJoined(userId, roomId);
// Persist state to DB
await _roomRepository.AddParticipantAsync(roomId, userId);
}
public async Task SendMessage(Guid roomId, string content)
{
var message = new Message(roomId, Context.UserIdentifier, content);
await _messageRepository.AddAsync(message);
// Broadcast message
await Clients.Group(roomId.ToString())
.ReceiveMessage(message.ToDto());
// Check AI trigger
if (content.StartsWith("@gpt "))
{
await StreamAIResponse(roomId, content[5..]);
}
}
public async IAsyncEnumerable<string> StreamAIResponse(
Guid roomId,
string prompt,
[EnumeratorCancellation] CancellationToken ct = default)
{
// Get chat history for context
var history = await _messageRepository.GetHistoryAsync(roomId, 20);
await foreach (var chunk in _aiService.StreamAsync(prompt, history, ct))
{
yield return chunk;
}
}
}
Scalability Architecture
Redis Backplane
graph LR
subgraph "Instance 1"
C1[Client A] --> H1[Hub 1]
end
subgraph "Instance 2"
C2[Client B] --> H2[Hub 2]
end
subgraph "Redis"
CH[Channel: chat.messages]
end
H1 -->|Publish| CH
CH -->|Subscribe| H2
H2 -->|Deliver| C2
style CH fill:#e74c3c,stroke:#c0392b,color:#fff
Scaling Options
| Option | Pros | Cons |
|---|---|---|
| Redis Backplane | Simple, on-premise | Requires Redis cluster management |
| Azure SignalR | Serverless, no sticky sessions | Vendor lock-in, cost |
| Sticky Sessions | Simplest | Imperfect for failover |
// Redis Backplane Configuration
builder.Services.AddSignalR()
.AddStackExchangeRedis(options =>
{
options.Configuration.ChannelPrefix =
RedisChannel.Literal("ChatService");
options.Configuration.AbortOnConnectFail = false;
});
// Azure SignalR Service
builder.Services.AddSignalR()
.AddAzureSignalR(options =>
{
options.ConnectionString = azureSignalRConnectionString;
options.ServerStickyMode = ServerStickyMode.Required;
});
User & Group Management
User Mapping
graph TB
subgraph "User Mapping"
U1[User ID: user-123]
C1[Connection 1<br/>Phone]
C2[Connection 2<br/>Laptop]
C3[Connection 3<br/>Tablet]
end
U1 --> C1 & C2 & C3
style U1 fill:#3498db,stroke:#2980b9,color:#fff
IUserIdProvider
public class ClaimsUserIdProvider : IUserIdProvider
{
public string? GetUserId(HubConnectionContext connection)
{
// Get User ID from JWT Claims
return connection.User?
.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}
}
// Registration
builder.Services.AddSingleton<IUserIdProvider, ClaimsUserIdProvider>();
Group State Management
// Problem: SignalR Groups don't persist on restart
// Solution: Store in database
public class GroupStateService
{
private readonly IDistributedCache _cache;
private readonly IChatRoomRepository _repository;
public async Task RestoreUserGroups(string userId, string connectionId)
{
var rooms = await _repository.GetUserRoomsAsync(userId);
foreach (var room in rooms)
{
await _hubContext.Groups.AddToGroupAsync(
connectionId,
room.Id.ToString()
);
}
}
}
AI Integration Flow
sequenceDiagram
participant User
participant Hub as ChatHub
participant AI as AIService
participant OpenAI
participant DB as Database
User->>Hub: "@gpt Explain DDD"
Hub->>DB: Load history (20 messages)
Hub->>AI: StreamAsync(prompt, history)
AI->>OpenAI: ChatCompletion (stream: true)
loop Streaming
OpenAI-->>AI: Token chunk
AI-->>Hub: yield chunk
Hub-->>User: ReceiveAIChunk
end
Hub->>DB: Save AI response
Hub-->>User: AIResponseComplete
Domain Model
Aggregate Roots
erDiagram
ChatRoom ||--o{ Message : contains
ChatRoom ||--o{ Participant : has
ChatRoom {
uuid id PK
string name
enum type
uuid owner_id
timestamp created_at
}
Message {
uuid id PK
uuid room_id FK
uuid sender_id
string content
enum type
timestamp sent_at
}
Participant {
uuid id PK
uuid room_id FK
uuid user_id
enum role
timestamp joined_at
timestamp last_read
}
Domain Events
| Event | Trigger | Handler |
|---|---|---|
MessageSentEvent |
New message | Update last_activity, broadcast |
UserJoinedRoomEvent |
User joins room | Notify members |
UserLeftRoomEvent |
User leaves room | Notify members |
RoomCreatedEvent |
Create new room | Add creator as admin |
MessagePack Protocol
JSON vs MessagePack Comparison
| Metric | JSON | MessagePack |
|---|---|---|
| Size | 100% | ~50-70% |
| Parse time | 1x | 0.5-0.8x |
| Type safety | Weak | Strong |
Configuration
// Server
builder.Services.AddSignalR()
.AddMessagePackProtocol(options =>
{
options.SerializerOptions =
MessagePackSerializerOptions.Standard
.WithSecurity(MessagePackSecurity.UntrustedData)
.WithCompression(MessagePackCompression.Lz4BlockArray);
});
Resiliency Patterns
Automatic Reconnect
stateDiagram-v2
[*] --> Connected
Connected --> Disconnected: Connection lost
Disconnected --> Reconnecting: Auto retry
Reconnecting --> Connected: Success
Reconnecting --> Reconnecting: Retry with backoff
Reconnecting --> Disconnected: Max retries exceeded
Disconnected --> [*]: User logout
Stateful Reconnect (.NET 8+)
// Server configuration
builder.Services.AddSignalR(options =>
{
options.StatefulReconnectBufferSize = 32 * 1024; // 32KB
});
// With UseStatefulReconnect
app.MapHub<ChatHub>("/chatHub", options =>
{
options.AllowStatefulReconnects = true;
});
Deployment Architecture
Kubernetes with Sticky Sessions
apiVersion: apps/v1
kind: Deployment
metadata:
name: chatservice
spec:
replicas: 3
template:
spec:
containers:
- name: chatservice
image: chatservice:latest
ports:
- containerPort: 8080
env:
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: chat-secrets
key: redis-url
---
apiVersion: v1
kind: Service
metadata:
name: chatservice
annotations:
service.beta.kubernetes.io/aws-load-balancer-sticky-sessions: "true"
spec:
type: LoadBalancer
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 3600
ports:
- port: 80
targetPort: 8080
Ingress with WebSocket Support
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: chatservice
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri"
nginx.ingress.kubernetes.io/affinity: "cookie"
spec:
rules:
- host: chat.goodgo.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: chatservice
port:
number: 80
Health Checks
builder.Services.AddHealthChecks()
.AddNpgSql(connectionString, name: "postgresql")
.AddRedis(redisConnectionString, name: "redis")
.AddSignalRHub<ChatHub>(name: "signalr-hub");