← Назад к вопросам

Что такое ошибка на миллион?

1.0 Junior🔥 71 комментариев
#Другое

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Ошибка на миллион (One in a Million Bug) в разработке

Что это такое

«Ошибка на миллион» (One in a Million Bug, OIMB) — это критическая ошибка, которая:

  • Проявляется очень редко, практически невоспроизводима
  • Происходит при очень специфичных условиях
  • Может быть вызвана одновременным стечением обстоятельств
  • Зачастую связана с race conditions, timing issues, состоянием системы
  • Трудна для диагностики и воспроизведения
  • Вызывает критические сбои при редком срабатывании

Примеры в Flutter/Dart

Пример 1: Race condition в WebSocket синхронизации

class DataService {
  final _dataController = StreamController<List<User>>();
  late WebSocketChannel _ws;
  
  // ПЛОХО - race condition
  Future<void> loadData() async {
    final data = await fetchFromServer(); // Запрос 1
    _dataController.add(data); // Добавляем в стрим
  }
  
  Future<void> subscribeToUpdates() async {
    _ws.stream.listen((message) {
      final update = parseUpdate(message);
      _dataController.add(update); // Может прийти раньше loadData
    });
  }
  
  // Сценарий ошибки на миллион:
  // 1. Пользователь открыл экран
  // 2. Одновременно: loadData() отправляет запрос И приходит WebSocket update
  // 3. Update обработан ДО получения ответа от loadData
  // 4. Потом приходит старая версия с сервера и перезаписывает новые данные
  // 5. UI показывает устаревшие данные
  
  void init() {
    loadData();
    subscribeToUpdates();
  }
}

Хороший подход:

class ImprovedDataService {
  final _dataSubject = BehaviorSubject<List<User>>([]);
  late WebSocketChannel _ws;
  final _versionNumber = 0;
  
  // ХОРОШО - версионирование
  Future<void> loadData() async {
    final version = _versionNumber++;
    final data = await fetchFromServer();
    
    // Проверяем, не устарела ли версия
    if (version == _versionNumber - 1) {
      _dataSubject.add(data);
    }
  }
  
  Future<void> subscribeToUpdates() async {
    _ws.stream.listen((message) {
      final update = parseUpdate(message);
      final currentData = _dataSubject.value;
      
      // Мерджим данные интеллигентно, не перезаписываем
      final merged = _mergeUpdate(currentData, update);
      _dataSubject.add(merged);
    });
  }
  
  List<User> _mergeUpdate(List<User> current, Map<String, dynamic> update) {
    // Обновляем только измененные элементы
    return current.map((user) {
      if (user.id == update['userId']) {
        return user.copyWith(name: update['newName']);
      }
      return user;
    }).toList();
  }
}

Пример 2: Memory leak в обработке events

class BadEventHandler {
  final _eventController = StreamController<String>();
  late StreamSubscription _subscription;
  
  // ПЛОХО - утечка памяти при редких обстоятельствах
  void initialize() {
    _subscription = _eventController.stream.listen((event) {
      processEvent(event);
    });
    // Если dispose() никогда не вызывается из-за ошибки
    // подписка остается живой и удерживает память
  }
  
  void dispose() {
    _subscription.cancel(); // Может не вызваться!
    _eventController.close();
  }
}

// Сценарий ошибки на миллион:
// 1. Пользователь открывает экран (subscribe создается)
// 2. Происходит редкая ошибка в обработке события
// 3. Exception выбрасывается до dispose()
// 4. StreamSubscription остается живой
// 5. При открытии экрана еще раз - еще одна подписка
// 6. После много раз - memory leak, приложение замерзает

Правильный подход:

class GoodEventHandler with ChangeNotifier {
  final _eventController = StreamController<String>.broadcast();
  late StreamSubscription _subscription;
  
  void initialize() {
    _subscription = _eventController.stream.listen(
      (event) => processEvent(event),
      onError: (error) => handleError(error),
      cancelOnError: false,
    );
  }
  
  void processEvent(String event) {
    try {
      // Обработка
      notifyListeners();
    } catch (e) {
      handleError(e);
      // Не выбрасываем исключение, логируем и продолжаем
    }
  }
  
  @override
  void dispose() {
    _subscription.cancel();
    _eventController.close();
    super.dispose();
  }
}

Пример 3: Timing issue при инициализации

// ПЛОХО - зависит от timing
class BadInitialization {
  bool _initialized = false;
  late Database db;
  
  void init() async {
    db = await Database.open(); // async операция
    _initialized = true;
  }
  
