Багло негаданно найдется, там где его совсем не ждешь

Иногда натыкаешься на программные ошибки, в местах где их меньше всего ожидаешь увидеть: в простом коде, который не менялся вот уже много лет и активно используется в разных частях программы.

Исправление таких “невозможных” ошибок обычно возможно только в случае особого стечения обстоятельств.

После обнаружения этой ошибки эмоции меня переполняли около суток.

Описание мигающего теста

А началось все с того, что у меня в проекте https://github.com/bozaro/git-as-svn мигал тест.

Этот тест относился к числу тех тестов, которые проверяли, что моя реализация Subversion-сервера совпадает по поведению с эталонной. В качестве эталонов использовались родной svnserve и SvnKit.

Данные тесты выполнялись по три раза:

  • с родным svnserve;
  • с SvnKit;
  • с Git as Svn.

При этом суть теста предельна проста:

# создается тестовый репозиторий; # берется svn lock на файл (блокировка_1); # снимается с файла блокировка_1; # берется svn lock на файл (блокировка_2); # снимается с файла блокировка_1 - тут ожидается ошибка о попытке снять уже несуществующую блокировку.

Мигала реализация с SvnKit: иногда удавалось снять ранее снятую блокировку без ошибок.

Ловля ошибки

Обычно мигание теста связано с состоянием гонки и ищется достаточно просто:

  • ставится breakpoint на место, где можно определить некорректное поведение;
  • тест запускается в цикле;
  • как только остановились на breakpoint-е, пытаемся выяснить, что пошло не так.

Я знал об этой ошибке, так как она регулярно проявляла себя на Travis-е, но не исправил её, так как она локально у меня не воспроизводилась.

В этот день она первый раз проявила себя на локальной машине и я начал охоту за ней.

В результате выяснилось: внезапно, обе блокировки иногда имеют одинаковый идентификатор.

Я начал смотреть, как этот идентификатор формируется и узрел простой вызов метода SVNUUIDGenerator.generateUUID(). То есть, получается, что два раза генерируется одинаковый GUID.

Далее я начал методом пристального взгляда изучать код генерации GUID-ов.

Суть проблемы оказалась следующая:

  • GUID получается из 2-х частей:
    • идентификатора запуска приложения (для нас - константа);
    • текущего времени, которое получается через SVNUUIDGenerator.getCurrentTime().
  • Метод SVNUUIDGenerator.getCurrentTime() должен всегда возвращать уникальное в рамках процесса значение не меньше текущего времени.
  • Для обеспечения уникальности значения было две переменных:
    • ourLastGeneratedTime - время последнего вызова;
    • ourFudgeFactor - сдвиг значения.

Сам метод:

    private static long getCurrentTime() {
        long currentTime = System.currentTimeMillis();
        /* if clock reading changed since last UUID generated... */
        if (ourLastGeneratedTime != currentTime) {
            /*
             * The clock reading has changed since the last UUID was generated.
             * Reset the fudge factor. if we are generating them too fast, then
             * the fudge may need to be reset to something greater than zero.
             */
            if (ourLastGeneratedTime + ourFudgeFactor > currentTime) { // <== BUG HERE
                ourFudgeFactor = ourLastGeneratedTime + ourFudgeFactor - currentTime + 1;
            } else {
                ourFudgeFactor = 0;
            }
            ourLastGeneratedTime = currentTime;
        } else {
            /* We generated two really fast. Bump the fudge factor. */
            ++ourFudgeFactor;
        }
        return currentTime + ourFudgeFactor;
    }

Исправление при этом выглядит очень просто:

diff --git a/svnkit/src/main/java/org/tmatesoft/svn/core/internal/util/SVNUUIDGenerator.java b/svnkit/src/main/java/org/tmatesoft/svn/core/internal/util/SVNUUIDGenerator.java
index ac13a02..228ff74 100644
--- a/svnkit/src/main/java/org/tmatesoft/svn/core/internal/util/SVNUUIDGenerator.java
+++ b/svnkit/src/main/java/org/tmatesoft/svn/core/internal/util/SVNUUIDGenerator.java
@@ -48,7 +48,7 @@ public class SVNUUIDGenerator {
              * Reset the fudge factor. if we are generating them too fast, then
              * the fudge may need to be reset to something greater than zero.
              */
-            if (ourLastGeneratedTime + ourFudgeFactor > currentTime) {
+            if (ourLastGeneratedTime + ourFudgeFactor >= currentTime) {
                 ourFudgeFactor = ourLastGeneratedTime + ourFudgeFactor - currentTime + 1;
             } else {
                 ourFudgeFactor = 0;

Последовательность событий, при которой воспроизводится проблема

Из-за ошибки знака, ошибка воспроизводилась примерно в следующей ситуации:

Первая генерация:

До вызова:

System.currentTimeMillis() == 42;
ourLastGeneratedTime = 0;
ourFudgeFactor = 0;

После вызова:

ourLastGeneratedTime = 42;
ourFudgeFactor = 0;

Результат: 42

Вторая генерация

До вызова:

System.currentTimeMillis() == 42;
ourLastGeneratedTime = 42;
ourFudgeFactor = 0;

После вызова:

ourLastGeneratedTime = 42;
ourFudgeFactor = 1;

Результат: 43

Третья генерация До вызова:

System.currentTimeMillis() == 43;
ourLastGeneratedTime = 42;
ourFudgeFactor = 1;

После вызова:

ourLastGeneratedTime = 43;
ourFudgeFactor = 0;

Результат: 43, но это значение уже было!

Итог

По результатам раскопок был заведен баг: https://issues.tmatesoft.com/issue/SVNKIT-608

Исправление попадает в SvnKit начиная с версии 1.8.11.

comments powered by Disqus