Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 182 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,182 @@
# RealTimeChat
Chat App example
# Real-Time Chat Application

Серверное приложение группового чата на **Spring Boot** с использованием **WebSocket (STOMP)**, многопоточности, AOP, Redis, Bucket4j и Resilience4j.

## 🛠 Технологии

- **Java 17**
- **Spring Boot 3.2**
- **Spring WebSocket + STOMP**
- **Spring Security + JWT**
- **Spring Data JPA** (H2 / PostgreSQL)
- **Spring AOP + AspectJ**
- **Redis** (Bucket4j + Dead Letter Stream)
- **Bucket4j** (rate limiting через Lettuce)
- **Resilience4j** (Circuit Breaker)
- **Micrometer** (метрики)
- **Lombok**
- **Testcontainers + JUnit 5**

## 🏗 Архитектура

Проект построен по **многослойной архитектуре** с чётким разделением ответственности и выносом сквозной логики в AOP.

```text
┌──────────────────┐
│ Web / WS Layer │ ← @Controller, @MessageMapping, WebSocketEventListener
├──────────────────┤
│ Service Layer │ ← ChatService, UserService, MessagePersistenceService
│ (обёрнута AOP) │
├──────────────────┤
│ Cross‑cutting │ ← @WithUserContext, @RateLimited, @Idempotent,
│ (Aspects) │ @CircuitBreaker, @Async
├──────────────────┤
│ Infrastructure │ ← ThreadPool, Redis, MeterRegistry, DLQ
├──────────────────┤
│ Persistence │ ← JPA + Redis Streams (DLQ)
└──────────────────┘
Структура пакетов
io.ylab.chat
├── aop/ # ChatGuardAspect, DegradationAspect
├── config/ # Security, WebSocket, Async, RateLimiter, Redis
├── controller/ # REST (Auth, Metrics)
├── websocket/ # ChatWebSocketController, WebSocketEventListener
├── service/ # ChatService, UserService, MessagePersistenceService, DLQ
├── context/ # UserContext (ThreadLocal)
├── concurrency/ # MessageIdRegistry, OnlineUserRegistry
├── entity/ # MessageEntity, UserEntity
├── repository/ # JPA репозитории
├── dto/ # ChatMessageDto, AuthRequest, AuthResponse
├── exception/ # Кастомные исключения
└── util/ # JwtUtil, TimeProvider, RedisConstants
```
### Основная функциональность
- Аутентификация через JWT (REST + WebSocket)
- Групповой чат (одна общая комната с broadcast)
- Отображение онлайн-статуса пользователей
- Идемпотентность сообщений (защита от дублей, TTL 5 мин)
- Rate limiting сообщений через Bucket4j + Redis
- Асинхронное сохранение сообщений в БД (@Async + отдельный пул потоков)
### Отказоустойчивость:
- Circuit Breaker (Resilience4j)
- Degraded Mode (чат работает даже при падении БД)
- Dead Letter Queue на Redis Streams

Сбор метрик через Micrometer

🚀 Установка и запуск
Требования

JDK 17
Maven 3.6+
Redis (локально или Docker)

Запуск Redis через Docker
```
docker run -d -p 6379:6379 --name chat-redis redis:7-alpine
```
Запуск приложения
```
mvn clean spring-boot:run
```
Приложение будет доступно по адресу: http://localhost:8080
H2 Console (для разработки):
Comment on lines +76 to +83

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

лучше было написать dockerfile и добавить в docker compose чтобы запускалось без проблем вне зависимости от окружения.

http://localhost:8080/h2-console
JDBC URL: jdbc:h2:mem:chatdb
Конфигурация (application.yml)
```YAML
jwt:
secret: base64Encoded512bitKey...
expiration: 3600000 # 1 час

rate-limiter:
messages:
capacity: 5
refill-tokens: 5
refill-duration-seconds: 60

spring:
data:
redis:
host: localhost
port: 6379
```
📡 API и WebSocket
REST Endpoints

| Метод | URL | Описание | Аутентификация |
|-------|------------------------|-----------------------------------|---------------------|
| POST | `/register` | Регистрация пользователя | Нет |
| POST | `/login` | Авторизация → JWT токен | Нет |
| GET | `/metrics/online` | Список онлайн пользователей | Bearer JWT |
| POST | `/metrics/recover` | Выход из degraded mode | Bearer JWT |


WebSocket (STOMP)

Endpoint: /ws (с поддержкой SockJS)
Подписка на сообщения: /topic/chat
Отправка сообщений: /app/chat.send
Приватные ошибки: /user/queue/errors
```

Пример подключения (JavaScript):
```js
JavaScriptconst token = 'your-jwt-token';
const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);

