Что будешь делать если при передаче данных в NoSQL произошла ошибка?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Обработка ошибок при записи в NoSQL базы данных
Это вопрос о надежности и обработке edge cases, очень важный для production-систем. Расскажу мой подход к обработке ошибок в NoSQL (MongoDB, DynamoDB, Firestore).
Типы ошибок, которые могут возникнуть
1. Ошибки валидации:
// Документ не соответствует схеме
{
name: 123, // Должна быть строка
email: "invalid", // Невалидный email
}
2. Ошибки уникальности:
// Дублирование индекса
db.users.insert({ email: "test@example.com" })
db.users.insert({ email: "test@example.com" }) // Ошибка: duplicate key
3. Ошибки размера:
// Документ слишком большой (>16MB в MongoDB)
const hugeDoc = { data: Buffer.alloc(20 * 1024 * 1024) };
4. Ошибки соединения:
// База данных недоступна
// Network timeout
// Connection pool exhausted
5. Ошибки памяти:
// Out of memory на сервере БД
// Disk full
Мой подход: Многоуровневая обработка ошибок
Уровень 1: Валидация ДО записи
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
class UserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(3)
name: string;
}
async function createUser(userData: any) {
// Валидируем ДО отправки в БД
const userDto = plainToInstance(UserDto, userData);
const errors = await validate(userDto);
if (errors.length > 0) {
throw new BadRequestException({
message: 'Validation failed',
errors: errors.map(e => ({
field: e.property,
constraints: e.constraints,
})),
});
}
// Валидация прошла — записываем в БД
return db.users.insertOne(userDto);
}
Уровень 2: Try-Catch с специфической обработкой
import { MongoError } from 'mongodb';
async function insertDocument(collection: string, document: any) {
try {
const result = await db.collection(collection).insertOne(document);
return { success: true, insertedId: result.insertedId };
} catch (error) {
// Дублирование ключа (E11000 в MongoDB)
if (error.code === 11000) {
const field = Object.keys(error.keyPattern)[0];
throw new ConflictException(
`${field} must be unique. Value "${document[field]}" already exists.`
);
}
// Документ слишком большой
if (error.code === 13) {
throw new BadRequestException(
'Document size exceeds maximum allowed (16MB)'
);
}
// Ошибка соединения
if (error.name === 'MongoNetworkError') {
throw new ServiceUnavailableException(
'Database temporarily unavailable. Please try again.'
);
}
// Неизвестная ошибка
logger.error('Database insert error:', error);
throw new InternalServerErrorException(
'Failed to save data. Please try again later.'
);
}
}
Retry механизм с exponential backoff
import pRetry from 'p-retry';
async function insertWithRetry(
collection: string,
document: any,
maxRetries = 3
) {
return pRetry(
async () => {
return db.collection(collection).insertOne(document);
},
{
retries: maxRetries,
onFailedAttempt: (error) => {
logger.warn(
`Insert attempt ${error.attemptNumber} failed. ` +
`${error.retriesLeft} retries left. ` +
`Error: ${error.message}`
);
},
// Exponential backoff: 100ms, 200ms, 400ms
minTimeout: 100,
maxTimeout: 1000,
factor: 2,
}
);
}
Когда НЕ нужно переить:
- Ошибки валидации (422)
- Дублирование уникального ключа (409)
- Недостаточно памяти/прав (413, 403)
Когда НУЖНО переити:
- Network timeout
- Временная недоступность БД
- Race conditions в distributed системе
Асинхронная очередь для надежности
Для критичных данных, которые нельзя потерять, используем очередь (RabbitMQ, Redis, Kafka):
import Bull from 'bull';
const writeQueue = new Bull('database-writes');
// Пользователь отправляет данные
async function handleUserSubmission(data: any) {
// Сразу добавляем в очередь (быстро)
await writeQueue.add({
action: 'insertUser',
data,
});
// Сразу отвечаем клиенту (не ждем БД)
return { status: 'accepted', id: uuid() };
}
// Worker обрабатывает очередь
writeQueue.process(async (job) => {
const { action, data } = job.data;
if (action === 'insertUser') {
try {
const result = await db.users.insertOne(data);
return { success: true, insertedId: result.insertedId };
} catch (error) {
// Если ошибка — job переит автоматически
if (isRetryable(error)) {
throw error; // Bull переит
} else {
// Если ошибка не переитается — логируем и бросаем alert
logger.error('Non-retryable error in queue:', error);
await sendAlert({
type: 'database-error',
jobId: job.id,
error: error.message,
});
throw error;
}
}
}
});
// Обработчик неудачных jobs
writeQueue.on('failed', (job, err) => {
logger.error(`Job ${job.id} failed after all retries:`, err);
// Могли отправить в Dead Letter Queue
deadLetterQueue.add(job.data);
});
Транзакции для операций с несколькими документами
// MongoDB транзакция
async function transferBalance(
fromUserId: string,
toUserId: string,
amount: number
) {
const session = db.startSession();
try {
await session.withTransaction(async () => {
// Вычитаем у отправителя
const fromResult = await db.users.findOneAndUpdate(
{ _id: fromUserId, balance: { $gte: amount } },
{ $inc: { balance: -amount } },
{ session }
);
if (!fromResult.value) {
throw new BadRequestException('Insufficient balance');
}
// Добавляем получателю
await db.users.findOneAndUpdate(
{ _id: toUserId },
{ $inc: { balance: amount } },
{ session }
);
// Логируем транзакцию
await db.transactions.insertOne(
{
from: fromUserId,
to: toUserId,
amount,
timestamp: new Date(),
status: 'completed',
},
{ session }
);
});
} catch (error) {
logger.error('Transaction failed:', error);
throw error;
} finally {
await session.endSession();
}
}
Мониторинг и алертинг
// Отслеживаем ошибки БД
const mongooseConnection = mongoose.connection;
mongooseConnection.on('error', (error) => {
logger.error('MongoDB connection error:', error);
sendAlert({
type: 'database-error',
severity: 'critical',
message: error.message,
});
});
// Метрики для мониторинга
const dbErrorCounter = new Counter({
name: 'db_errors_total',
help: 'Total database errors',
labelNames: ['error_code', 'collection'],
});
async function insertWithMonitoring(collection, document) {
try {
return await db.collection(collection).insertOne(document);
} catch (error) {
dbErrorCounter.labels(error.code, collection).inc();
throw error;
}
}
Чеклист для обработки ошибок NoSQL
- Валидация входных данных — перед отправкой в БД
- Специфическая обработка ошибок — разные коды ошибок обрабатываются по-разному
- Retry логика — с exponential backoff
- Очереди — для критичных данных
- Транзакции — для multi-document операций
- Мониторинг — логирование и алертинг
- Graceful degradation — система продолжает работать при ошибке
- User feedback — понятные сообщения об ошибках
- Logging — достаточно информации для debug
- Rate limiting — предотвращение DDoS на БД
Пример: Полный production-ready контроллер
@Post('users')
async createUser(@Body() createUserDto: CreateUserDto) {
// 1. Валидация
const errors = await validate(createUserDto);
if (errors.length > 0) {
throw new BadRequestException(errors);
}
// 2. Попытка с retry
try {
const user = await this.userService.createUserWithRetry(createUserDto);
return { success: true, user };
} catch (error) {
// 3. Специфическая обработка
if (error.code === 11000) {
throw new ConflictException('Email already exists');
}
// 4. Логирование и alert
this.logger.error('Failed to create user:', error);
await this.alertService.notifyAdmins({ error });
// 5. Graceful response
throw new InternalServerErrorException(
'Failed to create user. Please try again later.'
);
}
}
Итог: Обработка ошибок NoSQL — это не просто try-catch, а многоуровневая система с валидацией, retry логикой, очередями, мониторингом и правильными HTTP ответами. Это разница между "работающей" системой и production-ready системой.