Как стать автором
Обновить
32
0
Игорь Меньшенин @devalio

Разработчик программного обеспечения

Отправить сообщение

Ну ок, я не говорю, что этот вариант плохой. Он хорош. И да, я согласен, что можно не очень дотошно обрабатывать ошибку парсинга константы или игнорить вообще. Но я довольно консервативен в том, что касается читабельности и поддерживаемости кода. Поэтому я не люблю такие вещи как ORM или Query Builder (ни в коем случае не хочу уводить от темы или накидывать на вентилятор).

Но хочу прокомментировать ваш вариант использования sync.Once в конкретном кейсе инициализации. И вот как я рассуждаю:

Представим себе возможные варианты развития событий

  • Какой то мейнтейнер Вася добавляет новый функционал и по какой то причине он изменил ссылку в константе (может нашел апи получше). Вот он сидит и не понимает почему у него ничего не работает и чего не так с урлом (просто он много накодил и не понимает куда смотреть в первую очередь). Окей, его это не сильно заморочит (в конце концов он найдет причину), но все-таки причина не выпадет в лог, выпадет в лог только следствие.

  • Следующий мейнтейнер Петя чего-то добавляет и замечает sync.Once и думает, как же это удобно, можно сделать его функцию производительнее. А еще там ошибка не логируется (или логируется, но это уже будет бойлерпринт). Добавляет он свое туда и ошибки не обрабатывает тоже. А мейнтейнер Вася из первого кейса в следующий раз будет дольше страдать.

  • И еще один умный чел Дмитрий (будь как Дмитрий). Он добавляет в функцию возможность делать URL для разных эндпоинтов (ну просто переиспользует код). Видит, что там sync.Once и одно поле для хранения распаршеного урла, а ему нужно два. Он убирает нафиг все наши sync.Once и делает свое дело, а потом думает, ну пусть это работает на 1 милисекунду дольше, зато теперь читаемость лучше и никаких граблей. После этого Васе станет легче жить.

Учитывая все это я думаю, что sync.Once в данном случае хуже, чем вынести парсинг урла в Init и не париться. Хотя ни то ни другое не является плохим вариантом само по себе. И то и другое варианты подходящие, но лично мое мнение вот в этом комменте выше.

То чувство, когда поторопился в споре и зафейлился =)

А ведь подождал бы с отправкой 1 минуту и понял бы свою ошибку

Вам придется продумать вариант обработки ошибки err. Даже не смотря на то, что она маловероятна. Есть риск развития такой логики до такого состояния, что вам придется делать много сложных вещей внутри этого Do и это будет усложнять логику и читаемость экспоненциально. А какой Профит? Экономия 1 миллисекунды каждые 5-10 минут против простоты кода проигрывает на любых дистанциях.

Гораздо проще парсинг вынести в инит, но об этом я сам уже написал.

Ой, нет здесь я ошибся. это работать будет. И будет работать правильно. но есть одно но. Сначала ответьте правильно ли я понял предлагаемый код

вот этот код как раз будет работать безопасно, но он не выполняет функций, которые выполняет код моей функции. Вы теряете контекст, а поэтому тут не видно о чем я говорю. мы вобще пока общаемся в разных контекстах, но давайте я проясню...

func (tv *trudVsem) newVacanciesRequest(ctx context.Context, text string, offset, limit int) (*http.Request, error) {
    o.Do(func() {
      tv.URL, err := url.ParseRequestURI(serviceURL)
      if err != nil {
        // do something
      }
    })
    query := url.Values{
        "text":         []string{text},
        "offset":       []string{strconv.Itoa(offset)},
        "limit":        []string{strconv.Itoa(limit)},
        "modifiedFrom": []string{time.Now().Add(-time.Hour * 168).UTC().Format(time.RFC3339)},
    }
    tv.URL.RawQuery = query.Encode()
    return http.NewRequestWithContext(ctx, http.MethodGet, tv.URL.String(), http.NoBody)
}

Если вы такой код имели в виду, то вот оно работает не потокобезопасно. Если не такой - покажите свой вариант

Если once.Do вы оставите внутри рассматриваемой функции, тогда это будет работать только при условии, что запуск функции в разных рутинах не пересечется

А этого нельзя гарантировать полностью даже сейчас, не говоря уже о какой то долгосрочной перспективе

Если вы напишете код, я напишу вариант воспроизведения шагов race condition или есть вариант, что я вас неправильно понял.

Хороший примитив для реализации конкурентности в случае, если нам нужно что-то выполнить единожды, но тут совсем не то.

Попробуйте показать вариант реализации этой функции с использованием sync.Once без разделения логики. И при этом не забывайте о "состоянии гонки". Нам все равно при этом не обойтись без дополнительных полей + наверняка придется использовать еще и мьютексы.

Ну да, я об этом написал вот тут (может вышло не очень понятно):

Единственное, что можно было бы сделать — вынести парсинг константы отдельно, но этот участок кода настолько нетребователен к производительности, что разделять эту логику и выносить куда-то ее часть, я считаю нецелесообразным.