stompClient.connect({ Authorization: `Bearer ${token}` }, () => {
stompClient.subscribe('/topic/chat', (msg) => {
console.log('Received:', JSON.parse(msg.body));
});

stompClient.send('/app/chat.send', {}, JSON.stringify({
text: "Привет всем!"
}));
});
```
Формат сообщения (ChatMessageDto):
```JSON
{
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"sender": "john",
"text": "Hello, world!",
"timestamp": 1706473200000
}
```
🧪 Тестирование
```
mvn test
```
Основные тесты:

ChatGuardAspectTest — проверка AOP-аннотаций
MessagePersistenceFallbackTest — Circuit Breaker + DLQ
DegradationAspectTest, OnlineUserRegistryTest и др.


🎯 Аспекты (AOP)
| Аспект | Аннотация | Order | Ответственность |
|---------------------|----------------------------|-------|------------------------------------------------------|
| ChatGuardAspect | `@WithUserContext` | 10 | Установка UserContext + проверка аутентификации |
| ChatGuardAspect | `@Idempotent` | 10 | Защита от дублирования сообщений |
| ChatGuardAspect | `@RateLimited` | 10 | Rate limiting через Bucket4j + Redis |
| DegradationAspect | `execution(* ...saveAsync)`| 4 | Переход в degraded mode при ошибках сохранения |


⚙️ Многопоточность и отказоустойчивость

Асинхронное сохранение сообщений через @Async и отдельный ThreadPoolTaskExecutor (core=8, max=16)
Dead Letter Queue — Redis Stream chat:dead-letter:stream
Degraded Mode — при 5 последовательных ошибках сохранения чат продолжает работать без записи в БД
Circuit Breaker + fallback в DLQ

📊 Мониторинг и метрики
Доступны метрики Micrometer:

websocket.messages.sent
websocket.connections.total
websocket.online.users (Gauge)

Эндпоинт: GET /metrics (только для авторизованных пользователей)
53 changes: 53 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
version: '3.9'

services:
redis:
image: redis:latest
container_name: chat-redis
ports:
- "6379:6379"
command: redis-server --save 60 1 --loglevel warning
volumes:
- redis-data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5

prometheus:
image: prom/prometheus:v2.53.1
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
- '--web.console.templates=/usr/share/prometheus/consoles'
restart: unless-stopped

grafana:
image: grafana/grafana:11.4.0
container_name: grafana
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin123
- GF_USERS_ALLOW_SIGN_UP=false
depends_on:
- prometheus
restart: unless-stopped

volumes:
redis-data:
prometheus-data:
grafana-data:
49 changes: 49 additions & 0 deletions grafana/provisioning/dashboards/chat-app-dashboard.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"title": "Chat Application Dashboard",
"uid": "chat-app-dashboard",
"version": 1,
"time": { "from": "now-30m", "to": "now" },
"timepicker": { "refresh_intervals": ["5s","10s","30s","1m"] },
"panels": [
{
"title": "Active WebSocket Connections",
"type": "gauge",
"targets": [{ "expr": "spring_websocket_session_active", "legendFormat": "Active Sessions" }]
},
{
"title": "Messages Sent per Second",
"type": "timeseries",
"targets": [{ "expr": "rate(spring_websocket_message_sent_count[30s])", "legendFormat": "Messages/sec" }]
},
{
"title": "Persistence saveAsync - Latency",
"type": "timeseries",
"targets": [{ "expr": "rate(persistence_saveAsync_seconds_sum[30s]) / rate(persistence_saveAsync_seconds_count[30s])", "legendFormat": "Avg Latency" }]
},
{
"title": "Persistence saveAsync - Count",
"type": "stat",
"targets": [{ "expr": "persistence_saveAsync_seconds_count" }]
},
{
"title": "DLQ Push Count",
"type": "stat",
"targets": [{ "expr": "dlq_pushFailedMessage_seconds_count" }]
},
{
"title": "Circuit Breaker State",
"type": "stat",
"targets": [{ "expr": "resilience4j_circuitbreaker_state{name=\"messagePersistence\"}" }]
},
{
"title": "Circuit Breaker Failure Rate",
"type": "gauge",
"targets": [{ "expr": "resilience4j_circuitbreaker_failure_rate{name=\"messagePersistence\"}" }]
},
{
"title": "Online Users (Gauge)",
"type": "gauge",
"targets": [{ "expr": "websocket_online_users" }]
}
]
}
11 changes: 11 additions & 0 deletions grafana/provisioning/dashboards/dashboard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: 1

providers:
- name: 'Chat App Dashboards'
orgId: 1
folder: 'Chat Application'
type: file
disableDeletion: false
editable: true
options:
path: /etc/grafana/provisioning/dashboards
10 changes: 10 additions & 0 deletions grafana/provisioning/datasources/datasource.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: 1

datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
jsonData:
timeInterval: 10s
Loading