Slide 1

Slide 1 text

Правильная организация Рутины Александр Кирюхин, старший программист

Slide 2

Slide 2 text

Содержание Разработка микросервисов на потоке ✦ Проблема копипаста ✦ Проблема архитектуры ✦ Проблема владения кодовой базой

Slide 3

Slide 3 text

Содержание Разработка микросервисов на потоке ✦ Проблема копипаста ✦ Проблема архитектуры ✦ Проблема владения кодовой базой Путь нашей команды ✦ Первые шаги ✦ Управление корутинами на WaitGroup ✦ ErrGroup

Slide 4

Slide 4 text

Содержание Разработка микросервисов на потоке ✦ Проблема копипаста ✦ Проблема архитектуры ✦ Проблема владения кодовой базой Путь нашей команды ✦ Первые шаги ✦ Управление корутинами на WaitGroup ✦ ErrGroup Итоговое решение ✦ Rutina как «скелет» приложения ✦ Политики окончания ✦ Rutina для пакетных задач ✦ Примеси

Slide 5

Slide 5 text

Проблемы потоковой разработки

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Проблема общей архитектуры Решить это общими пакетами уже не получится, а на каждое изменение приходится либо копить техдолг, либо тратить время на рефакторинг. Этого никто не хочет. Активное развитие заставляет изменяться не только коду, но и самой архитектуре микросервисов.

Slide 8

Slide 8 text

Проблема владения кодовой базой Вне зависимости от размера команды, при микросервисном подходе становится актуальна проблема владения кодом всех разработчиков.

Slide 9

Slide 9 text

Проблема владения кодовой базой Вне зависимости от размера команды, при микросервисном подходе становится актуальна проблема владения кодом всех разработчиков. Приложения должны быть устроены так, чтобы любой разработчик мог быстро: ✦ Разобраться в их устройстве ✦ Найти интересующее место бизнес-логики, не отвлекаясь на сервисный код ✦ Выполнить поставленную задачу

Slide 10

Slide 10 text

Наш путь к решению этих проблем

Slide 11

Slide 11 text

Первые шаги - архитектура «в лоб» func main() { app := App{ Logger: logger, Config: &cfg, Service: service, DB: db, } msgs := app.Service.GetMessages() for { msg := <-msgs err, catch := handler(app, msg) if err != nil { app.Logger.Error("cannot handle event", zap.Error(err)) if catch { app.Service.Catch(msg, err) } } } } func handler(app *App, msg *babex.Message) (error) { ... полезная работа ... }

Slide 12

Slide 12 text

Первые шаги - архитектура «в лоб» func main() { app := App{ Logger: logger, Config: &cfg, Service: service, DB: db, } msgs := app.Service.GetMessages() for { msg := <-msgs err, catch := handler(app, msg) if err != nil { app.Logger.Error("cannot handle event", zap.Error(err)) if catch { app.Service.Catch(msg, err) } } } } func handler(app *App, msg *babex.Message) (error) { ... полезная работа ... } Хоть и просто, но много проблем

Slide 13

Slide 13 text