Но смысл в том, что мы отделяем маленькую часть от общей логики (создание URL реквеста) и тем самым ухудшаем чтение. Когда вам постоянно приходится бегать по коду глазами вверх-вниз, чтобы собрать в голове воедино кусочки чего то общего - это не очень правильно. А т.к. эта логика вообще не влияет на производительность, я не стал ее делить.

Выглядит так, как будто вам нужно что-то вроде инверсии зависимостей. Я бы не рекомендовал делать вот такие явные зависимости сервисов от сервисов.

Кроме того у вас не будет четкой гарантии в последовательности инициализации сервисов, даже не смотря на то, что они перечислены в массиве.

Давайте все же рассматривать инициализацию сервиса, как независимую стадию жизненного цикла. Т.е. не зависящую от состояния других частей системы. Это позволит нам избежать проблем, которые сложно решить.

Предположим, что у вас есть база данных, как сервис1 и сервис очередей, построенный на этой базе данных (да, такое бывает), как сервис2. Если в инициализации сервиса2 вам нужен рабочий коннект от сервиса1, но по какой то причине сервис1 еще не готов, то инициализация сервиса2 будет давать ошибку и все приложение остановится. Вы долго будете пытаться понять, в чем же дело и почему база данных не готова, прежде чем поймете, что она на самом деле готова, просто не вовремя. Да, пример простой и на самом деле в нем легко понять, что происходит, но проблему очень легко усложнить неправильными логами или другими bad-design практиками. И попробуйте потом отделить ошибку сервиса1 от ошибки сервиса2, когда пинг сервиса2 вернет error("service unavailable").

И все-же проблему необходимо решить. Я предлагаю вам два решения, можете выбрать любое:

  1. считаем что БД - это сервис для сервиса2, т.е. не показываем сервис1 в ServiceKeeper, но считаем его приватным полем структуры сервис2 и запускаем инициализацию сервис1 изнутри инита сервис2, тем же принципом поступаем в реализации пинга - пингуем заодно и сервис1. Если коннект к базе данных сервис1 требуется всему приложению, то просто создаем сервис3, который будет работать на все остальное приложение и регаться в ServiceKeeper, пока сервис2 будет иметь собственный коннект сервис1 и никому его не показывать.

  2. передайте в сервис2 интерфейсы для работы с сервис1 еще до инита, при создании самих структур (интерфейсов), но учитывайте, что когда запускается инит одного из сервисов, второй может быть еще не инициализирован, поэтому трогать его вы не можете. В этом случае используйте отложенную инициализацию (такой паттерн, когда инициализация выполняется не на старте, а при первом запросе) или продумайте систему подписки на событие инициализации на сервис от которого зависите и подписывайтесь на него. но, боже мой, тут можно так перемудрить, что потом кодинг станет болью.

Я написал много текста, но мой посыл в том, что нужно ослаблять зависимости между сервисами. Сервисы должны быть законченyой самостоятельной единицей. Ни в коем случае реализация одного сервиса не должна зависеть от реализации второго. Они даже не должны догадываться о том, что существуют другие сервисы. Но, если после всего сказанного все-равно не понятно, что конкретно делать, попробуйте в ответном комментарии описать логику ваших сервисов на верхнем уровне, чтобы я понимал, что можно посоветовать.

На подходе вторая часть этой статьи, где будет представлен пример маленького http-бэкенда. Там должно быть понятнее.

В любом случае в следующей статье можно будет уже общаться предметно. В этой я лишь задаю вектор рассуждений при разработке фундамента приложения.

Ну вообще вопросы kubernetes livenes/readines probes мы тут пока не трогаем. Их очередь еще придет.

А в случае отказа какого-то сервиса не должно происходить остановки приложения, в этом я с Вами согласен. Остановка происходит, когда на запрос Ping сервис отвечает ошибкой - это да. Тут пока мало документации и поэтому не понятно, что принятие решения в случае возникновения любой ошибки не лежит на плечах ServiceKeeper, вовсе нет, когда сервис понимает, что у него проблемы, есть два пути решения:

  1. отдать error, и, да, тут приложение завершится

  2. отдать nil и войти в режим восстановления, а это означает: поведение по-умолчанию для вызова своих АПИ и фоновые попытки восстановить коннекты/ресурсы и вернуться к нормальной работоспособности (на уровне конкретного ресурса без уведомления приложения)

    Второе можно применить только, если такое применить возможно, например, так может работать memcached - он будет отвечать Cache Miss на попытки получить что-то из кеша, а в фоне попытается восстановить связь с кеш-сервером.

т.е. чтобы меня правильно поняли: Ping возвращает error только в том случае, если работу продолжить невозможно

Для разработчика этой функции абсолютно не важно, есть ли вообще такая структура Application, потому, что жизненный цикл MainFunc должен быть не дольше времени перехода между srvStateRunning и srvStateShutdown. Т.е. глобально в MainFunc вся логика построена на работе как в обычном main, только мы уже сразу имеем канал (holdOn <-chan struct{}) сигнализирующий о завершении работы, как <-chan os.Signal, который мы обычно создаем сами

