Русский
Русский
English
Статистика
Реклама

Dodo engineering

Тонкости авторизации обзор технологии OAuth 2.0

21.09.2020 18:14:10 | Автор: admin
Информационная система Dodo IS состоит из 44 различных сервисов, таких как Трекер, Кассы ресторана или Базы знаний и многих других. 3 года назад мы написали сервис Auth для реализации сквозной аутентификации, а сейчас пишем уже вторую версию. В основе сервиса лежит стандарт авторизации OAuth 2.0. Он довольно сложный, но если будете работать над аналогичным сервисом, стандарт вам пригодится. В этой статье я постарался рассказать о стандарте максимально просто и понятно, чтобы вы сэкономили время на его изучение.



Задача Auth


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

У сервиса Auth есть три основные задачи:

  • Единая точка аутентификации (SSO) для всех сервисов системы. Сервисы не хранят учётные данные, а доверяют это одному выделенному сервису.
  • Безопасный и гранулированный доступ к ресурсам. Безопасный, потому что пароли хранятся в одном месте и максимально защищены. Гранулированный, так как владельцы сервисов могут настраивать доступ к ресурсам как они захотят, опираясь на данные, пришедшие из сервиса аутентификации.
  • Централизованное управление пользователями и доступом. Благодаря тому, что вся информация о пользователе хранится в сервисе аутентификации, мы можем управлять пользователями централизованно.

Проблемы


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

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

Dodo IS зависит от Auth. В старой реализации внешние сервисы обращаются к Auth при каждом действии пользователя, чтобы валидировать данные о нём. Настолько сильная привязка может привести к остановке работы всей Dodo IS, если Auth приляжет по какой-то причине.

Auth зависит от Redis. Притом достаточно сильно неисправность работы Redisа приведёт к падению Authа. Мы используем Azure Redis, для которого заявленный SLA 99,9%. Это значит, что сервис может быть недоступен до 44 минут в месяц. Такие простои не позволительны.

Текущая реализация Auth использует свой протокол аутентификации, не опираясь на стандарты. В большинстве своих сервисов мы используем C# (если говорим о backend) и у нас нет проблем с поддержкой библиотеки для нашего протокола. Но если вдруг появятся сервисы на Python, Go или Rust, разработка и поддержка библиотек под эти языки потребует дополнительных затрат времени и принесет дополнительные сложности.

Текущий Auth использует схему Roles Based Access Control, которая базируется на ролях. Обычно с ролью выдаётся полный доступ к определённому сервису, вместо привязки к конкретному функционалу. Например, в пиццериях есть заместители управляющего, которые могут вести определенные проекты: составлять графики или учитывать сырьё. Но у нас нет выдачи прав на конкретные компоненты системы. Приходится выдавать полный доступ к сервису, чтобы сотрудники могли получить доступ к составлению графиков или настройкам какого-либо компонента учёта.

Проблемы подтолкнули к тому, чтобы спроектировать и написать новую версию Auth. На старте проекта мы потратили 3 недели только на изучение стандартов авторизации и аутентификации OAuth 2.0 и OpenID Connect 1.0.

Примечание. Утрированно, статья это пересказ RFC, который приходилось перечитывать несколько раз, чтобы понять, что происходит вокруг. Здесь я постарался уйти от этой сложности и рассказать всё максимально просто, структурировано, кратко и без описания сложных вещей, например, какие символы может содержать в себе ответ сервиса. В отличии от RFC, прочитав это один раз, можно во всём разобраться. Надеюсь, статья будет полезна и сэкономит время при выборе решения для реализации сервиса аутентификации, а может, кого-то заставит задуматься о его необходимости.

Что такое ОAuth2.0?


Разработку нового Auth мы решили начать с изучения доступных протоколов и технологий. Самый распространённый стандарт авторизации фреймворк авторизации OAuth2.0.

Стандарт был принят в 2012 году, и за 8 лет протокол меняли и дополняли. RFC стало настолько много, что авторы оригинального протокола решили написать OAuth 2.1, который объединит все текущие изменения по OAuth 2.0 в одном документе. Пока он на стадии черновика.

Актуальная версия OAuth описанна в RFC 6749. Именно его мы и разберем.

OAuth 2.0 это фреймворк авторизации.

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

Особенности:

  • Разделение сущности пользователя и приложения, запрашивающего доступ. Благодаря этому разделению мы можем управлять правами приложения отдельно от прав пользователя.

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

Разберёмся подробнее в особенностях.

Роли


В OAuth 2.0 определены четыре роли:

  • Resource owner сущность, которая имеет права доступа на защищённый ресурс. Сущность может быть конечным пользователем или какой-либо системой. Защищённый ресурс это HTTP endpoint, которым может быть что угодно: API endpoint, файл на CDN, web-сервис.
  • Resource server сервер, на котором хранится защищённый ресурс, к которому имеет доступ resource owner.
  • Client. Это приложение, которое запрашивает доступ к защищённому ресурсу от имени resource owner и с его разрешения с авторизацией.
  • Authorization server сервер, который выдаёт клиенту токен для доступа к защищённому ресурсу, после успешной авторизации resource owner.

Каждый участник взаимодействия может совмещать в себе несколько ролей. Например, клиент может быть одновременно resource owner, и запрашивать доступ к своим же ресурсам. Схему взаимодействия рассмотрим дальше.

Важно: клиент должен быть заранее зарегистрирован в сервисе. Как это сделать?

Регистрация клиента


Способ регистрации клиента, например, ручной или service discovery, вы выбираете сами, в зависимости от фантазии конкретной реализации. Но при любом способе при регистрации, кроме ID клиента, должны быть обязательно указаны 2 параметра: redirection URI и client type.

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

Client type тип клиента, от которого зависит способ взаимодействия с ним. Тип клиента определяется его возможностью безопасно хранить свои учётные данные для авторизации токен. Поэтому существует всего 2 типа клиентов:

  • Confidential клиент, который может безопасно хранить свои учётные данные. Например, к такому типу клиентов относят web-приложения, имеющие backend.
  • Public не может безопасно хранить свои учётные данные. Этот клиент работает на устройстве владельца ресурса, например, это браузерные или мобильные приложения.

Токены


Токен в OAuth 2.0 это строка, непрозрачная для клиента. Обычно строка выглядит как случайно сгенерированная её формат не имеет значения для клиента. Токен это ключ доступа к чему-либо, например, к защищённому ресурсу (access token) или к новому токену (refresh Token).

У каждого токена своё время жизни. Но у refresh token оно должно быть больше, т.к. он используется для получения access token. Например, если срок жизни access token около часа, то refresh token можно оставить жить на целую неделю.

Refresh token опционален и доступен только для confedential клиентов. Пользуясь опциональностью токена, в некоторых реализациях время жизни access token сделано очень большим, а refresh token вообще не используется, чтобы не заморачиваться с обновлением. Но это не безопасно. Если access token был скомпрометирован, его можно обнулить, а сервис получит новый Access token с помощью refresh token. В случае, если refresh token нет, то потребуется проходить процесс авторизации заново.

За access token закреплён определённый набор прав доступа, который выдаётся клиенту во время авторизации. Давайте разберёмся, как выглядят права доступа в OAuth 2.0.

Права доступа


Права доступа выдаются клиенту в виде scope. Scope это параметр, который состоит из разделённых пробелами строк scope-token.

Каждый из scope-token представляет определённые права, выдающиеся клиенту. Например, scope-token doc_read может предоставлять доступ на чтение к какому-то документу на resource server, а employee доступ к функционалу приложения только для работников фирмы. Итоговый scope может выглядеть так: email doc_read employee.

В OAuth 2.0 мы сами создаём scope-token, настраивая их под свои нужды. Имена scope-token ограничиваются только фантазией и двумя символами таблицы ASCII " и \.

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

Абстрактный OAuth 2.0. Flow c применением Access token


Мы рассмотрели роли, рассмотрели виды токенов, а также как выглядят scope. Посмотрим на flow предоставления доступа к сервису.

Ниже представлена абстрактная схема (или flow) взаимодействия между участниками. Все шаги на данной схеме выполняются строго сверху вниз. Разберём детальнее.



  • Client отправляет запрос на доступ к требуемому ресурсу resource owner.
  • Resource owner передаёт обратно клиенту authorization grant, который подтверждает личность resource owner и его права на ресурс, доступ к которому запрашивает client. В зависимости от flow это может быть токен или учётные данные.
  • Client отправляет authorization grant, полученный в предыдущем шаге authorization server, ожидая от него Access token для доступа к защищённому ресурсу.
  • authorization server убеждается в валидности authorization grant, после чего отсылает access token клиенту в ответ.
  • Получив access token, клиент запрашивает защищённый ресурс у resource server.
  • Resource server убеждается в корректности access token, после чего предоставляет доступ к защищённому ресурсу.

Клиент получает одобрение от resource owner, на основе которого ему выдаётся доступ к ресурсу. Всё просто. А будет ли так же просто, если мы добавим в эту схему работу с refresh token?

Абстрактный OAuth 2.0. Flow c применением Refresh token


Первый и второй шаги опущены из данной схемы они ничем не отличаются от схемы абстрактного flow выше.



Схема подробнее:

  • Client приходит c authorization grant к authorization server и просит предоставить ему access token и refresh token.
  • Authorization server убеждается, что с authorization grant всё нормально и возвращает клиенту запрошенные access token и refresh token.
  • Client с access token запрашивает защищённый ресурс, пока не получит первую ошибку доступа к ресурсу invalid token error.
  • После получения ошибки доступа, клиент идет к authorization server с refresh token и просит заменить просроченный access token на новый.
  • В ответ клиент получает новый access token, а также новый refresh token, либо продлевается время жизни старого refresh token.

