Вот история ошибки, которую я недавно вызвал, нашел и исправил. Это не особенно сложно или сложно, и это не займет много времени, чтобы найти и исправить. Тем не менее, это научило меня некоторым хорошим урокам.
ОШИБКА
На работе одна из наших систем используется операторами мобильной связи для доставки текстовых сообщений (SMS). Это довольно сложная система, которая обычно обрабатывает сотни или тысячи сообщений в секунду. Недавно мы улучшили часть системы, которая обрабатывает составные SMS (сообщения, которые доставляются в нескольких частях). До этой новой функции все объединенные SMS-сообщения сохранялись в базе данных, а затем собирались после небольшой задержки и доставлялись. Это было сделано для того, чтобы все детали были доставлены в правильном порядке, так как детали могут поступать не в порядке, и, кроме того, детали могут доставляться в разные узлы.
Новая функция состояла в том, чтобы собирать детали в памяти, а не в базе данных, чтобы увеличить емкость. Все части данного сообщения собираются в определенном узле, что определяется алгоритмом хеширования по номеру телефона получателя. Новая функция работала, как и ожидалось, но некоторое время назад мы начали получать сообщения об ошибках, связанных с отказами в доставке составных сообщений. В отчеты об ошибках включена трассировка стека, показывающая исключение ArrayIndexOutOfBoundsException . Проблема возникла в той части кода, которую я написал, где хеширование номера телефона показывает, какой узел должен обрабатывать все части данного сообщения. Это довольно просто — просто возьмите хеш-код по модулю количества сегментов, чтобы найти узел:
int getBucket(Digits destMsisdn) { return destMsisdn.hashCode() % numberOfBuckets; }
Возникло исключение, потому что индекс был -3. Это было загадочно, поскольку функция работала корректно почти во всех случаях.
TRACE
Исключением является информация о том, что индекс равен -3. Это лучше, чем множество других исключений Java, но не так хорошо, как могло бы быть. Если бы индекс был слишком высоким (то есть 4 для массива длиной 4), вы не знали бы из исключения, насколько велик массив. Хорошее исключение или сообщение об ошибке должно содержать как можно больше динамической информации . Для ArrayIndexOutOfBoundsException это означает, что включает в себя как ошибочный индекс, так и допустимый диапазон массива . К сожалению, не вся динамическая информация является очень распространенной ошибкой для многих исключений Java.
Трассировка стека точно показывает, где что-то идет не так, но не показывает, почему . Почему индекс был отрицательным? Вы не можете сказать, просто посмотрев на трассировку стека. Вам также необходимо знать, над какими данными работает алгоритм. К счастью, в нашей системе у нас есть возможность использовать Trace on Error . Это форма ведения журнала на основе сеанса (называемая трассировкой на работе, чтобы отличить ее от традиционной регистрации).
Трассировка, включенная в отчет об ошибке, показала полное полученное сообщение, а также все ключевые шаги, предпринятые до появления ошибки. Среди прочего, в него включен пункт назначения MSISDN (номер телефона).
ТЕСТ
Есть много шагов при обработке составных сообщений новым способом. Получать входящие детали, находить, какой узел должен собрать все детали, собирать детали, доставлять все детали, когда прибывает последняя часть, время ожидания, если не все детали получены и т. Д. Если логика вообще сложная, мне нравится разбивать ее на отдельные тестируемые методы , и модульное тестирование частей в изоляции. Таким образом, когда я собираю все это вместе, по крайней мере, я знаю, что каждая часть делает то, что должна делать.
В этом случае у меня были модульные тесты, которые тестировали 4 разных телефонных номера в трех случаях: только 1 корзина, 2 корзины и 1000 корзин:
public void testGetBucket() { ConcatInMemoryHandler handler; Digits destMsisdn1 = new Digits("46702000021"); Digits destMsisdn2 = new Digits("6"); Digits destMsisdn3 = new Digits("4678900112"); // Only 1 bucket - all buckets should be 0 handler = new ConcatInMemoryHandler(1, 1, UUID.randomUUID(), UUID.randomUUID(), user); assertEquals(0, handler.getBucket(destMsisdn1)); assertEquals(0, handler.getBucket(destMsisdn2)); assertEquals(0, handler.getBucket(destMsisdn3)); // 2 buckets handler = new ConcatInMemoryHandler(2, 1, UUID.randomUUID(), UUID.randomUUID(), user); assertEquals(1, handler.getBucket(destMsisdn1)); assertEquals(0, handler.getBucket(destMsisdn2)); assertEquals(0, handler.getBucket(destMsisdn3)); // 1000 buckets handler = new ConcatInMemoryHandler(1000, 1, UUID.randomUUID(), UUID.randomUUID(), user); assertEquals(465, handler.getBucket(destMsisdn1)); assertEquals(736, handler.getBucket(destMsisdn2)); assertEquals(378, handler.getBucket(destMsisdn3)); }
Первым делом я вставил номер телефона из трассировки в тестовый модуль. И вот, я получил то же исключение. Теперь было легко найти причину проблемы: метод hashCode может (конечно) возвращать как отрицательные, так и положительные значения. Несмотря на то, что я тестировал несколько разных телефонных номеров, я не выбрал достаточно большой, чтобы метод hashCode возвращал отрицательное значение. Номер телефона из трассировки был 543484900001 (слегка изменен, чтобы не показывать реальный номер абонента), и он был достаточно большим, чтобы сгенерировать отрицательный хэш-код.
Исправление казалось очевидным — просто возьмите абсолютное значение (изменив отрицательное значение на соответствующее положительное значение). Однако произошел поворот.
Твист
Давным-давно я прочитал замечательную статью о том, как исправить ошибку: три вопроса о каждой найденной ошибке от Тома Ван Флека. Когда вы найдете и исправите ошибку, вы должны задать себе 3 вопроса:
- Эта ошибка также где-то еще?
- Какая следующая ошибка скрыта за этой?
- Что я должен сделать, чтобы предотвратить подобные ошибки?
Я всегда стараюсь задавать эти вопросы всякий раз, когда исправляю ошибку, и в этом случае вариант вопроса 2 заставил меня увидеть проблему с моей первой попыткой решения:
int getBucket(Digits destMsisdn) { return Math.abs(destMsisdn.hashCode()) % numberOfBuckets; }
Недавно мы обсуждали Integer.MIN_VALUE и Integer.MAX_VALUE на работе. Одним из свойств двоичной кодировки является то, что существует еще одно отрицательное значение int, чем положительные значения int . Поэтому мне было интересно, что произойдет, если вы возьмете абсолютное значение Integer.MIN_VALUE . Быстрая проверка показала, что он возвращает то же значение. Так что есть один (очень необычный случай), когда абсолютное значение Java int является отрицательным . Одним из решений было бы явным образом проверить этот случай и позволить getBucket () вернуть ноль вместо этого в этом случае. Однако вместо этого я выбрал это решение (и это тот случай, который заслуживает комментария):
int getBucket(Digits destMsisdn) { // hashCode() can give negative values. Math.abs() gives // negative value for Integer.MIN_VALUE, so important // to do Math.abs() *after* taking the remainder. return Math.abs(destMsisdn.hashCode() % numberOfBuckets); }
Я также добавил номер телефона в качестве четвертого номера телефона в модульном тесте.
УРОКИ ВЫУЧЕНЫ
Итак, что я узнал из этой ошибки . Несколько вещей:
Метод hashCode может возвращать отрицательные значения . В большинстве случаев это не имеет значения. Но поскольку я использовал хеш-код для выбора индекса массива, я думал только о значении как о беззнаковом. Хотя я знаю, что целые числа могут быть как положительными, так и отрицательными, в этом контексте я был слеп к этому.
След делает стрельбу проблемы намного легче. Существует большая разница между наличием только трассировки стека и доступностью всех данных сеанса при попытке выяснить, что произошло.
Модульные тесты помогают, но не все ловят . Тщательно продуманный код и модульные тесты позволяют быстро и легко проверить гипотезу и проверить исправление. Но ошибки все еще проходят. Следовательно, вам также необходимо иметь систему, которая легко устраняет неполадки.
Всегда задавайте 3 вопроса. Ошибка может быть больше, чем вы думаете, поэтому обязательно используйте 3 вопроса, чтобы увидеть больше случаев.
Неплохо для одной ошибки!