  void saveData(String data) {
    // Эта ошибка срабатывает 1 из 1000000
    // если saveData() вызвана ДО завершения async инициализации
    if (!_initialized) {
      throw Exception('Database not initialized!');
    }
    db.insert(data); // NullPointerException если db == null
  }
}

// ХОРОШО - гарантированная инициализация
class GoodInitialization {
  late final Future<void> _initFuture;
  late Database db;
  
  GoodInitialization() {
    _initFuture = _initialize();
  }
  
  Future<void> _initialize() async {
    db = await Database.open();
  }
  
  Future<void> saveData(String data) async {
    // Гарантировано, что инициализация завершена
    await _initFuture;
    db.insert(data);
  }
}

Пример 4: Concurrency issue в BLoC

class BadCounterBloc extends Bloc<CounterEvent, CounterState> {
  int count = 0;
  
  BadCounterBloc() : super(CounterInitial()) {
    on<IncrementCounter>((event, emit) async {
      // ОШИБКА НА МИЛЛИОН: если два события придут одновременно
      count++; // Не thread-safe!
      await Future.delayed(Duration(milliseconds: 100));
      emit(CounterUpdated(count));
    });
  }
  
  // Сценарий:
  // 1. Пользователь дважды быстро кликает кнопку
  // 2. Event 1: count = 1, emit(CounterUpdated(1))
  // 3. Event 2 одновременно: count = 1, emit(CounterUpdated(1))
  // 4. UI показывает 1 вместо 2
}

// ХОРОШО - state-based подход
class GoodCounterBloc extends Bloc<CounterEvent, CounterState> {
  GoodCounterBloc() : super(const CounterInitial()) {
    on<IncrementCounter>((event, emit) {
      final currentState = state;
      if (currentState is CounterUpdated) {
        // Используем текущее состояние, не переменную!
        emit(CounterUpdated(currentState.count + 1));
      }
    });
  }
}

Как диагностировать ошибку на миллион

1. Логирование стека вызовов

void logWithStackTrace(String message) {
  print('$message');
  print(StackTrace.current);
}

try {
  riskOperation();
} catch (e, stackTrace) {
  logWithStackTrace('Error: $e');
  print('Stack: $stackTrace');
}

2. Stress testing

import 'package:test/test.dart';

void main() {
  test('Stress test for race conditions', () async {
    final service = DataService();
    
    // Запустить 1000 операций одновременно
    final futures = List.generate(
      1000,
      (_) => service.loadData(),
    );
    
    await Future.wait(futures);
    
    // Проверить корректность
    expect(service.data.length, equals(expectedLength));
  });
}

3. Профилирование памяти

import 'dart:developer';

void checkMemory() {
  final info = await Service.getVM();
  final memoryUsage = info.memoryUsage;
  
  print('Memory: ${memoryUsage.heapUsage}');
  
  if (memoryUsage.heapUsage > threshold) {
    // Возможна утечка памяти
    Timeline.instantSync('Memory Spike', arguments: {
      'heap': memoryUsage.heapUsage,
    });
  }
}

Тестирование на ошибки миллиона

test('No race condition with concurrent operations', () async {
  final bloc = UserBloc(mockRepository);
  
  // Отправить события одновременно
  bloc.add(FetchUsers());
  bloc.add(RefreshUsers());
  bloc.add(UpdateUser(userId: 1));
  
  await expectLater(
    bloc.stream,
    emitsInOrder([
      isA<UserLoading>(),
      isA<UserLoaded>(),
    ]),
  );
});

test('No memory leaks on multiple cycles', () async {
  for (int i = 0; i < 1000; i++) {
    final screen = UserScreen();
    // Симулируем открытие/закрытие
  }
  // Проверить что память не растет
});

Лучшие практики для предотвращения

1. Используй immutable state

@immutable
class UserState {
  final List<User> users;
  final bool isLoading;
  UserState({required this.users, required this.isLoading});
}

2. Версионирование данных

class VersionedData<T> {
  final T value;
  final int version;
  VersionedData({required this.value, required this.version});
}

3. Explicit async/await

// Явно ждем инициализации
await initialization.complete;
await databaseReady;

4. Lock mechanisms для critical sections

final _lock = Mutex();

Future<void> criticalOperation() async {
  await _lock.lock();
  try {
    // Только один поток может исполнить
  } finally {
    _lock.unlock();
  }
}

Вывод

Ошибка на миллион — это не мистика, это результат:

  • Race conditions
  • Timing issues
  • State management problems
  • Неправильного управления памятью
  • Неполной инициализации

Предотвращение требует:

  • Правильной архитектуры
  • Immutable state
  • Explicit synchronization
  • Thorough testing
  • Профилирования
  • Логирования

Эти ошибки самые трудные, но их можно избежать через discipline в коде.