Что такое grant?


Grant это данные, которые представляют из себя успешную авторизацию клиента владельцем ресурса, используемые клиентом для полученияaccess token.

Например, когда мы где-либо аутентифицируемся с помощью Google, перед глазами всплывает уведомление. В нём говорится, что такой-то сервис хочет получить доступ к данным о вас или к вашим ресурсам (выводятся запрашиваемые scope-token). Это уведомление называется Consent Screen.

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

Существует 4 + 1 способа получения grant grant type:

  • Authorization code используется для confedencial клиентов web-сервисов.
  • Client credentials используется для confedential клиентов, которые запрашивают доступ к своим ресурсам или ресурсам, заранее согласованным с сервером авторизации.
  • Implicit использовался public-клиентами, которые умеют работать с redirection URI (например, для браузерных и мобильных приложений), но был вытеснен authorization code grant с PKCE (Proof Key for Code Exchange дополнительная проверка, позволяющая убедиться, что token получит тот же сервис, что его и запрашивал. Прочитать подробнее RFC 7636).
  • Resource owner password credentials. В RFC 6819, посвящённому безопасности в OAuth 2.0, данный тип grant считается ненадёжным. Если раньше его разрешалось использовать только для миграции сервисов на OAuth 2.0, то в данный момент его не разрешено использовать совсем.
  • Device authorization (добавлен в RFC 8628) используется для авторизации устройств, которые могут не иметь веб-браузеров, но могут работать через интернет. Например, это консольные приложения, умные устройства или Smart TV.

Актуальными можно считать только authorization code (с PKCE), client credentials и device authorization grant, но мы рассмотрим все. Рассматривать grant будем в порядке возрастания сложности понимания.

Client credentials grant flow


Имеет самый простой flow, напоминающий обычную авторизацию на любом сервисе. Она выполняется с помощью учётных данных клиента, которые представляют собой client id и client secret аналог логина и пароля для пользователя. Так как для аутентификации требуется client secret, который должен соответствующе храниться, данный flow могут использовать только confedential клиенты.



Схема проста: клиент аутентифицируется на сервере авторизации передавая client id и client secret. В ответ получает access token, с которым уже может получить доступ к нужному сервису.

Этот flow требуется, когда клиент пытается получить доступ к своим ресурсам или ресурсам, заранее согласованным с сервером авторизации. Например, сервису А нужно время от времени ходить в сервис Б и актуализировать там данные о количестве пиццерий в сети.

Resource owner password credentials flow


По текущим рекомендациям безопасности описанных в данном RFC, данный flow не рекомендуется использовать вовсе из-за явных проблем с безопасностью.



Resource owner передаёт свой логин и пароль клиенту, например, через формы на клиенте. Клиент, в свою очередь, с помощью него получает access token (и, опционально, refresh token).

Здесь есть проблема. Resource owner просто берёт и отдаёт в открытом виде свой логин и пароль клиенту, что не безопасно. Изначально он был сделан только для клиентов, которым вы доверяете или тех, что являются частью операционной системы. Позже он был разрешён только для миграции с аутентификации по логину и паролю на OAuth 2.0. Текущие рекомендации по безопасности запрещают его использование.

Authorization code


Самый распространённый flow на данный момент. В основном используется для confidential клиентов, но с появлением дополнительной проверки с помощью PKCE, может применяться и для public-клиентов.

В данном flow взаимодействие client с resource owner проходит через user-agent (браузер). К user-agent есть одно требование: он должен уметь работать с HTTP-редиректами. Без этого resource owner не сможет попасть к серверу авторизации и вернуться обратно с grant.



Данный flow сложнее, чем предыдущие, поэтому будем разбирать по шагам. Для начала представим, что мы resource owner и перешли на страницу сервиса онлайн-обучения, который хочет сохранять результаты обучения к нам в облако. Ему требуется получить доступ к нашему ресурсу, например, определённой директории в облаке. Мы нажимаем на Авторизоваться и начинается путешествие по Authorization code grant flow:

  • На первом шаге клиент перенаправляет resource owner с помощью user-agent на страницу аутентификации Authorization server. В URI он указывает client ID и redirection URI. Redirection URI используется для понимания, куда вернуть resource owner после того, как авторизация пройдёт успешно (resource owner выдаст разрешение на scope, запрашиваемый клиентом).
  • Взаимодействуя с сервером авторизации через user-agent, resource owner проходит аутентификацию на сервере авторизации.
  • Resource owner проверяет права, которые запрашивает клиент на consent screen и разрешает их выдачу.
  • Resource owner возвращается клиенту с помощью user-agent обратно на URI, который был указан как redirection URI. В качестве query-параметра будет добавлен authorization code строка, подтверждающая то, что resource owner выдал необходимые права сервису.
  • С этим authorization code клиент отправляется на сервер авторизации, чтобы получить в ответ access token (ну и refresh token, если требуется).
  • Сервер авторизации валидирует authorization code, убеждаясь, что токен корректный и выдаёт клиенту access token (и опционально refresh token). С его помощью клиент сможет получить доступ к заветному ресурсу.

Если представить нас на месте resource owner, то мы видим просто перенаправление на сервер авторизации, аутентифицируемся, подтверждаем доступ на Consent screen и нас отправляет на уже работающий сервис. Например, мы проходим это много раз, когда заходим на сервис под учётной записью Google, Facebook или Apple.

Следующий flow построен на основе этого.

Implicit grant


Это оптимизация Authorization code grant flow для public-клиентов, которые умеют работать с redirection URI. Например, для браузерных приложений на JavaScript, или мобильных приложений. Требование к user-agent, с помощью которого взаимодействуют клиент и resource owner, сохраняется: он должен уметь работать с HTTP-редиректами.

Между authorization code и implicit есть основное отличие: вместо получения authorization code и access token по нему, мы сразу получаем access token после успешной авторизации resource owner. Кроме того, здесь не используется client secret из соображений безопасности приложение можно дизассемблировать и получить его. Подлинность проверяется только по redirection URI.