func main() { app := App{ Logger: logger, Config: &cfg, Service: service, DB: db, } msgs := app.Service.GetMessages() wg := sync.WaitGroup{} ctx, cancel := context.WithCancel(context.Background()) // Процесс обработки сообщений wg.Add(1) go func () { defer wg.Done() for { select { case msg := <-msgs: if err := handler(app, msg); err != nil { cancel() return err } case <-ctx.Done(): return } } } // Процесс отслеживания сигналов ОС wg.Add(1) go func () { defer wg.Done() sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) select { case <- ctx.Done: return case <- sig: cancel() return } } // Ждем завершения всех горутин wg.Wait() // Завершаем работу... app.Service.Stop() app.Logger.Sync() } func handler(app *App, msg *babex.Message) error { ... полезная работа ... } Управление с помощью WaitGroup

Slide 14

Slide 14 text

func main() { app := App{ Logger: logger, Config: &cfg, Service: service, DB: db, } msgs := app.Service.GetMessages() ctx := context.Background() wg := errgroup.WithContext(ctx) // Процесс обработки сообщений wg.Go(func () error { for { select { case msg := <-msgs: if err := handler(app, msg); err != nil { return err } case <-ctx.Done(): return nil } } }) // Процесс отслеживания сигналов ОС wg.Go(func () error { defer wg.Done() sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) select { case <- ctx.Done: return nil case <- sig: return errors.New("os signal") } }) // Ждем завершения всех горутин if err := wg.Wait();err != nil { app.Logger.Error("app error", zap.Error(err)) } // Завершаем работу... app.Service.Stop() app.Logger.Sync() } func handler(app *App, msg *babex.Message) error { ... полезная работа ... } Управление с помощью ErrGroup

Slide 15

Slide 15 text

Итоговое решение – Rutina

Slide 16

Slide 16 text

Итоговый шаблон приложения func main() { app := App{ Logger: logger, Config: &cfg, Service: service, DB: db, } msgs := app.Service.GetMessages() r := rutina.New() // Процесс обработки сообщений r.Go(func (ctx context.Context) error { msg := <- msgs return handler(app, msg) }, rutina.RestartIfDone, rutina.ShutdownIfError) // Процесс отслеживания сигналов ОС r.ListenOsSignals() // Ждем завершения всех горутин if err := r.Wait();err != nil { app.Logger.Error("app error", zap.Error(err)) } // Завершаем работу... app.Service.Stop() app.Logger.Sync() } func handler(app *App, msg *babex.Message) error { ... полезная работа ... }

Slide 17

Slide 17 text

Политики окончания Rutina поддерживает политики действий при завершении контролируемой горутины без ошибок: ✦ DoNothingOnDone – не производится дополнительных действий ✦ RestartOnDone – горутина перезапускается с теми же аргументами ✦ ShutdownOnDone – закрывается контекст, тем самым отправляется сигнал на завершение другим горутинам

Slide 18

Slide 18 text

Политики окончания Rutina поддерживает политики действий при завершении контролируемой горутины без ошибок: ✦ DoNothingOnDone – не производится дополнительных действий ✦ RestartOnDone – горутина перезапускается с теми же аргументами ✦ ShutdownOnDone – закрывается контекст, тем самым отправляется сигнал на завершение другим горутинам Политики при завершении горутины с ошибкой: ✦ DoNothingOnError – не производится дополнительных действий ✦ RestartOnError – горутина перезапускается с теми же аргументами ✦ ShutdownOnError – закрывается контекст, тем самым отправляется сигнал на завершение другим горутинам

Slide 19

Slide 19 text

Политики окончания Rutina поддерживает политики действий при завершении контролируемой горутины без ошибок: ✦ DoNothingOnDone – не производится дополнительных действий ✦ RestartOnDone – горутина перезапускается с теми же аргументами ✦ ShutdownOnDone – закрывается контекст, тем самым отправляется сигнал на завершение другим горутинам Политики при завершении горутины с ошибкой: ✦ DoNothingOnError – не производится дополнительных действий ✦ RestartOnError – горутина перезапускается с теми же аргументами ✦ ShutdownOnError – закрывается контекст, тем самым отправляется сигнал на завершение другим горутинам Поведение по умолчанию совпадает с таковым у ErrGroup: ✦ ShutdownOnDone ✦ ShutdownOnError

Slide 20

Slide 20 text

Обработка пакетных задач r := rutina.New() tasks := []string{ "http://url1.ru/....", "http://url2.ru/....", "http://url3.ru/....", ... "http://urlN.ru/....", } for _, task := range tasks { task := task r.Go(func (ctx context.Context) error { // Некий парсинг со страницы return worker(ctx, task) }, rutina.RestartIfError) } r.Wait()

Slide 21

Slide 21 text

Обработка пакетных задач r := rutina.New() tasks := []string{ "http://url1.ru/....", "http://url2.ru/....", "http://url3.ru/....", ... "http://urlN.ru/....", } for _, task := range tasks { task := task r.Go(func (ctx context.Context) error { // Некий парсинг со страницы return worker(ctx, task) }, rutina.RestartIfError) } r.Wait() Но что делать, если мы хотим залогировать или как-то обработать ошибки, а не заглушить их?

Slide 22

Slide 22 text

Обработка пакетных задач r := rutina.New(rutina.WithErrChan()) tasks := []string{ "http://url1.ru/....", "http://url2.ru/....", "http://url3.ru/....", ... "http://urlN.ru/....", } for _, task := range tasks { task := task r.Go(func (ctx context.Context) error { // Некий парсинг со страницы return worker(ctx, task) }, rutina.RestartIfError) } r.Go(func (ctx context.Context) error { select { case <-ctx.Done: return nil case err := <-r.Errors(): log.Error(err) } }, rutina.RestartIfDone) r.Wait()

Slide 23

Slide 23 text

Миксины Rutina поддерживает следующие миксины, меняющие поведение: ✦ WithErrChan() – создает канал с ошибками горутин с политиками DoNothingOnError, RestartOnError ✦ WithStdLogger и WithLogger(logger log.Logger) – добавляет логирование стандартным или собственным логгером ✦ WithContext(ctx context.Context) – заставляет Rutina использовать внешний контекст, например, контекст запроса или родительского экземпляра Rutina ✦ WithLifecycleListener(func (event rutina.Event, rid int)) – позволяет повесить функцию-слушатель на внутренние события Rutina ✦ WithListenOsSignals() – автоматически слушает сигналы ОС, нет необходимости вручную запускать r.ListenOsSignals()

Slide 24

Slide 24 text

Спасибо за внимание! vk.com/neonxp github.com/neonxp

Slide 25

Slide 25 text

Ссылка на Rutina https://github.com/neonxp/rutina https://godoc.org/github.com/neonxp/rutina