Go: Отложенная обработка событий файловой системы
Допустим, вам нужно что-то сделать при возникновении какого-то события файловой системы. Например, перезапустить веб-сервер при изменении файлов. Довольно частая практика при разработке: повесить "слушателя" файловой системы, запустить приложение, сразу же после редактирования файлов "на лету" перекомпилировать проект и заново запустить его.
Этот сайт написан на Go, и недавно я решил добавить в него подобный hot-reload для markdown-файлов с постами: кладешь новый файл в папку, веб-сервер это замечает и переналивает внутренний in-memory-storage с публикациями без перезапуска самого себя. При этом мне хотелось именно "слушать" файловую систему, а не сканировать ее раз в несколько секунд.
Для этого уже написана хорошая библиотека fsnotify, которая умеет ловить события из каталогов. (К сожалению, нерекурсивно, но у меня этих каталогов не так много.)
В README приводится довольно понятный пример использования. Я обернул его в функцию Watcher()
и добавил туда свой канал, в который я что-то шлю, когда что-то происходит. Тип событий меня мало интересует, поэтому просто шлю всегда 1
. Функцию Watcher()
я обернул в другую функцию Watch()
, которая дергает переданную ей reload-функцию на каждом изменении.
Выглядело это примерно так (обработка ошибок и некоторые конструкции намеренно пропущены):
func main() {
dirs := []string[
"/foo",
"/bar",
]
go Watch(dirs, func() {
reloadStuff()
})
}
func Watch(dirs []string, reload func()) {
ch := make(chan int)
go watcher(dirs, ch)
// Выполнение reload-функции на каждом событии
// файловой системы
for range ch {
reload()
}
}
func Watcher(dirs []string, ch chan int) {
watcher, _ := fsnotify.NewWatcher()
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event, _ := <-watcher.Events:
// Шлем в наш канал уведомление
// о любом событии из watcher
ch <- 1
case err, _ := <-watcher.Errors:
log.Println("error:", err)
}
}
}()
for _, dir := range dirs {
watcher.Add(dir)
}
<-done
}
Я попробовал это запустить, погонял тесты и обнаружил, что при изменении файла может выстреливать больше одного события (например, последовательно CHMOD
и WRITE
). А если изменить несколько файлов одновременно (git checkout
, rsync
, touch *.*
), то их будет еще больше, и на каждое из них стриггерится мой hot-reload.
Мне же по сути надо триггернуть его только один раз, если за короткий промежуток времени пришло много событий. То есть накопить их, подождать полсекунды, и если больше ничего не пришло, сделать свое дело.
Признаюсь, самостоятельно я так и не смог придумать адекватное решение, но заметил, что CompileDaemon, который я как раз использую при разработке для перекомпиляции сорцов, работает ровно так, как я хочу. Решение оттуда вышло, скажем так, элегантное (насколько это может быть элегантным в Go), и заключается в использовании time.After(): она запускает таймер и по истечение указанного времени посылает текущее время в возвращаемый канал.
В итоге функция Watch()
претерпела следующие изменения:
func Watch(dirs []string, reload func()) {
ch := make(chan int)
go watcher(dirs, ch)
// Функция, возвращающая канал, в который приходит
// текущее время по окончании указанного временного
// интервала.
createThreshold := func() <-chan time.Time {
return time.After(time.Duration(500 * time.Millisecond))
}
// Можно сделать threshold := createThreshold(),
// если нужно триггернуть reload() при первом запуске.
// Мне такое не надо, поэтому достаточно пустого канала.
threshold := make(<-chan time.Time)
for {
select {
case <-ch:
// На каждом событии просто пересоздаем threshold,
// чтобы `case <-threshold` отложился еще на 500 мс.
threshold = createThreshold()
case <-threshold:
// Если в течение 500 мс в канал `ch` больше ничего
// не приходило, можно триггернуть reload и ждать
// следующего поступления событий.
reload()
}
}
}
Сработало ровно так, как мне хотелось, и теперь я могу без рестарта веб-сервера обновлять все файлы с данными и сам сайт через rsync
в течение 500 мс.
Я изобрел PHP.
Всегда жалуйтесь
На днях решили заказать на обед грузинской еды в Bolt Food. Нашли неплохой ресторан, понабрали на 40 евро, заказали, ждем. Доста...
Colon
В английском языке слово colon используется и для обозначения знака "двоеточие" : , и для обозначения толстого кишечника. Ка...
AI Shitstorm
Тут недавно прогремело несколько новостей: Slack тренирует свой AI на данных из корпоративных чатов; StackOverflow делает то ...
Docker Buildkit: Правильное использование --mount=type=cache
TL;DR Содержимое каталогов, смонтированных через --mount=type=cache , не сохраняется в docker-образе, поэтому кэшировать надо ...
Мне позвонил мошенник, и я ему надоел
Позвонил чувак, представился следователем МВД по экономическим преступлениям. Затирал про то, что вчера какая-то дама ("племянни...
Хорошо там, где нас нет
Иногда хочется поменять что-то в своей жизни: переехать в другой город, страну, сменить работу, род занятий, увлечение, религию....