Многие шаги из данной схемы похожи на шаги из authorization code, но предлагаю их разобрать также подробно. Представим, что некое браузерное приложение хочет сохранять свои настройки в нашем Git-репозитории. Мы нажимаете Войти в GitHub и на этом этапе начинается работа Implicit flow:

  • Клиент с помощью user-agent и HTTP-редиректа перенаправляет resource owner на сервер авторизации. В параметрах запроса передает client ID и redirection URI, которые нужны для аутентификации клиента и последующего возврата resource owner обратно.
  • Resourse owner аутентифицируется, взаимодействуя через user-agent с сервером авторизации. Заодно подтверждает выдачу grant клиенту, с client ID которого он пришёл.
  • После подтверждения выдачи grant (нажатия allow на consent screen), user-agent возвращает resource owner на redirection URI. Кроме того, в URI fragment передаётся access token (URI fragment это то, что обычно идёт в URI после символа #).
  • Сам фрагмент сохраняется локально в user-agent. User-agent двигается дальше по redirection URI за web-страницей, которая нужна для получения access token и других необходимых данных из фрагмента. Она может находиться как на самом клиенте, так и на удалённом ресурсе, например, на CDN.
  • Web-ресурс возвращает web-страницу (может содержать в себе скрипт), которая может прочитать полностью redirection URI, в том числе и значение, указанное в фрагменте.
  • User-agent отрисовывает локально полученную страницу, включая исполнение скриптов, которые он получил от web-hosted client resource, которые получают access token.
  • Полученный access token user-agent просто передаёт клиенту.

Это сложный flow. Он мало используется в реальных сценариях. Но его всё ещё можно встретить в legacy-проектах.

Device authorization (RFC 8628)


С 2012 до 2019 появилось много умных устройств, на которых неудобно авторизоваться. Например, неудобно вводить сложный логин и пароль на телевизоре каждый раз при открытии ресурса. На некоторых устройствах это невозможно, например на серверных ОС без графического интерфейса. В августе 2019 этот flow появился как раз для таких сценариев.

Есть, как минимум, 3 требования к устройствам, чтобы работа с помощью Device authoraztion grant flow была возможна:

  • Устройство должно иметь возможность совершать исходящие HTTPS-запросы.
  • Устройство должно иметь возможность отображать URI и код пользователю.
  • Каждое авторизуемое устройство принадлежит resource owner, который для успешной авторизации должен иметь другое устройство с браузером, чтобы перейти по указанному URI и ввести указанный код.



Возможно, схема кажется сложной из-за обилия стрелок. Разберём её также пошагово, как и разбирали сложные flow до него.

Представим, что мы пытаемся авторизоваться на web-сервисе с помощью телевизора. Мы видим кнопку Авторизоваться как устройство и нажимаем. В этот момент начинается наш Device flow:

  • Телевизор делает запрос на сервер авторизации, передавая ему свой client ID.
  • Сервер авторизации убеждается, что такой клиент зарегистрирован и имеет соответствующий тип grant.
  • Если всё хорошо, то Authorization server возвращает device code, user code и verification URI. Device code это уникальный идентификатор устройства, которое авторизуется в системе.
  • Устройство отображает user code и verification URI владельцу этого устройства resource owner. Redirection URI может быть передан как строкой, так и с помощью QR-кода ограничений нет.
  • После того, как устройство отобразило user code и verification URI, оно начинает раз в некоторое время опрашивать сервер авторизации о её успешности.
  • Дальше в бой вступает resource owner. Он переходит по указанному verification URI, аутентифицируется и вводит user code, который он получил от устройства, подтверждая выдачу необходимых scope устройству. На этом действия от имени resource owner закончены.
  • Всё это время устройство (пункт 3) опрашивало сервер авторизации о её успешности. Устройство в очередной раз идёт к серверу авторизации со своим device code и client ID в надежде, что авторизация на этот раз прошла.
  • В этот раз, когда resource owner подтвердил передачу необходимых прав устройству, сервер авторизации возвращает в ответе на запрос access token (если предусмотрено настройками сервера и refresh token). И с помощью токена устройство уже может продолжать работу с ресурсом.

Несмотря на кажущуюся сложность со стрелочками, этот flow тоже достаточно прост. Если вам требуется взаимодействовать с устройствами (а у нас их много: трекер, касса, витрины и прочие устройства), то вам стоит использовать этот flow.

Вместо вывода


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

Если хотите погрузиться в тематику детальнее, то рекомендую в RFC 6749 (для OAuth 2.0) и RFC 8628 (для Device Flow). Кроме того, следить за актуальными версиями RFC можно на ресурсе, посвящённому OAuth.

Если статья была полезна и захотите подробностей пишите в комментариях, и в следующих статьях расскажу о PKCE, о протоколе аутентификации OpenID Connect 1.0, о нашей реализации сервера аутентификации и многом другом.

Полезные ссылки:

Подробнее..

Зачем мне психотерапевт?

10.12.2020 20:17:14 | Автор: admin
После терапии несколько лет мне захотелось пробовать то, что я раньше боялся. Например, в 2014 году я решил начать выступать. Но так как мне было страшно даже разговаривать с людьми, каждый раз перед выступлением всю ночь не спал мозг взрывался. Со временем мозг переобучился и привык, и теперь страх для меня вызов. Это воспоминания Ивана Замесина о последствиях курса психотерапии. Иван предприниматель и основатель сервиса подбора психотерапевтов Мета. Недавно он приходил к нам на подкаст Ничего такого, где рассказал зачем нужен психотерапевт, какие установки мешают начать терапию и что можно от неё ожидать. Мы под впечатлением написали статью на основе разговора.




О нашем собеседнике: Иван Замесин (zamesin) отвечал за продукт в Яндекс.Картинках, работал в chatfuel.com как продакт-менеджер. Сейчас Иван предприниматель: проводит курсы по продуктовому мышлению в IT-компаниях, например, Skyeng, Cian, HH.ru, Mail.ru, и развивает сервис по подбору психотерапевтов Мета, как его создатель. Мета это сервис, который помогает сделать первый шаг к психотерапии. Ведёт блог, где делится результатами работы сервиса.



Зачем нужна психотерапия?


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

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

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

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

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

Утрированно эти три способа можно сравнить с ремонтом в квартире. Иногда достаточно поклеить новые обои и поменять диван, а иногда приходится восстанавливать всё после сильного пожара. Зависит от состояния.

Кто такой психотерапевт?


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

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

  • Высшее психологическое или медицинское образование по психиатрии.
  • 30 и больше часов супервизии (один из методов повышения квалификации) и не меньше 12 часов супервизии в год.
  • Больше 100 часов личной терапии.
  • Не меньше 500 часов дополнительного долгосрочного обучения в одном из направлений психотерапии, например, гештальт-терапии или когнитивно-поведенческой. При этом психотерапевт может стать психоаналитиком, если прошел подготовку в методе психоанализа.

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

Зачем ходить к психотерапевту?


Например, в 2010 году у меня отжали бизнес. Я остался без бизнеса, но с психологической травмой. После того, как от неё избавился с помощью психотерапии, запустил Мета.

Психотерапевт поможет, если:

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

С любой из этих проблем можно обратиться к специалисту за консультацией. Это может быть как психолог, так и психиатр. Можно одновременно наблюдаться у психиатра и посещать психотерапевта. Каждый из этих специалистов подберет своё решение проблемы, которое будет дополнять другое.

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

Давай сходим к специалисту, он тебе поможет наладить сон.

Не нужно говорить, что близкий болен. Говорите, что есть проблема, и мы её решим. Никто не хочет, чтобы его считали больным.

Что мешает обратиться за помощью?


Мифы. Например, предрассудки о постановке на учёт и о других ужасах психиатрии. Сейчас нет такого понятия учёт и страшных карательных механизмов, а есть диспансерное наблюдение. При этом самое страшное, что может случиться это посещать психиатра раз в месяц. Под наблюдение ставят, например, при суицидальных попытках и только государственные учреждения. Но даже если человек находится под наблюдением, он может вести обычную жизнь, например, водить машину и работать.

На это накладывается установка родом из советского прошлого (а может и более раннего) Ты мужик, сам справишься. Это вторая по популярности причина (по статистике ФОМ), по которой люди не идут к психотерапевтам. Если обратился за помощью слабак и нытик. Но это не так. Чтобы признать проблему и обратиться к психотерапевту нужна смелость.

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

Как выглядит работа с психотерапевтом?


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

Врач не оценивает, не критикует, не осуждает и никому не рассказывает о сеансах (врачебная тайна). Психотерапевт множество раз сталкивался с десятками видов проблем и ваша не будет новой. Для решения ваших проблем специалист подберет свой вид терапии. Например, психоанализ, чтобы выяснить причины сегодняшних проблем в прошлом, или когнитивно-поведенческую, при которой ломают шаблоны поведения.

Также терапия включает домашние задания и практики, например, телесные.

Что ожидать от психотерапии?


Освобождения, например, от лишних переживаний. В психотерапии метрики результата субъективны. Если вы чувствуете себя спокойнее, лучше спите, наладился аппетит, значит она работает. Например, после терапии несколько лет назад у меня как-то интуитивно запустился процесс пробования всего что можно. Я начал пробовать дикое количество всего разнообразного: медитации, курс випассаны, публичные выступления и даже переводил статьи про интерфейсы.

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

Психотерапия помогает, потому что большая часть ограничений в решении сложных задач, как в найме, так и в предпринимательстве, в психике. Например, при некоторых расстройствах падает интеллект. Одно из последствий депресссии это когнитивные нарушения: ухудшается память, внимание. Всё это влияет на способоность мыслить.

Некоторые травмы также влияют на интеллект, потому что снижается чувствительно. Об этом в книге Странный порядок вещей: жизнь, чувство и создание культур рассказывал Антонио Дамасио, профессор нейробиологии, психологии и философии Южно-Калифорнийского университета. Он утверждал, что травмы сказываются на интеллекте потому что он может зависеть от чувствительности.

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

Как терапия влияет на работу?


Не считая того, что с терапией я смог пережить потерю бизнеса, она помогла избавиться от слабых сторон моего продукта. В работе мы всегда вносим свой отпечаток. Мы личности, у нас есть свои тараканы и установки в голове. Илья Красинский высказал мысль: Слепые зоны компании это слепые зоны основателя. У меня слабая сторона операционная деятельность. Например, невнимательное отношение к найму дорого обошлось. Я практически бессознательно избегал скучные части компании и она страдала 1,5 года.

Кстати, в работе с багами сознания помогает предпринимательство. Оно подсвечивает слепые зоны, потому что всё быстро меняется. Нужно решать масштабные задачи и проблемы в несколько раз быстрее, чем когда работаешь по найму, даже на сложных ролях, например, тимлидов или продактов. При этом чаще ошибаешься и видишь, на что обратить внимание.

Как быстро ждать результат? Здесь никто не ответит. Иногда достаточно встречаться полгода раз в месяц. Иногда раз в неделю пару лет. Чтобы понять, сколько продлится лечение, нужно обговорить с врачом. Но сколько бы это ни заняло, психотерапия изменит жизнь в лучшую сторону.

При этом результат может быть отсрочен и появиться уже после лечения. Например, только спустя год после лечения и запуска Мета моя психика на 100% приняла, что работает над сервисом. Это похоже на то, как будто живешь в квартире, где одна комната нормальная, а другая всегда закрыта. Её как бы не существует. Проходишь и не видишь даже дверь также было у меня.

Полезные материалы


How people change: relationships and neuroplasticity in psychoterapy. Это сборник исследований нескольких авторов под кураторством Дениела Сигела и Марион Соломон. В книге много интересного про работу мозга, психотерапию и то, как люди меняются.

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

  • выдвинуть гипотезу;
  • поставить эксперимент;
  • понаблюдать за результатами;
  • улучшить гипотезу по результатам эксперимента;
  • повторить.

Другого способа не существует.

Дойч посвятил доказательству утверждения половину книги. Например, эволюция также создаёт знания ставит гипотезы и эксперименты. Если у одного из крокодилов случайно появляются лапы длиннее чем обычно, и он выживет, то передаст свою особенность потомкам. Эволюция создаст знание и передаст его через ДНК, в которой будут содержать данные, как должна выглядеть и функционировать конечность, чтобы носитель конечности был максимально приспособлен для передачи ДНК в текущем жизненном контексте среды.

Скотт Адамс, How to Fail at Almost Everything and Still Win Big. Перевод на русском ужасен берите оригинал.

Макс Тегмарк, Live 3.0. Жизнь 1.0 простая биологическая, например, бактерии. Жизнь 2.0 человек. Жизнь 3.0 пока ещё не появилась на Земле, но она сможет быстро менять сама себя в обход эволюции.

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

Человек сложная жизнь, но с той же целью. Например, человек добывает углеводороды из-под земли и расщепляет их, добывая тепло. Кажется, что и бизнес делает то же самое. Например, UBER организует машины, чтобы их было больше и они ездили дольше, перемещая людей на большие расстояние, где они могут потратить ещё больше энергии.

The Systems Bible: The Beginner's Guide to Systems Large and Small, Джон Гал. Забавная книга, которая почти полностью состоит из тезисов. Например:

  • Эффективность сложной системы редко превышает 5%. Под системой можно подразумевать любую сложную сущность, хоть Пенсионный фонд.
  • Армия полностью готов к войне, когда она закончилась. Это пример, когда во Франции после Первой мировой построили защиту от немцев, а они просто взяли и обошли её.
  • Временное решение с высокой вероятностью станет постоянным.

Весь Нассим Талеб. Он прекрасен рациональностью мышления.

Самая классная книга та, которую хочется читать прямо сейчас, даже если это Приключения Чиполлино или сборник задач по теории вероятностей. Поэтому каждый день, когда есть время, я начинаю читать новую книгу. Если первые 50 страниц нравятся продолжаю. Если нет иду спать.

Я читаю все, что мне интересно, даже если не понимаю. Чем больше читаю разных книг, тем больше вероятность найти интересную тему. Например, так я открыл для себя физику. Если полгода каждый день читать 50-100 страниц разных книг, то мозг будет воспринимать мир иначе. Он будет строить новые взаимосвязи, закономерности и выводы, потому что ты добавил в него много новых данных. Если не читать, то и не будет данных, на которых мозг может построить новые закономерности. Это отличная долгосрочная инвестиция с кратным возвратом инвестиций.

Подкаст Ничего такого. Дорогой читатель, эта статья была написана под впечатлением от выпуска нашего нового подкаста. Нам стало интересно, что такое психотерапия, как она влияет на продукт и реально ли работает. Поэтому мы встретились с Иваном и поговорили на эти темы.

Подкасты можно послушать:


Делитесь в комментариях встречались ли с психотерапией, обращались ли к врачам и как помогло. Если хотите обсудить статью подробее присоединяйтесь в Telegram чат в Dodo Engineering chat.
Подробнее..

Как заблокировать приложение с помощью runBlocking

10.02.2021 14:12:50 | Автор: admin

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

Напишите где-нибудь в UI потоке (например в методе onStart) такой код:

//где-то в UI потокеrunBlocking(Dispatchers.Main) {  println(Hello, World!)}

Вы получите дедлок приложение зависнет. Это не ошибка, а на 100% ожидаемое поведение. Тезис может показаться неочевидным и неявным, поэтому давайте погрузимся поглубже и я расскажу, что здесь происходит.


Сравним код выше с более низкоуровневым подходом с потоками. Вы можете написать в главном потоке вот так:

//где-то в UI потокеHandler().post {println("Hello, World!") // отработает в UI потоке}

Или даже так:

//где-то в UI потокеrunOnUiThread {  println("Hello, World!") // и это тоже отработает в UI потоке}

Вроде конструкция очень похожа на наш проблемный код, но здесь обе части кода работают (по-разному под капотом, но работают). Чем они отличаются от кода с runBlocking?

Как работает runBlocking

Для начала небольшой дисклеймер. runBlocking редко используется в продакшн коде Android-приложения. Обычно он предназначен для использования в синхронном коде, вроде функций main или unit-тестах.

Несмотря на это, мы всё-таки рассмотрим этот билдер при вызове в главном потоке Android-приложения потому, что:

  • Это наглядно. Ниже мы придем к тому, что это актуально и не только для UI-потока Android-приложения. Но для наглядности лучше всего подходит пример на UI-потоке.

  • Интересно разобраться, почему всё именно так работает.

  • Всё-таки иногда мы можем использовать runBlocking, пусть даже в тестовых приложениях.

Билдер runBlocking работает почти так же, как и launch: создает корутину и вызывает в ней блок кода. Но чтобы сделать вызов блокирующим runBlocking создает особую корутину под названием BlockingCoroutine, у которой есть дополнительная функция joinBlocking(). runBlocking вызывает joinBlocking() сразу же после запуска корутины.

Фрагмент из runBlocking():

// runBlocking() function// val coroutine = BlockingCoroutine<T>(newContext, )coroutine.start(CoroutineStart.DEFAULT, coroutine, block)return coroutine.joinBlocking()

Функция joinBlocking() использует механизм блокировки Java LockSupport для блокировки текущего потока с помощью функции park(). LockSupport это низкоуровневый и высокопроизводительный инструмент, обычно используется для написания собственных блокировок.

Кроме того, BlockingCoroutine переопределяет функцию afterCompletion(), которая вызывается после завершения работы корутины.

override fun afterCompletion(state: Any?) {//wake up blocked threadif (Thread.currentThread ()! = blockedThread)LockSupport.unpark (blockedThread)}

Эта функция просто разблокирует поток, если она была заблокирована до этого с помощью park().

Как это всё работает примерно показано на схеме работы runBlocking.

Что здесь делает Dispatchers

Хорошо, мы поняли, что делает билдер runBlocking. Но почему в одном случае он блокирует UI-поток, а в другом нет? Почему Dispatchers.Main приводит к дедлоку...

// Этот код создает дедлокrunBlocking(Dispatchers.Main) {  println(Hello, World!)}

...,а Dispatchers.Default нет?

// А этот код создает дедлокrunBlocking(Dispatchers.Default) {  println(Hello, World!)}

Для этого вспомним, что такое диспатчер и зачем он нужен.

Диспатчер определяет, какой поток или потоки использует корутина для своего выполнения. Это некий высокоуровневый аналог Java Executor. Мы даже можем создать диспатчер из Executorа с помощью удобного экстеншна:

public fun Executor.asCoroutineDispatcher(): CoroutineDispatcher

Dispatchers.Default реализует класс DefaultScheduler и делегирует обработку исполняемого блока кода объекту coroutineScheduler. Его функция dispatch() выглядит так:

override fun dispatch (context: CoroutineContext, block: Runnable) =  try {    coroutineScheduler.dispatch (block)  } catch (e: RejectedExecutionException) {    //    DefaultExecutor.dispatch(context, block)  }

Класс CoroutineScheduler отвечает за наиболее эффективное распределение обработанных корутин по потокам. Он реализует интерфейс Executor.

override fun execute(command: Runnable) = dispatch(command)

А что же делает функция CoroutineScheduler.dispatch()?

  • Добавляет исполняемый блок в очередь задач. При этом существует две очереди: локальная и глобальная. Это часть механизма приоритезации внешних задач.

  • Создает воркеры. Воркер это класс, унаследованный от обычного Java Thread (в данном случае daemon thread). Здесь создаются рабочие потоки. У воркера также есть локальная и глобальная очереди, из которых он выбирает задачи и выполняет их.

  • Запускает воркеры.

Теперь соединим всё, что разобрали выше про Dispatchers.Default, и напишем, что происходит в целом.

  • runBlocking запускает корутину, которая вызывает CoroutineScheduler.dispatch().

  • dispatch() запускает воркеры (под капотом Java потоки).

  • BlockingCoroutine блокирует текущий поток с помощью функции LockSupport.park().

  • Исполняемый блок кода выполняется.

  • Вызывается функция afterCompletion(), которая разблокирует текущий поток с помощью LockSupport.unpark().

Эта последовательность действий выглядит примерно так.

Перейдём к Dispatchers.Main

Это диспатчер, который создан специально для Android. Например, при использовании Dispatchers.Main фреймворк бросит исключение, если вы не добавляете зависимость:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:..*'

Перед началом разбора Dispatchers.Main стоит поговорить о HandlerContext. Это специальный класс, который добавлен в пакет coroutines для Android. Это диспатчер, который выполняет задачи с помощью Android Handler всё просто.

Dispatchers.Main создаёт HandlerContext с помощью AndroidDispatcherFactory через функцию createDispatcher().

override fun createDispatcher() =  HandlerContext(Looper.getMainLooper().asHandler(async = true))

И что мы тут видим? Looper.getMainLooper().asHandler() означает, что он принимает Handler главного потока Android. Получается, что Dispatchers.Main это просто HandlerContext с Handlerом главного потока Android.

Теперь посмотрим на функцию dispatch() у HandlerContext:

override fun dispatch(context: CoroutineContext, block: Runnable) {  handler.post(block)}

Он просто постит исполняемый код через Handler. В нашем случае Handler главного потока.

Итого, что же происходит?

  • runBlocking запускает корутину, которая вызывает CoroutineScheduler.dispatch().

  • dispatch() отправляет исполняемый блок кода через Handler главного потока.

  • BlockingCoroutine блокирует текущий поток с помощью функции LockSupport.park().

  • Main Looper никогда не получает сообщение с исполняемым блоком кода, потому что главный поток заблокирован.

  • Из-за этого afterCompletion() никогда не вызывается.

  • И из-за этого текущий поток не будет разблокирован (через unparked) в функции afterCompletion().

Эта последовательность действий выглядит примерно так.

Вот почему runBlocking с Dispatchers.Main блокирует UI-поток навсегда.

Главный потокблокируется и ждёт завершения исполняемого кода. Но он никогда не завершается, потому что Main Looper не может получить сообщение на запуск исполняемого кода. Дедлок.

Совсем простое объяснение

Помните пример с Handler().post в самом начале статьи? Там код работает и ничего не блокируется. Однако мы можем легко изменить его, чтобы он был в значительной степени похож на наш код с Dispatcher.Main, и стал ещё нагляднее. Для этого можем добавить операции parking и unparking к текущему потоку, иммитируя работу функций afterCompletion() и joinBlocking(). Код начинает работать почти так же, как с билдером runBlocking.

//где-то в UI потокеval thread = Thread.currentThread()Handler().post {  println("Hello, World!") // это никогда не будет вызвано  // имитируем afterCompletion()  LockSupport.unpark(thread)}// имитируем joinBlocking()LockSupport.park()

Но этот трюк не будет работать с функцией runOnUiThread.

//где-то в UI потокеval thread = Thread.currentThread()runOnUiThread {  println("Hello, World!") // этот код вызовется  LockSupport.unpark(thread)}LockSupport.park()

Это происходит потому, что runOnUiThread использует оптимизацию, проверяя текущий поток. Если текущий поток главный, то он сразу же выполнит блок кода. В противном случае сделает post в Handler главного потока.

Если всё же очень хочется использовать runBlocking в UI-потоке, то у Dispatchers.Main есть оптимизация Dispatchers.Main.immediate. Там аналогичная логика как у runOnUiThread. Поэтому этот блок кода будет работать и в UI-потоке:

//где-то в UI потокеrunBlocking(Dispatchers.Main.immediate) {   println(Hello, World!)}

Выводы

В статье я описал как безобидный билдер runBlocking может заморозить ваше приложение на Android. Это произойдет, если вызвать runBlocking в UI-потоке с диспатчером Dispatchers.Main. Приложение заблокируется по следующему алгоритму:

  • runBlocking создаёт блокирующую корутину BlockingCoroutine.

  • Dispatchers.Main отправляет на запуск исполняемый блок кода через Handler.post.

  • Но BlockingCoroutine тут же заблокирует UI поток.

  • Поэтому Main Looper никогда не получит сообщение с исполняемым блоком кода.

  • А UI не разблокируется, потому что корутина ждёт завершения исполняемого кода.

Эта статья больше теоретическая, чем практическая. Просто потому, что runBlocking редко встречается в продакшн-коде. Но примеры с UI-потоком наглядны, потому что можно сразу заблокировать приложение и разобраться, как работает runBlocking.

Но заблокировать исполнение можно не только в UI-потоке, но и с помощью других диспатчеров, если поток вызова и корутины окажется одним и тем же. В такую ситуацию можно попасть, если мы будем пытаться вызвать билдер runBlocking на том же самом потоке, что и корутина внутри него. Например, мы можем использовать newSingleThreadContext для создания нового диспатчера и результат будет тот же. Здесь UI не будет заморожен, но выполнение будет заблокировано.

val singleThreadDispatcher = newSingleThreadContext("Single Thread")GlobalScope.launch (singleThreadDispatcher) {  runBlocking (singleThreadDispatcher) {    println("Hello, World!") // этот кусок кода опять не выполнится  }}

Если очень надо написать runBlocking в главном потоке Android-приложения, то не используйте Dispatchers.Main. Используйте Dispatchers.Default или Dispatchers.Main.immediate в крайнем случае.


Также будет интересно почитать:

Оригинал статьи на английском How runBlocking May Surprise You.
Как страдали iOS-ники когда выпиливали Realm.
О том, над чем в целом мы тут работаем: монолит, монолит, опять монолит.
Кратко об истории Open Source просто развлечься (да и статья хорошая).

Подписывайтесь начат Dodo Engineering, если хотите обсудить эту и другие наши статьи и подходы, а также на канал Dodo Engineering, где мы постим всё, что с нами интересного происходит.

Подробнее..

Будущее, которое мы потеряли

21.01.2021 18:22:37 | Автор: admin

Если бы сбылись предсказания футурологов и фантастов космической эры, вроде Кларка или Азимова, мы бы жили в совсем другом мире. Базы на Луне и колонии на Марсе, к которым мы летим на ракетах с фотонными двигателями, антропоморфные роботы-помощники, подземные города, освоенная Арктика и вечная весна вот будущее, что мы потеряли. Но потеряли ли? Могло ли вообще сбыться это будущее?

Космической эрой 20-го века я называю 50-е и 60-е время, когда человек стремился в космос и надеялся на научно-технический прогресс. Это время великих предсказаний: мир работает во благо людей, новые технологии направлены на то, чтобы упростить нашу жизнь, мы освоили космос, а люди стали добрее и наступило всеобщее благоденствие. Отсылки на эту эпоху постоянно появляются в поп-культуре, например, в Футураме, а ракета Starship Илона маска выглядит будто срисованная с фантастических фильмов 50-х.

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

Примечание. Включите судля атмосферы и поехали.

Наивное будущее начала 20-го века

В начале 20-го века газета The New York Times опубликовала предсказание журналиста, оккультиста и кавалера ордена Почётного Легиона Анри Антуана Жюль-Буа. В 2009 году Анри видел, что на летающих велосипедах (мускулолётах) и летающих автомобилях люди добираются с работы в пригороды, потому что в городах больше никто не живет, а только работает.

А в 1900 году в журнале Ladies' Home Journal Джон Элфрет Уоткинс-младший рассказал, что в 21 веке комаров и мух больше не останется: все страны осушат болота, застойные бассейны и химически обработают все водоемы.

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

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

Что объединяет все эти предсказания? В них нет ничего, что не знали бы люди того времени:

  • Есть железные дороги.

  • В 1899 году началось производство первых дирижаблей-Цеппелинов для полётов.

  • Прототипы самолётов вовсю тестируют. Например, 6 мая 1896 на Аэродроме Лэнгли номер 5 впервые успешно испытан аппарат тяжелее воздуха с двигателем. А в 1900 братья Райт начинают свои эксперименты с планерами.

  • Подводная лодка известна еще с 18 века, когда в 1776 году французский изобретатель Бушнелл сделал подводную лодку Черепаха. Она даже поучаствовала в боевых действиях в войне за независимость США.

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

Индустриализация и гигантизм

Начало 20-го века время индустриализации, которая сменила традиционное аграрное общество. Новое ядро общества промышленность, которая влияла на экономику, культуру и политику. Жизнь обывателя 19 и 20 веков отличается кардинально.

Развитие науки. К 20-му веку создано радио, освоена горячая сварка, есть двигатель внутреннего сгорания, токарные станки. Открыто электричество, пастеризация, периодическая система элементов, найдены возбудители туберкулеза, холеры, дифтерита, тифа, разработаны лекарства и прививки от них.

Урбанизация. Множество людей переехали из сельской местности в города, где ритм жизни и плотность человека на метр совершенно иные. Житель деревни не видел столько лиц за всю жизнь, сколько в городе за день. Странные самоходные повозки без лошадей ездят по дорогам: по твёрдому покрытию, а не по земле.

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

Специализация. Например, заводы в Англии производили детали для станков в США, а Германия заняла рынок красителей. Здесь неизбежно возникновение жёсткой специализации и разделения труда уже в мировом масштабе. Целые страны становились цехами в мировом разделении производства.

Мечты о будущем усиление настоящего.

Мечты индустриального времени гигантские. Например, о небоскрёбах на сотни этажей и широких шоссе между улицами. Так изобразил будущее Нью-Йорка в 1910 году художник Ричард Раммел.

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

Обратите внимание на странную конструкцию из колеса и башни. Это танк, а рядом с ним дома. Так представляли себе войну. Это неудивительно прототипы танков уже существовали, а уже в 1915 году их начали серийно производить британцы. Модель называлась Mk I и их выпустили 48 штук. Правда, в первом же сражении в Первой Мировой 32 из них не доехали до цели. Странно, ведь их производил не Land Rover.

На примере танков хорошо прослеживается гигантомания времени после Mk I машины всё росли и росли. Но подвижность и функциональность ухудшалась, пока Луи Рено не додумался отказаться от тяжелого корпуса и сделать упор на подвижности. Так появился серийный танк Рено FT-17 лёгкий, манёвренный и быстрый.

FT-17 слева, Mk I справа.FT-17 слева, Mk I справа.

Космическая эра

50-е и 60-е золотая эра футуризма. Резкий рост популярности научной фантастики при содействии Азимова, Бредбери или Стругацких, подстегнул изучение научно-технического прогресса. Именно поэтому большинство предсказаний связаны с освоением космоса и колонизацией других планет. Притом они появились до того, как вообще первый человек попала в космос. Люди начали мечтать масштабно о:

  • термоядерных реакторах;

  • подводной агрономии на шельфах северных морей;

  • переводе всего транспорта на аккумуляторы;

  • генетической модификации живых организмов;

  • полётах в космос и на другие планеты.

Человек 50-х и 60-х чувствует, что живет будущим.

Космическая тема проникает в культуру. Дизайнеры по-новому взглянули на цвет и форму: в одежде и интерьерах преобладают яркие зелёные, золотые и красные цвета. Стал активно использоваться акриловый и цветной пластик для производства посуды, предметов интерьера и мебели. Появилась даже мебель из металла и стекла. Квартира обычного бухгалтера стала выглядеть как космический корабль с кислотными коврами и виниловыми обоями. Бытовая техника эволюционирует: появились кофеварки, тостеры, а телевизоры растут в размерах и качестве картинки.

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

Мечты о космосе отражаются на автомобилях. Не только концепты, но и серийные автомобили становятся похожи на космические корабли.

Футуризм 50-х и 60-х в США хорошо описывает серия работ Артура Радебо (с 1958 по 1962 год). Футуристичные автомобили, которые перекрашиваются электромагнитной пушкой, почтальоны с реактивными ранцами, модифицированные растения на автоматизированных сельхозугодьях узнаваемый стиль времени, яркие цвета, улыбающиеся люди. Но на картинках мы сразу видим прокачанные 50-е и 60-е.

Дух времени покорение: космоса, Арктики, морей, природы.

Что советские, что американские фантасты они мечтали практически об одном и том же. Например, в диафильме В 2017 году в СССР построили плотину через Берингов пролив, пустили Енисей и Обь в Каспийское море, создали подлёдные города. Тепло собирают из глубин Земли, а каналы строят атомными взрывами.

Особая сокровищница несбывшихся предсказаний журнал Техника молодежи. Представления советских футурологов это совершенно иной мир со своим колоритом. Подземные города, лифты ко дну моря, импульсные пушки, невидимое магнитное поле, выводящее из строя танки, и самолёты с атомным двигателем.

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

Люди мечтали масштабно.

На Западе прогнозисты предсказывали, что через несколько десятков лет мы построим базы на Луне и посетим Марс. Мы сможем посещать другие планеты, как туристы, например, с помощью космического лайнера на 50 000 тонн, вместимостью 10 000 пассажиров, что предсказывал инженер Дэндридж М.Коул. А математик и учёный Д. Г. Бреннан описывал, что к 2018 году мы откроем антигравитацию и сможем летать.

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

Компьютерами будут заменены все рутинные профессии, вроде менеджеров или бухгалтеров. Рабочие специальности исчезнут из-за автоматизированного производства.Останутся только верхнеуровневые руководители.

Работа будет привилегией и благом. Как следствие, без дела будут сидеть 90% населения, но будут получать хорошие пособия и не голодать. Но те, кто будут работать, будут работать недолго пенсия сдвинется к 50 годам.

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

Предсказания это экстраполяция достижений науки и техники своего времени.

Люди мечтали об усовершенствованном настоящем: больше космических достижений, больше компьютеров, больше науки. Невозможно представить жизнь без автомобиля? Вот вам супершоссе от Дисней: направления обозначены цветами, дорога нагревается и испаряет воду (после дождя или снега), а в пустынях охлаждается.

Экстраполяция хорошо заметна на примере Айзека Азимова. В 1964 году писатель опубликовал эссе в The New York Times, где описал 2014 год. Колонизация полюсов, подводные гостиницы, подземные дома, автоматизированные бытовые приборы, много компьютеров, 3D-фильмы, автономная техника на батареях и парящие над землей машины. Половина электроэнергии будет вырабатываться на АЭС, а в космосе солнечные батареи будут передавать энергию Солнца на Землю.

Может Азимов взял это всё из головы и предсказал, например, IoT или умные колонки? Нет, это всё он увидел на Всемирной выставке в Нью-Йорке в 1964 году. Все эти чудеса именно там и демонстрировались: футуристичные автомобили, поля с культурами с автоматическим орошением, порт в шельфе Арктики, подводные дома. Азимов даже не стал ничего придумывать просто описал, что увидел.

Чудеса Всемирной выставки: футуристические автомобили, города будущего, подводные, арктические и космические колонии.Чудеса Всемирной выставки: футуристические автомобили, города будущего, подводные, арктические и космические колонии.

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

Почему будущее так и не случилось?

Потому что люди представляли не будущее, а усовершенствованное настоящее.

  • Вокруг фабрики и заводы на которы ломят спины люди? Хотим автоматическое производство или роботов.

  • Вокруг космическая гонка? Хотим летать на Марс на работу, а отдыхать на даче на Луне, самолеты с атомным двигателем и покорение арктики.

  • Вокруг корпорации с миллиардными активами, которые диктуют свои правила и наращивают власть? Видимо, наше будущее киберпанк, где корпорации заменили государство, где есть частная полиция и не работая в корпорации ты будешь бомжевать.

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

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

  • Первый мобильны телефон создала Моторола в году. Но это устройство они увидели в сериале Стартрек.

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

  • В Пятом элементе та же полиция, но в странной броне, машины, но летают, и генетические эксперименты, правда на недосягаемом уровне.

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

В предсказаниях прошлого о будущем нет будущего только аллюзия настоящего.

Наш разум просто создает привычную картину мира, помноженную на мечты гадает. Именно поэтому все футурологи почти не описывали социальное и политическое устройство они не могли представить мир иначе. Просто потому, что это тяжело.

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

Например, человек заснул в 1720, а проснулся в 1820ж В целом, он бы не заметил большую разницу: Люди также живут в деревнях, работают в полях, пашут животной силой, особо не следят за временем: ещё светло работай. В обществе всё ещё жесткое разделение между крестьянами и аристократами (элитой), которая живет в городах, а из транспорта гужевые повозки, связь между городами медленная.

А вот если бы этот же человек проснулся бы в 1920, то всё было бы иначе: огромные города, застроенные гигантскими высотками, люди массово работают не в полях, а в огромных зданиях с трубами, из которых извергаются клубы дыма. Город кипит, шумит и вообще страшно, потому что по улицам ездят странные железные повозки без лошадей. По небу в это время летят большие железные птицы, а кто-то передает свои слова по воздуху (радио).

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

Примечание автора. Рассказав о том, что предсказания прошлого лишь фантазии, я не ставил целью кого-то принизить. Я сам был бы рад, если бы предсказания о великих достижениях сбылись. Больше всего мне грустно, что не наступило будущее космической эры 50-х и 60-х. У меня в руках коробочка, которая в 1000 раз мощнее компьютеров, что запустили людей на Луну. И что я с ней делаю? Запускаю эмодзи с ракетами в чатиках. Поэтому мне грустно, когда ночью я смотрю на звёзды. У нас есть всё технологии, чтобы создать будущее, как его представляли в космическую эру и создать эпоху больших достижений. У нас есть всё, но нет желания. Почему? Я не знаю.

Заходите к нам Телеграм-чат, чтобы покритиковать статью и автора. Подписывайтесь на канал Dodo Engineering: там мы постим анонсы статей, подкасты и делимся тем, что нам интересно.

Подробнее..

Быстрый, простой, сложный как мы выпилили Realm

27.01.2021 12:10:40 | Автор: admin

Когда на собеседованиях я спрашиваю у кандидата с какими базами он работал и что делал, то чаще всего слышу про Realm. Типичный ответ: он быстрее и с ним проще работать, создал конфиг, описал модель, готово. Но за все удобства Realm придётся расплачиваться: он вроде бы решает одну проблему, но тут же добавляет пару своих. А последствия, недостатки и проблемы обычно обсудить не получается: нужен опыт использования в пару лет, а его обычно нет.

Мы тоже использовали Realm 3 года подряд. Сначала он помогал, потом раздражал, пару раз выстрелил и в конце чуть не вогнал команду в депрессию. В итоге мы удалили Realm из проекта, потому что это сложный инструмент, который нужно правильно обслуживать, а простота интеграции обманчива.

Примечание. Realm читается как рэлм или рилм, но давайте только не реалм

Зачем нужна база данных для заказа пиццы?

Кратко незачем. База данных сначала прикрывала плохое API.

В 2017 году Dodo Pizza решила написать свое приложение. Серверная часть уже работала 6 лет и обслуживала 250+ пиццерий (на начало 2021 почти 700). Много работы было сделано для бизнеса, а для клиентов был только сайт нужно делать приложение.

Чтобы подключить приложение нужно новое API и срочно, ребята торопились. Чтобы ускориться, часть работы разделили: API было больше похоже на прокси для базы данных, а часть логики решало приложение.

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

Весь жизненный путь приложения сложно угадать на старте, база данных могла как сильно помочь, так и оказаться избыточной. Немного промахнулись, оказалось, что проще без неё.

Realm vs Core Data

Сложно вспомнить почему выбрали Realm, а не Core Data. Скорее всего, так было проще: схему базы рисовать не нужно, объекты создаются сразу в коде, работает быстрее, да и опыт работы с ней был. Так и поехало.

Как работало

Первую версию приложения сделала команда на аутсорсе. Уже тогда понимали, что проект будет жить долго, стран будет много, фич навалом. Тогда казалось важным заложить поддержку работы в офлайне. Если нет, то, хотя бы, настроить восстановление между сессиями работы приложения, чтобы меню появлялось сразу на запуске, без скачивания. Архитектурно это заложили с первой строки.

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

  • получили данные из сети;

  • положили в базу, разметили связи между таблицами;

  • прочитали из базы, связанные объекты подтянулись сами;

  • переложили данные во view-модели, а дальше уже MVVM.

Realm требователен к переключению потоков, поэтому вся работа с базой архитектурно заворачивалась в один фоновый поток. Это было правильное и удачное решение (без шуток), но к нему мы ещё вернемся.

Недостатки Realm

После релиза проект перешел на поддержку от аутсорса к внутренней команде, её для этого только собрали. Новым разработчикам, конечно, не нравятся все старые решения, но понимания проекта не было, поэтому особо ничего не меняли рефакторили только рядом с бизнес-фичами. С Realm работать не нравилось, но это был лишь вопрос вкуса.

Realm накладывает ряд ограничений сам по себе.

Хранит только сырые данные. Enum надо перекладывать в String или Int, Optional в RealmOptional, массивы в List, обратные ссылки в LinkedList. Чтобы превращать это в нормальные объекты надо писать какие-то конвертеры. В итоге кода становится сильно больше, модели дублируются, проект становится хрупче.

По всему коду размазано обращение к Realm: он импортируется в файл, передается в качестве параметра, из базы тянутся объекты. Мы активно заворачивали всё в репозитории, чтобы скрыть работу с базой, а интерфейсом выходил доменный объект. Но это дополнительный код и слой в архитектуру.

Работа с базой превратилась в целый слой, который надо поддерживать: писать маперы, обертки. Добавить новую сущность это слишком много ручной работы: создать Entity, переложить из DTO в нее, потом из Entity в доменную модель. Это всё ещё и протестировать надо, а мы даже на UI выводить ничего не начали.

Realm-объект должен быть классом. Все его свойства надо пометить как динамичные, при этом нельзя их сделать немутабельными, а конструктор Entity не имеет смысла, он всегда пустой. В итоге легко добавить свойство, но забыть поставить его из всех нужных мест.

Realm большая и очень тяжелая зависимость. Наш проект весил 55 Мб, Realm занимал 7 и очень долго билдился. Мы решили проблему пребилдом перенесли билд на этап pod install, стало реже и легче. Но плагин компиляции стал влиять и на другие поды, например, он не работал с XCFramework и мы не могли обновить поды, которые перешли на него. Убрать пребилд мы уже не могли, потому что привыкли к нормальной скорости сборки.

Ну и Realm мог бы и складывать свои файлы в одну папку!

По умолчанию Realm складывает всё в папку DocumentsПо умолчанию Realm складывает всё в папку Documents

Проблемы в проекте из-за недостатков

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

Realm стал целым слоем обработки данных, все операции проходили через него. При этом, вся архитектура с бекапом в Realm не работает, если на девайсе мало места. Из-за размера фреймворка не получится переиспользовать код и написать, например, аппклипс: из 10 доступных мегабайт он займет все 10.

Страдает производительность. Обратные связи могут порождать очень большие и сложные деревья, сохранение и запись могут растягиваться. Мы столкнулись с этим в меню, когда появились изменяемые комбо. В комбо были слоты, каждый мог содержать десятки ссылок на продукты. При получении меню запись и чтение из базы занимало 2/3 времени: сетевой запрос проходил за полсекунды, а ещё одну мы просто разбирались с базой в приложении.

Офлайн-режим нашему приложению совсем не нужен, а персистентость нужна лишь частичная. Нет смысла хранить всю корзину, если можно обновить её с сервера по ID. Даже если захотим хранить её, то это проще сделать другим способом: на уровне кеша сети, ручным кешем оригинального JSON или конвертацией доменной модели в файл через Codable.

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

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

Сложно писать тесты. Непонятны зависимости, часто есть только одна, на базу. Что он из неё читает? Города, профиль, корзину? Иногда нужные записи находятся в нескольких таблицах, для теста мучительно ищешь их по дебагеру. Из интерфейса функции совершенно ничего не понятно:

// К каким таблицам пойдёт запись? От чего зависит работа функции?public func saveOrder(_ order: Order, to realm: Realm) 

При обновлении Xcode каждый раз ломался CI. Обновление Realm его быстро чинило, но это лишние нервы каждый год.

Всё вместе это приводило к тому, что весь код вокруг Realm превращался в легаси:

  • его сложно рефакторить;

  • надо помнить про миграции;

  • могли быть неожиданные ошибки.

Это всё неприятно, но не критично: чуть больше кода, чуть меньше контроля, но работает.

Реально бесил лишь перформанс меню, но это можно было решить, стоило только сфокусироваться и попрофилировать. Коллеги на Android столкнулись с отсутствием каскадного удаления, но на iOS мы достаточно хорошо обработали это вручную, когда перед добавлением удаляли все прошлые объекты одного типа. Это же спасало и от разбухания базы.

Многие проблемы можно было решить инфраструктурно, но это только усложняло код. Например, чтобы не работать с объектами базы напрямую мы завели репозитории, которые конвертировали Realm Object в доменные объекты. Но это всё дополнительный код и усложнение.

При этом надо постоянно иметь дело с сырыми типами в Realm. Особенно сложно было с объектами большой вложенности, где для вложенных объектов тоже пришлось создавать репозитории, конвертеры и всё такое. Со всем этим легко промахнуться в перформансе: читаешь и конвертируешь огромную модель, а потом берешь от неё только одно свойство.

Почему решили удалить две последние капли

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

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

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

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

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

Ситуация всех вымотала, но мы чувствовали, что проблема была в нашем коде. Мы думали, что виноват не Realm, поэтому снова ничего не делали, но затаили обиду.

Realm мигрирует без учета версии схемы. Могут быть сложности при повторном переименовании Property.

Откат. Через два месяца мы столкнулись в непонятным крешем Realm accessed in incorrect thread. Это было очень странно, потому что мы были точно уверены, что работаем с потоком правильно: вся работа с базой велась строго в отдельном потоке. Креш случался в самых разных местах, стабильности не было. Искали его неделю: у нас был pull request на версию с ошибкой, мы отревьювили 700 файлов 3 раза, но не смогли найти проблему.

Миграций базы уже не было, поэтому в качестве быстрого решения мы откатились на прошлую версию приложения. Это была ошибка. С откатом всё стало только хуже: Realm не мог прочитать свой файл из-за разницы версий самого Realm, он не смог прочитать свой файл. Повезло, что мы обновили только 1% пользователей и вовремя остановили. Откат обошелся в 3000 крешей.

Креши нам показали место, на которое мы не обратили внимание: версия Realm обновилась. На устранение проблемы ушла ещё неделя: написали удаление файла Realm в случае проблем с его чтением, дождались ревью и раскатки до пользователей.

Стало ясно, что так выкатывать приложение нельзя, каждый раз что-то случается, в этом каждый раз задействован Realm. Конечно, на ошибках мы учились, но так подставлять нельзя ни пользователей, ни бизнес. Каждый новый релиз стал восприниматься как смертельное решение, страшно было катнуть даже маленький фикс с переводами. Есть ли креши? Никто не знает, он рандомный: UI-тесты иногда показывали, а иногда и по 5 раз проходили без проблем.

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

Можно ли было решить каждую из проблем? Да, конечно. Можно ли было угадать что-то наперед? Наверно да, вопрос миграций стоял давно. Надо ли это было делать срочно? Нууу, риски были но 3 года же нормально работало, что случится?

Краткий итог критичных проблем:

  • проблемы с несколькими миграциями одного поля;

  • проблемы многопоточности в новой версии Realm.

Примечание. Забегая вперед скажу, что ошибку в Realm поправили в версии 5.3.5 20-го августа, а столкнулись мы 6-го. Фикс Realm вышел через две недели после наших проблем, но брейкинчедж появился 16 мая проблему починили только спустя 3 месяца. Нам просто повезло, что мы не обновились раньше.

Как продали бизнесу удаление Realm

В итоге за 3 месяца мы трижды столкнулись с крупными проблемами на релизе, каждый раз с новыми. На починку суммарно ушло 20 дней. Это время кажется какой-то бесполезной антиработой.

iOS команда не нашла аргументов за то, чтобы оставить Realm. При этом он мог нам заблокировать любой релиз новым неожиданным образом.

Увы, тут было не до продажи просто поставили перед фактом.

Выпиливание Realm не та задача, которую можно сделать в фоне, да ещё и в конечный период. Пришлось ставить ультиматум, что релизить в таком состоянии мы не можем, надо остановить разработку на какое-то время и выпилить целый слой в приложении. За дело берутся все команды, на тот момент это было 4 iOS-разработчика.

Естественно, первый вопрос от бизнеса на сколько времени останавливаемся. Ответ примерно месяц. Офигели все.

План работ по сносу

Делать такую задачу без плана самоубийство. Надо составить план задач и отслеживать прогресс.

Ключевые строки. Мы выписали ключевые строки, по которым можно отслеживать как много Realm используется в проекте. Это могло бы быть мерилом качества инкапсуляции Realm. Нашли 3300 мест. Погнали выпиливать.

Но такая верхнеуровневая метрика не рассказывает о сложности работы, только её количество.

Домены. Тогда мы выписали наши домены. За 3 года работы над приложением мы развязали домены, работать над ними можно было параллельно. Получилось так:

  • меню;

  • города и страны;

  • профиль;

  • адреса;

  • активные заказы;

  • корзина и детали заказа;

  • оценка заказа;

  • очередь синхронизации продуктов в корзине.

По каждому домену оценили сколько упоминаний их объектов, а потом всё сложили. Получилось 1500 мест.

Разделить работу оказалось удобно по доменам: одной команде один домен. Начали с самых больших и критичных: меню, корзина, активные заказы.

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

Ревизии. Каждый день делали ревизию по количеству упоминаний, строили график нашей скорости. Дольше всего выпиливали Realm из меню, в нём было 26 видов объектов с 852 упоминаниями. Над ним работало 2 человека и потратили 112 человеко-часов.

Многие домены очистились меньше чем за день. Это было шоком: мы пару лет бесились с того, как сложно работает корзина, а оказалось, что отказаться от кеширования в Realm можно за несколько часов.

Как удаляли

Простое удаление. Прозвучит странно, но где-то просто оказался лишний код. Например, в корзине у нас есть очередь из продуктов, которые ещё не были отправлены на сервер. Она нужна, чтобы не потерять продукт при сетевой ошибке. Мы сохраняли эту очередь в Realm, чтобы продукты не терялись даже между запусками. Хорошо, что это предусмотрено, но реальный шанс так потерять данные очень низкий. Для скорости выпиливания мы отказались от бэкапа корзины.

Замена объектов. Мы уже начали упрощать работу с Realm оборачивая его в абстракцию репозитория. План был такой: про способ хранения знает только репозиторий, а в приложение он отдает только доменные модели. Переписать успели процентов 30, это сильно помогло при удалении. При переписывании мы весь слой старых репозиториев с Realm заменяли на самописные репозитории, которые конвертировали структуры моделей через Codable и сохраняли в файл. Данных у нас не так много, способ подходит.

Самое сложное в таком случае правильно поменять Realm класс Entity на структуру: нужно поменять способ мутации объекта, ведь теперь он каждый раз копируется. Суммарно ошиблись пару раз в тех местах где тестов ещё не было.

Обычно работы по замене выглядели так:

  • Убираем наследование от Object, убираем всё @objc dynamic декларации у property, меняем класс на структуру (если надо).

  • Меняем запросы к Realm на обращение в наш репозиторий.

  • Правим мелочи: тесты, доступ.

  • Чистим: меняем типы property с сырых на доменные. Больше никаких непонятных Int, только Enum.

Переписывание. Какие-то части было проще переписать слишком много завязано на логику самого Realm. Так случилось с меню. Домен большой, но у нас было написано довольно много тестов на него, поэтому можно было переписать логику по старым тестам, временами ещё и удаляя легаси.

Ещё проблемы, которые нашли

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

Адреса. Они состоят из 3-х слоев: объект адреса, набор полей, которые его описывают, у каждого поля есть его тип. Например: нужна улица, её значение Ленина и она часть адреса Ленина 25. Простая система, но из-за обратных ссылок в коде можно было ходить по вложенности в любом порядке: не только 1-2-3, но и 1-2-1-2-3-2. Это сильно усложняло код. Написали тесты, поменяли структуру моделей, отрефакторили, теперь можно двигаться только в одном направлении 1-2-3 читать стало проще.

Города. В нашем домене встречаются две модели городов:

  • короткая нужна только для списка городов на старте приложения;

  • полная, которая подгружается после того, как выбрали город и нужна для работы приложения.

Оказалось, что в Realm они были описаны одной полной моделью, а данные могли как оказаться в ней, так и нет. При этом приложение стартовало с простой модели, а потом докачивало данные. Естественно, могло и не докачать, и приложение бы работало как есть.

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

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

Пауза: фидбек и переоценка сроков

После недели интенсивного рефакторинга мы взяли паузу: стабилизировали релиз, начали брать бизнес-задачи. За неделю мы сделали очень много по доменам упоминание снизилось на 60%. В итоге у нас осталось 3 несвязанных домена: города, оценка заказа и очередь продуктов в корзине.

Релизить всё ещё было страшно, но мы были вдохновлены результатами и даже выпустили релиз.

С новым XCode мы получили новые проблемы с Realm, но и новые пути решений у нас тоже были:

Как нам казалось, до конца проекта оставалось пару недель, поэтому 2 из 3-х команд начали брать бизнес-задачи, а одна продолжила рефакторить проект.

Не так страшны первые 90% рефакторинга, как вторые 90%

В последнюю очередь мы меняли логику для городов. В городах всё оказалось сильно сложнее:

  • На старте приложения мы берем города из JSON в бандле приложения, чтобы можно было показать список городов без задержек и как можно быстрее перейти к меню.

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

  • После выбора города скачивалась полная модель она хранится отдельно. Приложение должно работать только с ней.

  • Информацию о городе получали в десятки мест приложения, каждый раз по ID с опциональным результатом. Это всё надо было унифицировать, а потом понять, что мы ничего не сломали. На всё подряд тесты не напишешь, нужен другой подход.

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

Флоу получился такой: мигрируем данные, пользователь выбирает город, докачиваем детали города, кешируем и показываем меню. Внутри есть особенности для разных стран, но сейчас они для нас не важны, да и всё скрыто внутри фреймворка.

Этот этап оказался сложным технически и занял пару месяцев работы. Сам Realm удалился за неделю, в остальном мы рефакторили и выносили код во фреймворк, чтобы однозначно сказать, что всё работает правильно.

Раньше в приложении много где дублировался код:

  • Взять текущий идентификатор города.

  • Получить запись из базы по идентификатору.

  • Взять первый объект в ответе, это считаем текущим городом.

  • Повторить в каждом месте.

Надежность перешла на уровень зависимостей между модулями. У приложения всегда есть текущий город, он строго один.Если города нет, то его надо выбрать пользователю (или получить от сервера при миграции) и только потом стартовать основную часть приложения.

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

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

Миграция

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

В этом нам сильно повезло: когда мы отказались от миграций в Realm, мы перенесли все нужные для работы ID в UserDefaults. Мы знали ID корзины или выбранного города, поэтому на старте нужно было только получить новые данные от API.

Если бы критичные данные были только в Realm, то пришлось бы удалять базу в несколько апдейтов сильно растягивая переход по времени. Или вообще просить пользователя выбрать город ещё раз.

Храните критичные ID вне базы пригодятся.

Механизм миграции пригодился и для UI-тестов: можно пропустить выбор городов если сразу передать правильный ID. За счет миграции мы получим все нужные данные с сервера и сразу покажем меню, пропуская выбор страны и города.

Чистка после Realm

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

/// Давным давно, когда API был не очень, мы использовали Realm: собирали все ответы в одном базе, а потом читали из неё./// Больше такой фигни нет и мы всё аккуратно раскладываем по репозиториям./// Теперь на месте Realm вот такой маленький шрам, для того чтобы очистить старых клиентов./// Удали этот код, если читаешь это в 2022 году.internal final class RealmCleaner {    let fileManager = FileManager.default     /// Remove all realm files    /// - Returns: total size of removed files    func removeRealmFiles() {        let pathes = filePathes()        fileManager.removeItems(at: pathes)    }     private func filePathes() -> [URL] {        let baseURL = fileManager.documentsDirectory().appendingPathComponent("default.realm")        let realmURLs = [            baseURL,            baseURL.appendingPathExtension("lock"),            baseURL.appendingPathExtension("note"),            baseURL.appendingPathExtension("management"),            baseURL.appendingPathExtension("log_a"),            baseURL.appendingPathExtension("log_b")        ]         return realmURLs    }}

Мы замерили размер удаляемых файлов: в основном меньше 15 МБ, но было и несколько пользователей с размером в 150 и даже 300 МБ. И это не девайсы тестировщиков.

Новое хранилище

Какие-то данные всё равно хочется хранить. Мы уже избавились от Realm-объектов, перевели все на доменные. Хочется использовать их так, чтобы больше не надо было конвертировать из одного типа в другой только для хранения. Core Data таким образом тоже не подходит.

Мы собрали требования к хранению:

  • Хотим работать с доменным объектами.

  • Умеет работать с разным количеством объектов: хранит как один объект для типа (профиль пользователя может быть только один), так и коллекцию (список из городов).

  • Хранить можно в памяти или с кешем на диск. Приложение должно работать даже если на диске нет места. Кеш на диске опционален.

  • Для кеша на диск готовы запариться чуть больше, если надо.

  • Объемы данных всегда небольшие (меньше мегабайта) и слабо связанные реляционная БД не нужна.

  • Допускаем, что каждый фреймворк приложения может хранить данные по-своему. Для сообщений база данных нужна, для кеша городов точно нет, а что-то лежит в UserDefaults. Но стандартизируем подходы по возможности.

Мы разделили способ хранения и количество объектов, которое можно хранить. В объявлении репозитория видно ключевые части:

public class ProfileRepository: SingleRepository<ProfileModel> {    public init() {        super.init(storage: InMemoryStorageWithFilePersistance().toAny())    }}
  • SingleRepository хранит один объект.

  • Хранит только модель ProfileModel.

  • Хранит объект в памяти и кеширует на диск.

  • Ещё есть InMemoryStorage и FileStorage. Для хранения на диске модель должна реализовать протокол Codable, а для хранения в памяти это не нужно. Для доменной модели это вполне подходит и легко поддерживать. Теперь отдельную модель для записи в базе создавать не нужно.

Коллекция пиццерий хранится в CollectionRepository: синтаксис похож, только наследуемся от другого класса.

public class PizzeriaRepository: CollectionRepository<PizzeriaModel> {    public init() {        super.init(storage: InMemoryStorageWithFilePersistance().toAny())    }}

Примечание. Про устройство рассказывать долго: там и box typing, и работа с асинхронностью. Пишите в комментарии, если интересно узнать как работает внутри.

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

Мониторинг

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

Запросы во время миграции. Мы пропустили, что во время миграции может восстановиться push-токен от Firebase и мы отправим его в наше API. Хедеры запроса зависят от текущей страны, а она в процессе миграции. Запрос не проходил, возник фон некритичных ошибок.

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

Крешрейт к Новому годы мы довели до 99.95%. Можно улучшать ещё, ведь теперь креши не в рандомных местах Realm, а только в нашем продукте и понятно как их чинить.

Результаты

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

Домены. От изначальной проблемы связанных доменов почти ничего не осталось: всё работает независимо, мы активно разделяем приложение на фреймворки. Работать с такими модулями удобно: быстро компилируются, мало зависимостей и связей, понятная ответственность, легче тестировать. Можно даже из одного модуля создать отдельное приложение-витрину и написать для него UI-тесты.

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

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

Объём. Приложение уменьшилось на 8 МБ от Realm, запустили процесс по ревизии размера и уменьшили ещё на 10 МБ за счет бандла. Начали трекать размер приложения при каждом релизе.

Сроки. От начала проекта до полного выпиливания прошло 3,5 месяца, но все команды остановились только на одну неделю. В остальное время разработка продолжалась.

Потоки. В середине августа у Realm вышло обновление, которое полностью починило нашу проблему с потоками. Нам повезло, что мы натолкнулись на проблему в июле, уже перед фиксом, но впервые она появилась в мае. Получается, что на исправление у ребят ушло несколько релизов и 3 месяца работы. Всё это время понять статус фикса невозможно: вроде чинят, а оно всё равно вылетает.

Стало проще. Мы нашли старые баги и заревьювили несколько архитектурных задач. Работать стало проще. Теперь в приложении всегда можно быть уверенным, что город имеет все данные и не нужно возиться с опциональностью. Стало проще разрабатывать: у нас получились нормальные доменные объекты, удалили целый слой приложения и маперы из одного в другое, кое-где переосмыслили архитектуру. Упростилось деление приложения на фреймворки, можно идти в сторону аппклипс. Домен городов уехал в отдельный фреймворк.

Блокировки. Перестали блокироваться релизами Realm при обновлении Xcode, смогли обновить Cocoapods и поды на XCFramework.


У каждой зависимости своя цена и её надо прикидывать с самого начала. Нормально стартануть в типовой зависимостью, но надо иметь план на то, как выпиливать, как менять и как это поддерживать.

Realm сложный инструмент и его надо уметь обслуживать. Простота интеграции бывает обманчива.

Больше новостей про разработку в Додо Пицце я пишу в канале Dodo Pizza Mobile. Также подписывайтесь на чат Dodo Engineering, если хотите обсудить эту и другие наши статьи и подходы.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru