Недавно я сделал общедоступным планировщик в памяти, написанный на Golang, https://gitlab.com/kylehqcom/kevin.
Написание кода для публичного использования всегда пугало. Прекрасно зная о критике, тщательном изучении и потирании подбородка, которые исходят от ваших сверстников и не сверстников. Но, будучи оптимистом, я ценю любые отзывы. Хорошо это или плохо, это всегда хорошо воспринимается, так как все это просто помогает в обучении. Моя цель здесь — поделиться некоторыми из тех уроков, ошибок и «ха-ха» моментов, которые я обнаружил при разработке с помощью Go.
Долго это может продолжаться!
Задний план
В свободное время я писал новый SaaS. В настоящее время это закрыто, но подробности появятся по мере выхода бета-версии. Сам проект состоит из 3 отдельных частей: геттера, измельчителя и отправителя. Начав с «получателя», довольно рано стало очевидно, что мне понадобится способ планирования «получения». Но такой планировщик очень пригодился бы позже и с моим «отправителем». Сначала я взглянул на существующие пакеты с открытым исходным кодом.
Из того, что я нашел, большинство из них были излишними для того, что мне было нужно. Конечно, хронос AirBnB и даже урезанная версия кала справились бы со своей задачей, но в настоящее время у меня слишком много накладных расходов.
На другом конце шкалы большинство из них выбрали прямой подход к работе с Linux cron. gocron, безусловно, будет повторять задачи, но у него не было удобного способа манипулировать запущенными задачами или извлекать детали из этих задач для самоанализа. Именно в этот момент я черпал вдохновение из вышесказанного и закатал рукава. Для справки взгляните на источник Кевина.
Горутины и каналы
Поскольку мои собственные требования не требовали постоянного хранилища для отслеживания запущенных заданий, реализовать пул расписаний было так же просто, как создать ключ сопоставления в строке.
type schedules map[string]*schedule
Это позволяло достаточно легко возвращать расписание на основе общего имени. При добавлении задания в расписание после проверки детали задания отправляются в неблокирующую *Горутину. Это гарантировало, что любой окружающий код может продолжать выполняться, и задание будет успешно выполняться в фоновом режиме. При отправке, если идентификатор не был передан, возвращался сгенерированный идентификатор для проверки задания в будущем. Сам бегун заданий использует встроенный в Golang
time.NewTicker(time.Duration)
для повторения фактических вызовов функций, назначенных заданию. Так что ничего страшного или из ряда вон выходящего здесь не происходит. Ну, это было до тех пор, пока я не захотел использовать идентификатор, чтобы остановить запущенное задание. Хотя в то время мои «знания» о проблеме были немного недостаточными, я был вполне уверен, что смогу получить желаемый результат.
Вопрос заключался в том, как я могу получить доступ к значениям функции, работающей в фоновом режиме Goroutine?
Насколько я понял «в то время», каждая горутина живет в какой-то «запретной зоне» вдали от переменной области видимости. Это оставило меня с одним вариантом, используя несколько каналов с благими намерениями. Каналы (среди прочего) — это способ общения горутин друг с другом. Но как мне использовать **стоп-канал для пула расписаний с многочисленными заданиями??? Интернет — ваш друг.
После просмотра многочисленных результатов Stackoverflow на моем лице появилась ржаная улыбка, когда я, наконец, нашел свой путь к @matreyer — ранее в этом году я связывался с Мэтом по поводу позиции GoLang, поэтому я не был удивлен, обнаружив его репозиторий для обработки моего точного варианта использования https://github.com/matryer/runner Бац, счастливых дней! Теперь у меня был способ остановить горутину изнутри.
Но, как и в большинстве случаев, я не был полностью доволен этой первоначальной реализацией. Я столкнулся с проблемой продолжительности между вызовом остановки извне и проверкой таймера задания на статус остановки. Короче говоря, тикер — это бесконечный «цикл for», в котором вы «вырываетесь» из или останавливаете().
// do is called via Run in a go routine.
func do(r *runner) {
// This will tick on "Every" forever.
e := time.NewTicker(r.job.Every)
for range e.C {
// Work happens
// But you can stop via a channel and
// break from the timer.
if <- stop {
break
}
Возьмем, к примеру, работу, которая получает значение «Каждая» в одну неделю. Если мы находимся в один день недели и на работе объявляется стоп, бегун будет сидеть и ждать еще 6 дней, прежде чем тикер тикает и останавливается. Я не хотел этого ожидания, поэтому решил найти решение.
Любопытство к победе.
После нескольких часов исследований, попыток кода, горутины и примеров каналов я «случайно» наткнулся на этот пример игровой площадки. Пожалуйста, посмотрите.
https://play.golang.org/p/FZKVATjcu0
Из этого примера видно, что горутины по-прежнему имеют доступ к переменным в области пакета. Обратите внимание, что var x int
обновляется одной горутиной, а затем рендерится из другой. Теперь, когда я знаю, что горутины могут обращаться к переменной в области видимости, можно ли вместо этого заменить переменную вне области видимости указателем?
Указатель — это адрес памяти на переменную. В прошлом коде я использовал указатели для оптимизации памяти или для обеспечения единой точки правды, я просто не применял те же мысли к горутинам. Вот я сижу и думаю, если горутина имеет доступ к указателю переменной из цикла тикера, то зачем нам вообще нужны эти каналы? Ответ: нет!
Моя текущая реализация использует указатель экземпляра бегуна https://gitlab.com/kylehqcom/kevin/blob/master/runner.go#L110, и экземпляр бегуна назначается карте расписания через JobID. Это гарантирует, что любые обновления флага остановки на экземпляре бегуна могут быть прочитаны тикером и будут остановлены/прерваны по мере необходимости. Победа!
Как бы мне ни нравилось внедрять пакет @matreyer, гораздо приятнее избавиться от внешней зависимости!
Урок
Каналы и горутины — это круто и весело, но используйте их только при необходимости. Часто можно найти более простое решение. Даже без пакета Mat’s runner я все еще привязан к остановке тикера на основе его продолжительности Every. Но я рад, что нашел лучшее решение. Теперь у меня есть более глубокое понимание, и мой Голанг только что выровнялся.
* В Интернете есть множество ресурсов о горутинах, поэтому, пожалуйста, «пойдите» и прочитайте, если они для вас новы.
** Интересно, что я все еще мог бы использовать один канал для отправки JobID. Хотя, как я сейчас понимаю, это будет означать, что каждый бегун будет получать ВСЕ обновления работы. Значения JobID будут отправлены по каналу, и бегуны должны будут самостоятельно проверить себя на соответствие JobID. Ужасно неэффективно.
Первоначально опубликовано на www.kylehq.com.