Comments 8
Мне понравилось, довольно хорошо описано. Механизм шифрования понятный, envelope описан ясно. Желаю успехов в развитии данного проекта, ведь тема таких мессенджеров сейчас как никогда актуальна.
Этаж чё получается... Мошенники могут общаться в нем и обманывать людей? А у правоохранительных органов доступа нету? ЗАПРЕТИТЬ!)
Привет!
За завтраком полистал код на гитхабе.
Действительно проект явно учебный. Я пока не дошел до шифрования, да и не претендую на понимание WebCrypto, но множество нездоровых моментов вынужден подсветить:
private void notifySharedChatsAboutProfileUpdate(Long updatedUserId) {
Set<Long> chatIds = new LinkedHashSet<>();
participantRepository.findByUserId(updatedUserId)
.forEach(participant -> chatIds.add(participant.getChatId()));
for (Long chatId : chatIds) {
participantRepository.findByChatId(chatId).forEach(participant ->
userRepository.findById(participant.getUserId()).ifPresent(user ->
messagingTemplate.convertAndSend(
"/topic/users/" + user.getUsername() + "/chats",
Map.of(
"chatId", chatId,
"reason", "profile_updated",
"updatedUserId", updatedUserId,
"timestamp", System.currentTimeMillis()
)
)
)
);
}
}Вот код уведомления юзеров.
Вы явно тут пропустили N+1 проблему. Тут - participantRepository.findByChatId(chatId) - Для КАЖДОГО чата делается выборка ВСЕХ участников. Если у пользователя 100 чатов, будет 101 запрос (1 на получение его чатов + 100 на участников каждого чата).
Кроме того если пользователь состоит в разных чатах, он получит уведомление из каждого. Стоит пересмотреть и сэкономить на количестве отправок (чем больше чатов тем больше будет заметна разница).
В итоге ваш чат упадет на реальной нагрузке с >100 чатами и >50 участниками.
@Transactional
public void markChatAsRead(String username, Long chatId) {
User user = requireUser(username);
UserDevice currentDevice = currentDeviceOrNull();
String deviceId = deviceIdOrFallback(currentDevice);
requireParticipant(chatId, user.getId());
unreadService.reset(user.getId(), chatId);
List<Message> messages = messageRepository.findByChatIdAndSenderIdNot(chatId, user.getId());
for (Message message : messages) {
markReceiptRead(message, user.getId(), deviceId);
updateAggregateStatus(message);
sendStatusToSenderDevices(message, message.getStatus().name());
}
incrementCounter("messages_read_total", messages.size());
notifyChatListUpdated(chatId, "chat_read");
}Или вот другой метод. Он делает слишком много. Если в чате 10 000 сообщений, этот метод
Достанет их все из БД
Пройдёт по каждому в цикле
Пошлёт 10 000 WebSocket уведомлений
И ожидаемо ляжет под нагрузкой.
Кроме того куча элементарного: куча логики в контроллерах (и даже создание дто КАРЛ!), возвращение Map<String, Object> вместо объекта, повсеместная конкатенация строк (String key = "unread:" + userId + ":" + chatId;) которая тоже по капле сожрет вашу память, сортировка в памяти (.sorted((a, b) -> { ... })), что также не даст пользоваться проектом большому количеству людей - все это либо сознательное упущение в пользу учебного проекта, либо Вам есть куда учиться.
Так или иначе я бы не стал заявлять столь громогласно о готовности)))
Я посмотрел не все. Проект большой и очень амбициозный. И я понимаю, что основная концентрация была скорее всего на E2EE, но в остальном данный проект лучше не запускать в большой мир - утонете в багах.
Будет желание - пишите, могу помочь с подсветкой подобных проблем. Могу порекомендовать неплохое видео по пагинации (не реклама).
Привет! Спасибо за подробный разбор. Такой комментарий действительно полезен: по обычному демо в браузере часть подобных проблем легко не заметить, а при чтении кода они становятся видны сразу.
По нескольким пунктам я с вами согласен.
Замечание про N+1 в уведомлениях при обновлении профиля справедливое. В старом варианте логика действительно могла идти через цепочку “найти чаты пользователя → для каждого чата найти участников → для каждого участника найти пользователя → отправить событие”. Для небольшого демо это незаметно, но как паттерн для роста нагрузки такой подход неудачный.
То же самое с markChatAsRead(). В исходном варианте метод делал слишком много за один вызов: получал список сообщений, проходил по ним в цикле, обновлял receipt/status и отправлял отдельные события. На длинной истории это действительно плохой путь, особенно если чат активно используется.
После вашего комментария я отдельно прошёлся по backend и переработал эти участки.
Что было изменено:
— уведомления при обновлении профиля и обновлении списка чатов переведены на batch-запросы и отправку по уникальным получателям;
— mark read / mark delivered переписаны через bulk SQL-операции вместо поштучного прохода по сообщениям;
— вместо множества отдельных status events добавлен bulk status event;
— timeline reactions теперь грузятся пачкой, без N+1 на каждое сообщение;
— список чатов получил пагинацию и сортировку ближе к БД;
— часть логики вынесена из контроллеров в сервисы;
— Map<String, Object> в ключевых REST-ответах заменён на typed DTO;
— добавлены индексы под горячие запросы через миграцию V23__performance_indexes.sql;
— после рефакторинга обновлены тесты.

Отдельно прогнал тесты и нагрузочные сценарии по direct-chat path. Backend сейчас проходит 192 теста. По k6 direct-chat battery получилось:

Самым важным результатом для меня было не просто отсутствие ошибок, а то, что mark read / mark delivered не начали деградировать на длинном прогоне. В 30-минутном soak:
При этом я не хочу выдавать эти результаты за полную production-готовность. Это пока проверка именно direct-chat HTTP/API-сценария. Групповые чаты, WebSocket load, сценарии с заранее набитыми 10k+ сообщений, поведение БД на больших объёмах и инфраструктурные лимиты ещё нужно проверять отдельно.
Поэтому я бы сейчас аккуратно сформулировал статус проекта так: это не production-ready мессенджер, но уже рабочий MVP/pet project, который постепенно приводится к более зрелому backend-состоянию.
Ваш комментарий помог подсветить места, которые на обычном демо не бросались в глаза. Спасибо за это. Если будет желание — буду рад продолжить предметно по query patterns, pagination strategy, receipt aggregation и group chat fanout.
Дополнительно проверили WebSocket‑часть: на локальном стенде соединения держатся стабильно, а предел дальше упирается уже в ресурсы машины.

Как я написал E2EE-мессенджер на Spring Boot и WebCrypto — и почему сервер не видит сообщения