Fixing precompilation hangs due to open tasks or IO

На Julia 1.10 или выше вы можете увидеть следующее сообщение:

Скриншот зависания при препроцессинге

Это может повторяться. Если это продолжает повторяться без намеков на то, что это разрешится само собой, у вас может быть "зависание предварительной компиляции", которое требует исправления. Даже если это временно, вы можете предпочесть решить эту проблему, чтобы пользователи не беспокоились об этом предупреждении. Эта страница проведет вас через процесс анализа и исправления таких проблем.

Если вы следуете совету и нажимаете Ctrl-C, вы можете увидеть

^C Interrupted: Exiting precompilation...

  1 dependency had warnings during precompilation:
┌ Test1 [ac89d554-e2ba-40bc-bc5c-de68b658c982]
│  [pid 2745] waiting for IO to finish:
│   Handle type        uv_handle_t->data
│   timer              0x55580decd1e0->0x7f94c3a4c340

Это сообщение передает две ключевые информации:

  • зависание происходит во время предварительной компиляции Test1, зависимости Test2 (пакета, который мы пытались загрузить с помощью using Test2)
  • во время предварительной компиляции Test1 Julia создала объект Timer (используйте ?Timer, если вы не знакомы с таймерами), который все еще открыт; пока он не закроется, процесс завис.

Если этого достаточно, чтобы вы поняли, как создается timer = Timer(args...), одно хорошее решение — добавить wait(timer), если timer в конечном итоге завершится сам по себе, или close(timer), если вам нужно принудительно закрыть его, перед финальным end модуля.

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

  • Опция 1: Pkg.add("Aqua") и используйте Aqua.test_persistent_tasks. Это должно помочь вам определить, какой пакет вызывает проблему, после чего следует выполнить инструкции below. Если необходимо, вы можете создать PkgId как Base.PkgId(UUID("..."), "Test1"), где ... берется из записи uuid в Test1/Project.toml.
  • Вариант 2: вручную диагностировать источник зависания.

Чтобы вручную диагностировать:

  1. Pkg.develop("Test1")
  2. Закомментируйте весь код, включенный или определенный в Test1, кроме операторов using/import.
  3. Попробуйте снова using Test2 (или даже using Test1, предполагая, что это тоже зависает)

Теперь мы подошли к развилке: либо

Diagnosing and fixing hangs due to a package dependency

Используйте бинарный поиск, чтобы определить проблемную зависимость: начните с закомментирования половины ваших зависимостей, затем, когда вы изолируете, какая половина отвечает, закомментируйте половину этой половины и так далее. (Вам не нужно удалять их из проекта, просто закомментируйте операторы using/import.)

Как только вы идентифицировали подозреваемый пакет (здесь мы будем называть его ThePackageYouThinkIsCausingTheProblem), сначала попробуйте предварительно скомпилировать этот пакет. Если он также зависает во время предварительной компиляции, продолжайте искать проблему в обратном направлении.

Однако, скорее всего, ThePackageYouThinkIsCausingTheProblem будет предварительно скомпилирован нормально. Это предполагает, что проблема в функции ThePackageYouThinkIsCausingTheProblem.__init__, которая не выполняется во время предварительной компиляции ThePackageYouThinkIsCausingTheProblem, но выполняется в любом пакете, который загружает ThePackageYouThinkIsCausingTheProblem. Чтобы протестировать эту теорию, настройте минимальный рабочий пример (MWE), что-то вроде

(@v1.10) pkg> generate MWE
  Generating  project MWE:
    MWE\Project.toml
    MWE\src\MWE.jl

где исходный код MWE.jl находится

module MWE
using ThePackageYouThinkIsCausingTheProblem
end

и вы добавили ThePackageYouThinkIsCausingTheProblem в зависимости MWE.

Если этот минимальный рабочий пример воспроизводит зависание, вы нашли своего виновника: ThePackageYouThinkIsCausingTheProblem.__init__ должен создавать объект Timer. Если объект таймера можно безопасно закрыть, это хороший вариант. В противном случае наиболее распространенным решением является избегание создания таймера, пока любой пакет компилируется заранее: добавьте

ccall(:jl_generating_output, Cint, ()) == 1 && return nothing

как первая строка ThePackageYouThinkIsCausingTheProblem.__init__, и это позволит избежать инициализации в любом процессе Julia, целью которого является предварительная компиляция пакетов.

Fixing package code to avoid hangs

Поиск в вашем пакете на предмет suggestive слов (например, "Timer") и посмотрите, можете ли вы определить, где возникает проблема. Обратите внимание, что определение метода, как

maketimer() = Timer(timer -> println("hi"), 0; interval=1)

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

const GLOBAL_TIMER = maketimer()

или это может произойти в precompile workload.

Если вам трудно определить причинные строки, подумайте о том, чтобы выполнить бинарный поиск: закомментируйте разделы вашего пакета (или используйте include, чтобы исключить целые файлы) до тех пор, пока вы не сузите проблему.