Однако, хорошее замечание про оркестрацию! И вот мой ответ: в том месте, где вы начинаете оркестрировать экземплярами Application, и нужно размещать интерфейс Application с его

  • Run() error - для старта (блокирующий, кстати)

  • Halt() - для остановки

  • Shutdown() - для экстренной "жесткой" остановки

и больше ничего не нужно. Состояние state используется исключительно для синхронизации вызовов внутри реализации Application и выдавать наружу не нужно

Хорошее замечание. Тут явно видна какая то моя заделка которая не дошла до своего финала?

С интерфейсом Resources такая история: фактически Application совершенно не волнует какая ему предоставлена реализация, он хочет получить возможность управлять ресурсами на верхнем уровне, просто командуя Init, Watch и прочее. Так же и с реализацией ServiceKeeper - он просто хочет массив интерфейсов Service, чтобы управлять зоопарком ресурсов. Я написал базовую реализацию ServiceKeeper только для того, чтобы можно было видеть, как этим пользоваться и, если бы разделил services.go от application.go на разные пакеты в своем репозитории, то имел бы законченную мысль.

Однако это все еще не отвечает на Ваш вопрос по поводу Application interface. Я не вижу какого то случая, при котором бы понадобился такой интерфейс. Куда вы его примостите и зачем? Если в основе лежит идея о том, что Application запускает MainFunc и следит за Health приложения, то кто будет следить за Application и для чего?

Ну и как исключительный вариант - Вы же можете создать этот интерфейс в любом пакете своего приложения и получив ссылку на Application экземпляр общаться с ним при помощи этого интерфейса.

Если канал не будет буферизированный, то в случае, когда все сервисы вернут ошибку, произойдет следующее:

  1. первый сервис записал в канал ошибку, мы прочитали и вышли из функции

  2. второй сервис пытается записать - а никто уже не читает. висим

но вообще хорошо, что обратили внимание на эту функцию, она мне самому не нравится и, если Вы перейдете по ссылке на мой гитхаб, то увидите, что там вообще используется кое-что похожее на sync/errgroup, но свое

Дельный комментарий, спасибо

Но я тоже немного парирую: во многих чек-пунктах у меня структура такая: "[ ] я сделал {то-то то-то} или {я знаю, почему могу без этого}". Самое главное тут не выполнить пункт, а знать, что есть определенная идея/техника и что в моем конкретном случае она поможет или будет лишней.

На счет монолита на ruby: вообще не буду связываться - я бы с таким умер от инфаркта, но у некоторых получается, и я снимаю перед ними шляпу.

вот же: "Если Вы не видите объективных причин - считайте, что параллельно с Вами собеседовался специалист, который сильнее Вас. Его-то и выбрали."

Как Вы будете анализировать то, что невозможно записать и нет фидбека? Но я в своей практике не встречался с такой ситуацией, чтобы мне казалось, что я на 100% прошел собеседование и нет никакого поля для работы над ошибками, но при этом мне еще и фидбека не дали. Было даже наоборот - я знаю, где косякнул, я понимаю, что не до конца раскрыл тему и даже ответил немного спутано, но все равно получил оффер.

ахах
А вы сейчас работаете на США, UK или Израиль?

Если Вы не видите объективных причин - считайте, что параллельно с Вами собеседовался специалист, который сильнее Вас. Его-то и выбрали.

Если Вы не способны анализировать свое поведение во время собеседования - то начните с самых глубин, научитесь видеть себя со стороны. Тут, простите, нет простых решений.

Если же Вы способны на самоанализ, то тут вообще все просто:

  1. никогда не вините никого кроме себя

  2. запишите (прямо во время собеседования) вопросы, на которые отвечали неуверенно

  3. запишите темы (укрупненно) вопросов, которые задавали

  4. поработайте над этим дома - читайте хабры, смотрите ютуб каналы (авторитетные каналы, а не просто блогеров). "качать скилуху" можно так же, как качают пресс

Обратите внимание, что ни один из этих пунктов не завязан на фидбеке. Фидбека вобще не ждите!

А подталкивать удачу не нужно, она сама липнет к тем, кто к ней стремится

Ваш нон-конформистский и даже слегка панковский стиль мне импонирует конечно =) Но разрешите с Вами не согласиться. Опираясь на какие факты вы считаете, что Вас унижали 30-40 минут? Применяли оскорбительные слова или показывали пальцем? И вряд ли кто-то считает, что Вы кому то должны. Ну кроме тех, кому Вы действительно должны.

А произошло по-моему вот что: первый работодатель на собеседовании проверял Ваши знания linux и сети только потому, что на их вакансии это очень важные навыки. Скорее всего, последующее объяснение причин по которым эти навыки для них так важны, Вы посчитали оскорблением. Ведь фактически для Вас это прозвучало, как "ты вот этого не знаешь и вот этого, поэтому не соответствуешь нашим требованиям".

Ну а то, что во втором случае все Ваши навыки полностью совпали с требованиями и уровень ЗП был выше - это лишь означает, что на рынке труда требуются и специалисты без навыков в linux и сетях.

Ну а последнее Ваше "жизнь одна..." я целиком разделяю.

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность