Недавно я сделал общедоступным планировщик в памяти, написанный на 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.