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.
Я начал делать приложение для учета денег. Зачем?
Примерно с 2014 года я вел учет денег в Google Spreadsheets. Происходило это всегда так: 2-3 раза в неделю я садился за компьюте...
🎙 Крафтовый Димарик №5 - затир с Юлей за жизнь
Теплая беседа в выходной день: сидим на кухне, пьем чай, бесстыдно смеемся, плохо шутим, перебиваем друг друга и много говорим п...
🎙 Крафтовый Димарик №2 - сериальчики
0:00 - Нам пишут 1:38 - Название подкаста 1:53 - К делу 2:49 - В ее глазах 4:00 - Сквозь снег 6:12 - Хороший доктор 8:11 -...
Семнадцать мгновений весны
Неожиданно хорошее кино. Я думал, всё будет старым, неинтересным и тягомотным, и, в принципе, первые 1-2 серии примерно так и ощ...
Всегда жалуйтесь
На днях решили заказать на обед грузинской еды в Bolt Food. Нашли неплохой ресторан, понабрали на 40 евро, заказали, ждем. Доста...
Airplane! (1980)
Посмотрел вчера фильм Airplane! (1980), это одна из тех старых комедий, где люди каламбурят на серьезных щах, создавая абсурдн...