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.
🎙 Крафтовый Димарик №17 - Viskas Gerai*
Поездка в Милан и концерт группы Epica. Поездка в Берлин и концерт Сэма Смита. Веселье с общественным транспортом в Германии....
Дилемма об отношении к курьерам
Курьерам Деливери клаба, Яндекс.Еды, Яндекс.Лавки, Самоката и прочим пешевелосипедным трудягам зимой объективно тяжело работать ...
Не прокатило
Звонок программисту (П) от рекрутера (HR). HR: Ало, привет, я Маша из агентства "Олег и таланты", у меня есть классная вакансия...
Абстракции и наследование в Си - стреляем по ногам красиво
TL;DR Иногда нет-нет да и хочется что-нибудь абстрагировать и обобщить в коде на Си. К примеру, хочешь ты принтануть содержимо...
Я написал программу, которая сделала меня здоровее
Периодически из-за тонзиллита мне надо посещать лора примерно два раза в год: осмотреться, подлечиться, и всякое по мелочи. Про...
Медовый месяц в Беларуси: Дорога и первый день в Минске
До Жуковского аэропорта от Москвы можно доехать на Рутакси за косарь и час-полтора. Жуковский аэропорт - маленький, тихий, спок...