15 KiB
почему в качестве валидации email - регулярка, когда можно использовать общепринятую функцию
tls конфликт
sea-orm: runtime-tokio-native-tls
sqlx: runtime-tokio-rustls
Причина долгого исполнения тестов: компиляция сервера каждый раз.
Command::new("cargo")
.args(["r", "--bin", "client"])
- решение: предскомпилировать сервер, и переиспользовать.
- Распараллелить тесты (может быть сложно)
- Проблема:
DELETE FROM table;— медленная операция (проверяет constraints, пишет в WAL). TRUNCATE — мгновенно.
async fn clear_db(db: &DbConn) -> Result<(), DbErr> {
db.execute_unprepared("
TRUNCATE abuse, abuse_reason, admin, \"like\", mail, refreshtoken,
thread, trail, trails_visit_history, \"user\"
RESTART IDENTITY CASCADE;
").await?;
Ok(())
}
1. **Токио
tokio = { version = "1,<1.48", features = ["full"] }
2. SeaORM
sea-orm = { version = "=1.1.17", ... }
3. SQLx + SeaORM
Почему это плохо:
sea-ormиспользуетsqlxпод капотом, но с другими runtime features (native-tlsvsrustls)- У вас два пула соединений с БД (один от SeaORM, второй от sqlx)
- Миграции на SeaORM, а тесты могут использовать sqlx напрямую → schema drift (расхождение схемы)
Конкретный риск: В tests/Cargo.toml вы подключаете оба. Если напишете тест, который использует sqlx::query! и sea_orm::Entity::insert, у вас будет race condition на уровне транзакций. PostgreSQL не увидит изменения из одного пула в другом до коммита, и ваши тесты будут flaky (нестабильными).
Техдолг: Вы платите за complexity tax (налог на сложность) дважды: за абстракцию ORM и за прямой доступ.
4. Storage Service: Premature abstraction, которая убьет вас
Ваша абстракция хранилища с mirror, backup, single стратегиями — это over-engineering без причины.
Почему это ошибка:
- У вас нет SLA на durability (надежность хранения)
- У вас нет метрик на RTO/RPO (Recovery Time/Point Objectives)
- У вас нет idempotency в storage operations
Конкретный баг: В mirror.rs если первый write успешен, а второй — нет, у вас silent data corruption (тихая порча данных). У вас нет transaction log для отката первого write. Это eventual consistency without reconciliation (согласование в конечном счете без сверки).
Техдолг: Вы построили distributed system там, где был достаточен std::fs::write. Через год, когда storage заполнится, вы не поймете, почему mirror стратегия оставляет мусор в старых бакетах — потому что нет garbage collection.
5. DTO Hell: Вы создаете 3-4 структуры на каждую сущность
Посмотрите на domain/src/client/profiles/requests.rs и responses.rs. У вас:
CreateUserRequestUpdateUserRequestPublicUserResponsePrivateUserResponseUserResponse
Это не separation of concerns, это duplication of concerns. Каждое изменение поля username — это изменение в 5 файлах и 2 миграциях.
Что вы потеряли: Single source of truth. У вас нет User entity из домена, который бы гарантировал инварианты. Вместо этого вы полагаетесь на валидацию в каждом DTO отдельно.
Техдолг: Schema spread. Когда добавите поле last_login_at, вы забудете обновить один из Response DTO, и клиент получит panicking deserialization.
6. Abuse System: Security theater (имитация безопасности)
У вас есть CreateAbuseRequest, abuse.rs, abuse_reason.rs, но нет rate limiting на создание abuse-репортов. Я могу написать скрипт, который заспамит 1M репортов на один trail и сложит вашу БД.
Что не так:
- Нет
abuse_reports_per_user_per_hourконстрейнта - Нет
abuse_scoreалгоритма (например, если 10 репортов от разных юзеров — автоматический бан) - Нет
abuse_report_fingerprint(я могу создавать репорты с разных аккаунтов, но с одного IP)
Техдолг: У вас есть data model для безопасности, но нет enforcement. Это как замок на двери без стен.
7. Transactions: Невидимый кошмар
В repository трейтах у вас методы типа create_trail, toggle_trail_like — каждый своя собственная транзакция.
Почему это баг: Если в use-case нужно создать trail, создать thread, поставить лайк — у вас три отдельные транзакции. Если последняя упадет, первые две останутся в БД. У вас inconsistent state без ROLLBACK.
Где посмотреть: api-core должен принимать &DatabaseTransaction и выполнять все операции в ней. Сейчас у вас autocommit hell — каждый repo.insert() — это COMMIT.
Техдолг: У вас нет transactional boundaries на уровне use-case. Это значит, ваши данные не консистентны в границах бизнес-операции.
8. API Keys Middleware: Тёмная материя без тестов
В api/src/middleware/api_key_middleware.rs (который вы не показали, но он подключен) — вероятно, нет integration tests.
Что это значит: Вы не знаете, работает ли rate limiting. Я могу brute-force API keys со скоростью 10k req/s, и ваша middleware может не выдержать нагрузку.
Техдолг: У вас нет нагрузочных тестов на middleware. Когда ваше приложение ляжет от DDoS, вы не поймете, виноват ли axum, tower, или ваш кастомный код.
9. Thread Model: Race condition при append
append_trail_to_thread — это concurrency nightmare.
Сценарий:
- User A читает thread (id=1, last_trail_id=5)
- User B читает thread (id=1, last_trail_id=5)
- Оба одновременно
append_trail_to_threadс parent_id=5 - У вас два trail'а с parent_id=5, но thread не знает, кто "победил"
У вас нет optimistic locking (version поле) или SELECT FOR UPDATE. Это lost update problem на уровне приложения.
Техдолг: У вас нет SERIALIZABLE isolation для критичных операций. Ваши треды могут разветвляться в невалидные структуры.
10. Observability: Вы слепы в production
У вас есть tracing, но нет:
- Metrics (
prometheus,metrics-rs) - Structured logging (JSON logs для ELK/Loki)
- Distributed tracing (OpenTelemetry для Jaeger/Tempo)
Что это значит: Когда в продакшене упадет запрос, вы увидите:
INFO: request started
INFO: request failed
Но не увидите: user_id, trace_id, duration_ms, db_query_count. Вы не сможете отладить performance regression после вашего PR.
Техдолг: У вас нет production-grade observability. Это значит, вы не сможете понять, что сломали, когда добавите ML-фичу.
11. Release Profile: Thin LTO — это медленный production
[profile.release]
lto = "thin"
Для вашего уровня performance-чувствительности (RTX 3050, 24GB RAM) это сознательный выбор скорости компиляции в ущерб runtime.
Что это значит: Ваш бинарник будет на 15-30% медленнее, чем мог бы быть. Для "тропиночного" приложения это не критично, но если вы планируете batch ML inference на GPU, вы теряете performance из-за неинлайненных вызовов.
Техдолг: У вас нет performance budget. Вы не знаете, сколько RPS может выдержать ваш сервер, потому что не тестировали под нагрузкой с lto = "fat".
12. Migrations: Append-only log без checksums
Ваш Migrator — это poor man's Flyway. Но нет:
- Checksum каждой миграции (если кто-то изменит старую миграцию, вы не узнаете)
- Baseline миграции (невозможно подключиться к существующей БД)
- Repair mode (если миграция упала на production)
Что это значит: Если ваш DevOps-engineer случайно отредактирует m20241005_112021_create_like.rs на проде-сервере, cargo run --bin migrate не заметит изменения и накатит кривую схему.
Техдолг: У вас нет migration integrity checks. Это значит, схема БД — mutable history, а не immutable log.
13. Dependencies: 150+ crate'ов, и вы не знаете, зачем
Запустите cargo tree | wc -l. У вас больше 150 зависимостей для CRUD приложения.
Конкретные лишние:
rust-argon2иbcrypt— два password hashing алгоритма. Выбери один.lazy_staticиonce_cell— дублирование (once_cell в std с 1.70)utoipaиutoipa-swagger-ui— генерируете OpenAPI, но нет тестов, что спецификация валиднаopendal— вы используете толькоmemoryиfs, но это 50+ transitive dependencies
Техдолг: У вас dependency bloat. Каждый cargo build тянет мир, а security audit (cargo audit) будет плакать каждую неделю.
14. Test Coverage: Hurl — это не enough
У вас 50+ hurl файлов, но нет unit тестов на api-core. Это значит, вы тестируете только happy path через HTTP.
Что вы не тестируете:
- Error conditions: Что если
TrailRepository::createвернетDbErr::ConnectionAcquire? - Edge cases: Что если
user_idотрицательный? (SeaORM принимает i64, но должна валидировать) - Concurrency: Что если 1000 потоков вызывают
toggle_trail_likeодновременно?
Техдолг: У вас нет property-based testing (proptest) и нет fuzzing (cargo-fuzz). Вы не нашли race conditions и integer overflow bugs, которые 100% есть в коде.
15. DTO Validation: Validate на уровне HTTP, а не домена
Ваша валидация в domain DTO — это security theater. Почему?
#[derive(Validate)]
pub struct CreateTrailRequest {
#[validate(length(min = 3, max = 200))]
pub title: String,
}
Эта валидация не вызывается автоматически. Она вызывается в extractor. Если кто-то создаст Trail напрямую через api-core, минуя HTTP — валидация пропущена.
Правильно: Валидация должна быть в NewTrail domain entity, и она должна быть infallible constructor (конструктор, который не позволит создать невалидный объект).
Техдолг: У вас type safety holes. Невалидные данные могут просочиться в БД через прямой вызов api-core.
Финальная правда: Это хороший прототип, но не production system
Ваш проект — MVP с правильной архитектурной формой, но без production-grade деталей. Он выживет до первого серьезного инцидента:
- До первого DDoS — упадет, потому что нет rate limiting
- До первого data corruption — потеряете данные, потому что нет транзакций
- До первого security breach — скомпрометируете API keys, потому что нет audit log
- До первого performance regression — не поймете, что сломали, потому что нет метрик
Что делать сейчас (порядок приоритета):
- Удалите
sqlxиз workspace — используйте только SeaORM - Добавьте
TransactionTraitво все use-кейсы вapi-core - Напишите 10 unit тестов на
TrailServiceсMockTrailRepository— вы поймете, где у вас logic gaps - Запустите
cargo auditи пофиксите critical уязвимости (их будет 5-10) - Добавьте один rate limiter в
api_key_middleware— поймете, насколько ваш design fragile
Это не "у вас всё плохо". Это "у вас всё правильно начато, но не закончено".