Pull to refresh

Comments 8

Мне понравилось, довольно хорошо описано. Механизм шифрования понятный, envelope описан ясно. Желаю успехов в развитии данного проекта, ведь тема таких мессенджеров сейчас как никогда актуальна.

Этаж чё получается... Мошенники могут общаться в нем и обманывать людей? А у правоохранительных органов доступа нету? ЗАПРЕТИТЬ!)

█████████████████████████████████▶ 75% начинаю удаление репозитория…

Привет!

За завтраком полистал код на гитхабе.

Действительно проект явно учебный. Я пока не дошел до шифрования, да и не претендую на понимание 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 получилось:

Суммарно по direct-chat battery:
Суммарно по 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.

Вот ведь чего бывает, если не пользуешься нейро... тьфу ты, пакость...
</sarcasm>

Дополнительно проверили WebSocket‑часть: на локальном стенде соединения держатся стабильно, а предел дальше упирается уже в ресурсы машины.

Sign up to leave a comment.

Articles