Команды программистов из 3-7 человек это идеальная машина по быстрому созданию качественного продукта. Слишком много - и все погрязнут в бесконечных обсуждениях, слишком мало - будет сбиваться ритм и снижаться продуктивность и качество.
Я мало что понимаю в менеджменте, поэтому меня больше интересуют вопросы конфликтов и улучшение инспекции кода для улучшения продукта и сплочения команды.
Год назад мы взяли в кредит дом. Старенький, с деревянными продуваемыми окнами, без утепления и с основным дровяным отоплением. Коллеги брали наоборот новые, рядные дома где всё уже готово. Я расчитывал что так я выиграю в цене, гибкости, а в итоге и в качестве.
Заказав 16 м³ дров за ~700€ и протопив сезон, я понял что это вам не квартирные удобства. Хотя дрова самые дешёвые и экологические в отоплении, с ними неудобно
Надо дровник и тратить пару дней на их переноску и выкладывание
Дрова бывают разные и по-разному разгораются (берёза лучше осины)
Зимой надо минимум 2-3 вязанки дров за вечер что-бы натопить. Таскать их в мороз и складировать у плиты — то ещё удовольствие.
Топить через маленькую плиту, подбрасывая дрова каждые пол-часа/час. Это слишком отвлекает от домашнего быта.
Температура неравномерна и не постоянна
Когда топишь то возле плиты воздух может быть 30˚C уже, тогда как где-то в дальнем углу ещё 18˚C.
Приходишь домой — дома 15˚C, надо ждать 2 часа покуда прогреется часть этажа. Встаёшь утром - тоже холодно, душ принимать становится просто опасным для здоровья — легко простудиться.
Кроме того, есть теплонасос воздух-воздух, но его мощности хватало только на четверть дома, он достаточно шумный и при морозах его мощность сильно падала. Поэтому приходилось дополнительно топить электрическими конвекторами. Но электричество достаточно дорогое — примерно +50€/мес при отоплении только спальни. Отапливать весь дом электрическими обогревателями влетело бы в ~300 €/мес.
Уже исходя из требований, выходит что минимальная мощность источника отопления при зимних тепло-потерях — 20 квт
Я рассматривал разные источники тепла:
Газ хоть и дешёвый, но проводить трассу от Eesti energia, или создавать поземное хранилище вышло бы слишком дорого. Баллоны таскать было бы слишком напряжно, да и с точки зрения безопасности казалось опасным
Дизелем топит сосед. Достаточно дорого, на зиму запас проблематично делать, нужен большой бак. Удобно если бы у меня была машина на дизеле
Воздух-вода очень заманчивый вариант экономичностью и автономностью, но как и воздух-воздух, эти насосы теряют мощность при низких температурах. Грубо говоря при -30 градусах, насос который рекламируется как 15 квт, и стоит 8 к€ станет работать как 3 квт, периодически подключая ТЭН
Вода-вода, или как у нас рекламируют maaküte ("отопление земли"). Закапывать трубы на участке вертикально (15м) или горизонтально (пару км) мне кажется сомнительным — будет промораживать землю, да и подходящего места нет.
Камин. Если продолжать топить дровами то вместо старой узкой печки можно использовать камин. Но для этого надо установить металлическую сердцевину и провести в разные комнаты отводы тёплого воздуха — это улучшит равномерность прогревания, но не автономность
Для равномерного прогревания дома, система тёплых полов самая идеальная. Но для этого трубки должны быть встроены в бетон, а у меня увы, перекрытия деревянные.
Я получил 3 предложения от 5 фирм, в итоге одно из них выбрал и чуть-чуть поменял.
Выбрал я в итоге пеллетный котёл от датской NBE, RTB30 (30квт). Степень сгорания более 91%, что значит что для смены бака надо сжечь порядка тонны пеллетов (т.е. менять золу надо будет раз в пару месяцев)
Вместе с котлом поставляется компрессор, который подключён к горелке. После прогорания, горелка автоматически чистится воздухом под давлением
На самом котле есть dashboard где показано подключение и состояние в целом. Внутри - меню offline управление.
Котёл подключается по wi-fi к интернету к порталу stokkercloud.dk, где можно посмотреть как публичную информацию разных котлов в мире, так и залогиниться в админку своего котла (об этом ниже). Админка кстати написана на ангуляре, а бэкэнд на php
Отапливается всё 10 радиаторами соединённых в два контура (верхний и нижний этажи). Отдельный насос на этаж в случае неполадки позволит отключить только часть дома, даст более точное управление теплораздачей за счёт регулируемого давления (скорости движения воды). Не уверен, но думаю что разница в высоте как-то влияет на давление в системе.
В дополнение к этому, старый маленький электрический бойлер для горячей воды я решил тоже сменить на 150л и подсоединить к котлу. Зимой это позволит сэкономить на электричестве.
Сеть из труб достаточно интересная и полна нюансов.
сразу на выходе из котла стоит труба соединяющая горячую воду сразу со входом. Эта обратка разрывается как только котёл достаточно нагреется.
у котла внизу есть предохранительный клапан, который откроется самостоятельно если в трубах будет критическое давление воды
вода в бойлере и вода в радиаторной сети никак не связаны и нагрев происходит за счёт теплоотдачи, т.е. вода в радиаторах никуда не девается и циркулирует по кругу
поскольку горячая вода находится в подвале, то при открытии крана, ей прийдётся подняться на высоту 3м по трубам длиной ~20 м, под давлением насосной мембраны в 2.5 атмосфер. Это достаточно долго (30 секунд) и хочется мгновенной горячей воды. Поэтому горячая вода замкнута в цикл и гоняется она постоянно циркуляционным насосом.
Из проблем или неудобств, стоит выделить высокую минимальную мощность. Минимально настраиваемая мощность в 10% потребляет достаточно много топлива и быстро нагревает дом до 26˚C для экономии осенью проще поставить вариацию по времени (включение через каждые 2 часа), но частое включение горелки плохо сказывается на её надёжности. В этом плане идеально было бы подключить дополнительно систему воздух-вода для температур 0 — +10˚C. Ближе к зиме я настрою постоянное включение
Первый факап - котёл должен иметь металлический дымоход, что-бы кирпичная кладка не разрушалась из-за конденсата который образуется из-за низких температур дыма. Этот конденсат стекает по трубе вниз в ведро (можно увидеть на фото). Чисто эстетически это неприятно, но факап установки в том, что установив трубу мне перекрыли дымоход из бани. Отчасти это была моя вина, прийдётся делать электрическую баню.
Второй факап более тяжёлый. Несколько месяцев я бился с регулировкой котла - мощность его обычно не превышала 30%, но отапливать весь дом он не мог почему-то. Он достаточно быстро выключался, показывая что достиг своей температуры, а обогреть два этажа так и не мог. В лучшем случае он мог хорошо обогревать только один этаж.
Я было уже начал думать что сам котёл слишком мощный вышел, а трубы и батареи хилые, но..
После того как я стал поднимать ожидаемую температуру на выходе, ситуация ещё больше усугубилась - до какой-то границы (70 градусов) температура росла, а потом резко перекрывалась , вводя котёл в экстремальные температуры в 90 градусов, провоцирующие аварийное отключение.
После экспериментальных опытов было установлено, что рабочие перепутали открытие и закрытие насосов - в настройке или сами провода. Когда насосы должны были увеличить процент открытия они закрывались и наоборот.
Из-за этого в лучшем, полуоткрытом случае вода подавалась только на 50%, показывая при этом правильные температуры. Насосы при этом надрывались на максимальной мощности, но потока было мало и нагретая вода направлялась обратно в котёл, перегревая его.
Пеллетные гранулы бывают 6мм и 8мм в длину. Разница лишь в том, что это влияет на объём который забирает hopper из бака (для распаливания 40г например надо).
Гранулы бывают разного качества. Хреновые делаются из коры деревьев и поэтому могут содержать песок, который может навредить трубопроводу или камере сгорания (закоптится). Похожие последствия бывают скажем если топить дровами из хвои.
В Эстонии заказать можно машину (2-4 тонны), а можно купить пакетами по 15-25 кг. Расчитывая расход осенью в 10кг в день (как вы видели на скриншотах выше), получается 300кг в месяц (40 евро/мес). В зимний период я думаю расход будет раза в два больше (под 100 евро/мес). Оптовые производители:
Pro Wood (165 евро за тонну, в 15 кг пакетах чуть дороже, но в строительных их же продукция в магазинах выходит аж по 230 евро за тонну)
Pelletiküte (160-180 евро за тонну) - увы эти товарищи в Таллинне складов не имеют, можно только заказать доставку (4 тонны самое оптимальное)
Nordic Pellets (в строительных последние можно найти по 215 евро за тонну, 20 кг пакеты)
Тут всё зависит от того есть ли у вас подходящее помещение для хранения топлива. Я пока предпочитаю покупать пакеты специально для бака, зимой надо будет брать чуть чуть с запасом
После нескольких месяцев отопления, пришёл к таким заключениям..
Автоматика работает по приоритетам - сначала нагрев воды (DHW), потом нагрев первого этажа (zone 1), потом уже нагрев второго этажа.
Если вы поставите скажем на zone 1 заведомо недостижимую температуру (80 градусов при температуре котла в 65), то весь напор воды будет идти только на первый этаж, а второй будет в холоде
Температура котла прыгает волнами, что-бы выровнять её, можно повысить минимальную мощность
В прошлой статье я начал изучать микросервисный подход, но до полноценного решения тогда не дошёл. Тогда я пробовал сам поднимать процессы на php, управлять ими, связывать между собой через очереди потому что монолит не мог справиться с асинхронными задачами. Но чего у меня реально нехватало, так это нормальной изоляции и деплоймента, межсервисного взаимодействия и стабильной работы.
Images (образы) это изолированная среда, как правило с конкретным языком (node, php) или библиотекой. За это отвечает Dockerfile, который ссылается на существующий, скачиваемый образ на докер хабе.
Что-бы увидеть установленные образы, есть комманда docker images .
Контейнеры это запущенные образы которые изолированы от host-машины. Обычно контейнер поднимается для какого-то сервиса, поэтому у него есть своё название, явно объявлен маппинг сетевых портов на host-машину и способ монтирования файловой системы - за это отвечает docker-compose.yml
Что-бы увидеть установленные образы, есть комманда docker images .
Контейнеры это запущенные образы которые изолированы от host-машины. Обычно контейнер поднимается для какого-то сервиса, поэтому у него есть своё название, явно объявлен маппинг сетевых портов на host-машину и способ монтирования файловой системы - за это отвечает docker-compose.yml
По умолчанию порт не перебрасываются на такой же , а файлы хоста не линкуются и не будут обновляться внутри контейнера после старта.
Что-бы увидеть запущенные контейнеры, есть комманда docker ps (а лучше dry)
Что-бы запустить текущую папку есть docker-compose down && docker-compose up — это основная комманда которой вы будете пользоваться в разработке если будете делать изменения
Для автоматизации перезагрузки контейнера при изменении файлов на hostе, для node надо копать в сторону pm2-docker
Консул это сервер, который служит для связывания сервисов воедино (service discovery), поддержания их состояния здоровья (healthcheck) и для их конфигурации без перезапуска (key-value storage)
Каждый поднимающийся сервис сам должен зарегистрировать себя в консуле, указать на каком IP/порте он работает. Сам должен открыть HTTP endpoint который будет выдавать состояние здоровья
При создании микросервисов самая сложная часть это граница и управление. Сервис должен отвечать за конкретную бизнес-функцию и доменную область. Это вертикальный срез приложения. Но при этом сервис должен выполнять весь стек работы — ui, backend, db storage, queue processor. Впихивать столько ответсвенности в один сервис технологически сложно, поэтому приходится дробить вертикальный стек ещё на несколько горизонтальных слоёв. Образно был article-manager - стал article-frontend, article-server, article-worker, над которыми нависают ещё всякие сервисы мониторинга. Это тяжело связывать вместе и деплоить разом.
Поскольку нода висит постоянно в памяти, в отличие от php, который запускается при запросе, то возникает проблема перезагрузки кода при его изменении. Вот как выглядит Dockerfile с PM2:
FROM node:6-alpine USER root ENV PORT=3000 EXPOSE 3000 RUN npm install pm2 -g WORKDIR /app CMD ["pm2-docker", "start", "pm2.json", "--watch"]
Теперь в pm2.json описывается при каком случае надо перезапускать докер сервис
Недавно начал использовать триггеры в БД. Полезная штука.
Первый случай использования - валидация данных. Позволяет на уровне БД сохранять транзитивную целостность, т.е. более глубокую проверку, чем просто существующий foreign key.
Вот простейший пример. Есть три таблички - site, category, article. При добавлении нового ряда в article, мы хотим проверить что categoryID использует тот же siteID что и article. Забудем сейчас про нормализацию, допустим siteID нужен и там и там. Триггером валидация решается так..
DROPTRIGGERIFEXISTS trigger_article_insert; CREATETRIGGER trigger_advert_insert BEFORE INSERTON article FOR EACH ROW BEGIN IF NEW.siteID <> (SELECT category.siteID FROM category WHERE category.ID=NEW.categoryID) THEN SIGNAL SQLSTATE '46000' SET MESSAGE_TEXT = 'Cannot insertrow: site ID is different for article and category ENDIF;END;
Аналогично мы можем решать и вопрос обновления данных.
В обычных случаях мы могли делать сразу JOIN или подзапрос, либо уже создать VIEW, но при высоких нагрузках лучше иметь триггер..
DROPTRIGGERIFEXISTS trigger_article_insert2; CREATETRIGGER trigger_article_insert2 AFTERINSERTON advert FOR EACH ROWUPDATE category c SET size =(SELECTcount(a.id)FROM article a WHERE a.categoryID=NEW.categoryID AND a.status='published') WHERE c.id=NEW.campaignID;
Хождение по воде и разработка по спецификации легки, если и обе заморожены
Эдвард В. Берард
С самого начала моей карьеры, спецификация была больной темой. В маленькой веб-студии написание «спеки» сводилось в лучшем случае к двум-страничкам A4 с описанием нового модуля для уже работающей CMSки. Со временем я повидал и спеки как на 80 страниц от сторонних компаний в фиксированном pdf виде, так и в режиме постоянного scrumа где спека размазана по десяткам задачам в issue tracker'е.
Есть разница между спецификацией (SRS, техническим заданием), документацией и руководству по техническому обслуживанию, она в цели.
Если вы так же озабочены тестированием как и я, то вы возможно сталкивались с проблемами дедлоков при транзакциях. Транзакции в БД дело хорошее, особенно на все REST-запросы, т.к. он становится атомарной операцией. Однако если вы вместе с этим затрагиваете часто используемую таблицу или операция происходит на файлах или с другими сетевыми запросами, то вы можете вызвать дедлок.
MySQL/InnoDB дедлок возникает из-за двух транзакций пытающихся в разном порядке изменить одни и те же данные. Если вы используете триггеры, то вам в коде даже не надо явно объявлять о транзакции - любой UPDATE с триггером потенциально опасен дедлоком. При этом уровень изоляции транзакции не влияет на вероятность его получения
#dont update unless you want CentOS 6.8 upgrade #yum update -y #install basic tools yum install -y htop nano #reboot for kernel updates to take place shutdown -r now
Систему надо настроить для вагранта, если мы хотим её автоматически выкатывать под разные проекты..
Сначала - настроить пользователя
sudo visudo -f /etc/sudoers.d/vagrant #adding to this file: vagrant ALL(ALL) NOPASSWD:ALL #setup vagrant public key access mkdir -p /home/vagrant/.ssh chmod 0700 /home/vagrant/.ssh wget --no-check-certificate \ https://raw.github.com/mitchellh/vagrant/master/keys/vagrant.pub \ -O /home/vagrant/.ssh/authorized_keys chmod 0600 /home/vagrant/.ssh/authorized_keys chown -R vagrant /home/vagrant/.ssh nano /etc/ssh/sshd_config #uncomment AuthorizedKeysFile %h/.ssh/authorized_keys #enable public key authentication service sshd restart #start ssh on reboot chkconfig sshd on visudo #comment out requiretty option
Теперь для вагранта нужны Guest Additions. Это iso файл который у virtual box'а уже есть. Надо смонтировать его на диск и запустить установку пакетов
yum install -y gcc build-essential perl kernel-devel #mount VBoxGuestAdditions.iso on CD in Virtualbox and run these: sudo mount /dev/cdrom /mnt cd /mnt sudo ./VBoxLinuxAdditions.run echo export KERN_DIR=/usr/src/kernels/`uname -r`/ >> ~/.bashrc
Теперь, у вас вероятно стоит надстройка для безопасности - SELinux. Если вы будете разрабатывать локально, то лучше его отключить
setenforce Permissive nano /etc/sysconfig/selinux #set to: #SELINUX=disabled
Теперь когда всё готово, выходим из виртуалки и упаковываем её для vagrant'а. Для этого надо зайти в папку где лежит виртуалка и запустить комманду которая сгенерит package.box файлик на который в дальнейшем будет ссылаться Vagrantfile в нашем проекте:
#package box and move it to desktop vagrant package --base CentosEmpty && mv package.box ~/Desktop/ #clean vagrant cache vagrant global-status --prune #cd to your project that references new box from desktop vagrant up
Одна из проблем с которой сталкивается backend разработчик разрабатывая приложение с отложенными вычислениями это управление параллельными процессами. Например, если вы делаете загрузку картинок которые необходимо в дальнейшем обработать (уменьшить, вырезать, передвинуть на другой сервер), то для масштабирования такого решения под N -> infinity пользователей, каждый этап имеет смысл делать в отдельном процессе, число которых тоже можно масштабировать вместе с ростом N.
Я по-прежнему использую php демонов, которые рождаются кроном (если на nohup полагаться не хочется). Но поскольку мне хочется одновременно делать ресайз картинки в 5 разных размеров одновременно, я хочу управлять количеством instance'ов php процессов. Это решается простой bash-коммандой:
Заметьте что ключевая комманда -lt. Если коротко, то мы ищем процессы (ps) по их имени (grep), исключая самих себя и считаем количество найденных строк (wc), количество которых должно быть меньше (lt) количества заданных мною инстансов. Как вариант вместо -lt можно использовать -eq 0, если хочется только одного демона
При постройке качественного single-page приложения рано или поздно встаёт вопрос синхронизации данных. Это тем более актуально, когда дело касается работы разных пользователей. Приложения типа google wave, google docs, push-notifications в IOS, подняли планку ожидания пользователей и от обычных веб-систем
Увеличивает реактивность, поддерживая данные в реальном времени без обновления страницы. Типичная ситуация когда вы работаете в нескольких табах и данные не обновляются потому что закешировались
Улучшает безопасность — если пользователь выходит из одного таба, его можно выкинуть из остальных автоматически
Снижает нагрузку на сервер — можно высылать оповещение о состоянии файла, если он обрабатывается в реальном времени, подобно ютубу без какого-либо взаимодействия с пользователем или поллинга
Если вы решили что это must-have фича, прежде чем начать реализовывать её у себя, задумайтесь о последствиях:
Вам понадобится транспортный механизм (см. ниже), который обычно ограничен количеством подключённых пользователей (макс. 10 тыс), размером сообщения и объёмом траффика
Безопасность. Транспортный механизм и источник оповещения должен точечно высылать данные только нужному пользователю (без broadcast-а).
Лучше всего источник оповещения ставить на backend, сразу после закрытия транзакции. В этом случае полезно привязываться к сессии на бэкэнде
Я советую не использовать datasync-сервис в качестве источника данных. Это должен быть легковесный вестник адреса по которому можно прочитать детали. Более того, формат этого сообщения должен быть стандартизирован. Например {action:'file.added', id:1345}
Весь front-end код должен хранить данные в сыром виде в сервисах/моделях, которые будут точечно обновляться.
Соответсвенно все события будут делится на прямое вмешательство пользователя и на datasync-действия
Код при этом будет запускать похожие функции над данными
Все use-кейсы усложняются из-за ситуаций
что делать если A удалил данные который B просматривает (редиректить B, блокировать A, показывать оповещение B?)
что делать если A и B одновременно сохраняют изменения над одним и тем же обьектом (провести обе операции, кто последний тот и победил? провести первую и заблокировать последующие до окончания операции?)
что делать если A составляет новый обьект 3, ссылающийся на обьект 2, который удаляет B, а потом происходит сохранение (удалять include при удалении или при сохранении?)
Если вы используете систему привилегий, то надо дополнительно иметь оповещения о присвоении или изъятия обьекта из видимости пользователей
Если ваш сервис синхронизации вдруг упадёт, то при поднимании надо сделать так что-бы сообщения не потерялись из-за ещё неподключившихся пользователей (у socket.io 5 сек)
Не ушёл в небытие, я по-прежнему вижу как некоторые сайты настойчиво стучат в бэкэнд. Например движение общественного транспорта в Таллинне. Видимо это проще всего реализовать и проще всего использовать вместо API. Что может быть проще для сторонних разработчиков - просто читай .json файл так часто как тебе это надо. А он в свою очередь может выплёвываться из кэша-в-памяти
Pusher
Пушер — сервис который вобщем-то следующий по сложности для разработчиков. Внедряете скрипт, подписываетесь на события в js. Высылаете события, которые будут получать все пользователи.
Лучше всего это делать на уровне backend'а, иначе захламите frontend и могут возникнуть редкие баги. Например — поставил заливаться файл и закрыл браузер, а клиент X не получил итогового оповещения. Для более тонкой настройки прийдётся повозиться с интеграцией авторизации и токенами
Наконец, ещё один сервис, купленный гуглом, который больше позиционируется как nosql-база данных с синхронизацией между клиентами. Для того что-бы настроить её, как систему оповещений, но при этом оставить хранение данных у себя, прийдётся повозиться. Изначальный setup такой же простой как у pusher'а, да и авторизация по токену такая же, но поскольку это БД, то подписываться надо не на события оповещений, а на изменения json-схемы. Саму json-схему вы делаете сами, но я советую для этого случая использовать что-то типа..
{ notifications:[ 2345:[ 'file X added by 2346', 'file Y deleted by 2346' ], 2346:[ 'file X added by 2345' ] ] }
Socket.io вы должны хостить сами и для этого нужен nodejs. Socket.io реализует браузерные веб-сокеты, протокол которых менялся несколько раз. Из-за этого могут быть трудности с подключением со стороны php к нему. Я использую elephant.io, пока полёт нормальный. Самый простой начальный вариант — socket.io принимает подключение и делает broadcast всем подключённым клиентам. Для более тонкой настройки прийдётся повозиться с куки, php сессиями и желательно переносом их хранения в БД, что-бы backend мог высылать безопасно сообщение конкретному подключённому пользователю, после чего контролировать по session_id + socket кто что будет получать.
У socket.io есть альтернативные библиотечки — socky (любит руби), sockJs, вроде как очень быстрый ws, engine.io, data.io. Faye я сам не пробовал, но судя по описанию растёт ещё со времени когда терминология с кометами была на слуху. Видимо теперь тоже работает на веб-сокетах
Микросервисы в современной веб-разработке это архитектурный подход по разделению изначального монолитного приложения на независимые системные (linux) процессы. Необходимость в таком разделении возникает когда монолит становится слишком медленным для одного синхронного процесса, когда код тянет слишком много зависимостей и когда повышается риск что-то сломать в этой длинной цепочке обработки данных.
Микросервисы — не панацея и тоже усложняют всё приложение в плане транзактивности, логов, и обработки ошибок, управления конфигураций, версионирования, деплоя, возникает дублирование bootstrap-кода из-за изоляции сервисов. Поэтому стоит осторожно подходить к тому, что вы хотите выделить в микросервис и что он даст по характеристикам приложения (performance, стабильность, масштабирование, разделение нагрузки). Как правило, веб-приложения создаются сначала монолитом, а потом разделяются на сервисы — так эффективней и проще эволюционировать.
Синхронно — сервис вызывают явно как веб-сервис по HTTP/REST, делая работу синхронно по-старинке, подобно веб-серверу, тогда как клиент ждёт ответа (подобно ajax)
Асинхронно — cервис вызывают явно по HTTP/REST, регистрируется задача и клиент тут же получает job ID. Клиент обязан сам проверить состояние задачи, периодически спрашивая у сервиса (polling, aws transcoder)
Асинхронно — сервис как работник (демон) без публичного доступа. Слушает какой-то ресурс и генерирует новые вызовы. Это может быть
хранилище с локами (файлы, память, Memcache, Redis)
другие ресурсы (процессы, сеть, железо)
Асинхронно, не используя MQ, регистрирует слушателей в себе и сам знает к кому куда стучать
Главное что-бы подход был единый, с предсказуемым форматом данных (JSON, protobuf, thrift, messagepack).Для удобной конфигурации и дружбы между сервисами, надо ставить Consul, ETCD или ZooKeeper — они позволят абстрагироваться от конкретных IP адресов и PID процессов.
Например пользователь приложения имеет возможность загружать файлы.Обработка файла очень тяжёлая по CPU, генерирует несколько результатов и занимает много времени из-за долгих внутренних сетевых запросов по загрузке готовых обработанных файлов в хранилище. Допустим эта синхронная операция upload+resize+store занимает 20 секунд в монолитном приложении. В микросервисном приложении, вы решаете создать сервис обработки картинки, а для очередей сначала создаёте табличку в БД. Отлично, это даёт возможность сервис положить на другую машину и не нагружать основной сайт. Теперь возникают практические вопросы — как это всё сделать? Практически, если раньше в монолите разные слои приложения проверяли всё за вас, то теперь чуть ли не все значимые классы надо будет выделять отдельные сервисы авторизацию, логирование, менеджер картинок, менеджер транзакций и сам ресайз картинок.
Если ваш сервис должен иметь публичный веб-интерфейс, проще всего запустить php в качестве сервера под конкретную папку и она будет по запросу дёргаться. Тут пригодятся микро-фреймворки типа Silex, Slim или Lumen
#run me as: #php -S 0.0.0.0:80 -t /var/www/silex #install me with: #composer require silex/silex:~1.3 $loader = require __DIR__.'/vendor/autoload.php'; $app = new Silex\Application(); #синхронная работа $app->post('/resizeImage', function(Request $request) use($app) { #загрузить imagemagick, сделать resize, сохранить return "{success:true}"; }); #асинхронная работа $app->post('/resizeImageAsync', function(Request $request) use($app) { #зарегистрировать новую работу и передать её демону через RabbitMQ, см. ниже #клиента оповестим автоматом через websocketы return "{success:true}"; });
Хотя php изначально разрабатывался как интерпретатор html файлов, современный php может работать в постоянном режиме как сервис если его запустить в CLI режиме. В реальной жизни, php демоны как правило нестабильны из-за утечек памяти и не так эффективны по скорости как сервисы на nodejs или go. Запустить демон можно из коммандной строки..
php -f /var/www/app/daemon/myImageProcessor.php #let it live after terminal exit: nohup php -f myImageProcessor.php &
С демонами есть несколько особенностей
Работая в бесконечном цикле надо чистить ресурсы (DB-соединения, file handles)
Надо мониторить рост своей памяти что-бы не умереть от fatal error
Надо поддерживать graceful stop при получении SIGTERM и прочих сигналов что-бы не повредить данные на случай если кто-то остановит демона из консоли или перезапустит сервис
Надо ловить все исключения, делать альтернативную функциональность бэкапов и откатов, формировать красивый log
Перезапуск сервера должен запускать демоны заново — через крон или через регистрацию демона в виде системного сервиса
У вас может быть несколько экземпляров (instance) одного и того же демона в виде разных процессов. Тогда очень важно не попасть в проблемы многопоточности — дедлоки транзакций, запись в один log файл параллельно и обработка одного и того же задания. Обо всём этом чуть позже..
А пока, вот простой пример с бесконечным циклом и псевдо-кодом:
//myImageProcessor.php //set custom process name cli_set_process_title('php - daemon - myImageProcessor.php'); //enable process interruption handling declare(ticks = 1); $interrupted = false; function handleSignal(){ $interrupted = true; } pcntl_signal(SIGTERM, "handleSignal"); pcntl_signal(SIGHUP, "handleSignal"); pcntl_signal(SIGUSR1, "handleSignal"); pcntl_signal(SIGINT, "handleSignal"); while(true){ $db->connect(); $db->query("SELECT * FROM images WHERE processed = 0"); //do some work here with resize & upload $db->disconnect(); //memory cleanup & check gc_collect_cycles(); const MAX_MEMORY_EXTRA = 10 * 1024 * 1024; if (convertToBytes(ini_get('memory_limit')) < (memory_get_usage(true) + MAX_MEMORY_EXTRA)) { exit(); } //graceful shutdown if($interrupted){ exit(); } //wait for some time to decrease load on DB above sleep(10); }
Проверку на УЖЕ запущенного демона можно сделать как на уровне bash, так и на уровне самого php с использованием экзотических флагов в базе, флагов во временных файлах или же используя такой же доступ к shell. С такой проверкой, комманду можно смело класть в cron и демон будет запускаться автоматом (раз в минуту)
Для более могучего nodejs, существует отличный process manager, который может управлять и php-демонами! С его помощью можно сразу видеть только процессы демонов. Кроме того:
перезапускать демона сразу после его падения
следить за изменениями файла что-бы перезапустить демон (watch)
следить за памятью и CPU и перезапускать при их превышениях
Как только вы начали использовать демонов, вероятней всего вы станете использовать и паттерн Command, по которому микросервисы выполняют роль работников (workers). MQ-сервер выступает в роли брокера и шины, связывающей асинхронные процессы воедино. Он отвечает за надёжность передачи сообщения сервису и за то что несколько сервисов не получат одну и ту же комманду. Реализовывать это на стандартной БД было бы сложней
Для PHP-демонов, подключение rabbitMQ делается с помощью php-amqplib библиотеки и бесконечный цикл в этом случае начинает зависеть от неё..
$callback = function($msg){ //actual work here $data = json_decode($msg->body, true); //Ack message that everything is done $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']); } //technical details.. $connection = new AMQPStreamConnection('localhost', 5672, 'root', 'pass'); $channel = $connection->channel(); //new queue if it was missing $channel->queue_declare($queue, false, true, false, false); //one entry at a time $channel->basic_qos(null, 1, null); $channel->basic_consume($queue, $receiverName, false, false, false, false, $callback); //almost infinite cycle while (count($channel->callbacks)) { $channel->wait(null); } $channel->close(); $connection->close();
Понятно что один сервис хорошо, но архитектура ценна именно тем что сервисы взаимодействуют между собой. Поэтому сообщения можно генерировать другому сервису. Например если обработалось изображение, значит надо оповестить пользователя в реальном времени и обновить UI.
Многие в курсе что при параллельных запросах от браузера к серверу, браузер старается ограничить число активных запросов (по ~5 на домен). Но та же проблема возникает и на бэкэнде с ресурсами
Одна проблема с БД решается транзакциями. Но вторая, более явная остаётся - сессии. В моём случае эта проблема вылезла при загрузке файлов в SPA-приложении. Делаете 20 POST запросов с файлами и параллельно ходите по сайту. Выглядит всё так, что покуда файл не загрузится и сервер не ответит, следующий GET не сработает и UI не обновится
Происходит это потому что php блокирует потоки где есть session_start(). В моём случае это все запросы, потому что есть авторизация
Решается очень просто:
session_write_close();
Закрываем явно запись в сессию. Читать мы всё ещё сможем оттуда, но зато следующие запросы смогут быстро исполняться. Я это делаю ещё до того как файл начнётся обрабатываться
Добавляете CORS-файл на S3, вбиваете в нужные места ключи — вот у вас готов аплоадер, поддерживающий multipart, параллельную загрузку файлов, необходимую для файлов в 100MB и выше
Я не так давно писал о protractor'е — он облегчает работу с selenium, хоть и завязан с jasmine. Недавно вышла 3я версия и я, заодно обновив jasmine до 2.4.1, решил двинуться дальше со сценарными тестами, а именно - лишиться их, разбив на изолированные UI-тесты. Зачем?
Когда вы пишете сценарные тесты, то довольно скоро сталкиваетесь с проблемой заготовки данных. В интеграционных тестах я уже решал эту проблему, используя отдельную базу данных на каждый тест. Это довольно тяжело и энергоёмко. Когда дело касается UI-тестов, это ещё сложней.
Иногда, когда пишешь терминальное приложение (миграции например), хочется кроме стандартного потока, создавать более красочные сообщения и прогресс-бары. Для этого надо использовать управляющие коды для шелла. Перепечатаю для себя шпаргалку на будущее..
echo "\033[s"; //Сохранить положение курсора echo "\033[u"; //Восстановить положение курсора echo "\033[<линия>;<колонка>H"; //Поставить курсор на линию и колонку echo "\033[<линий>A"; //Сдвинуть курсор вверх echo "\033[<линий>B"; //Сдвинуть курсор вниз echo "\033[<колонок>C"; //Сдвинуть курсор вправо echo "\033[<колонок>D"; //Сдвинуть курсор влево echo "\033[2J"; //Очистить экран и перейти на начало echo "\033[K"; //Очистить строку до конца echo "\e"; //Аналог \033 echo "\r"; //Вернуться на начало строки
/** * @deprecated Устаревшая функция * @see nameSpace.ClassName#methodName Note, there are no { } brackets. * @see namespace.ClassName Note, there are no { } brackets. **//** This is a comment of how {@link nameSpace.ClassName} is similar to this object. **/
/** * @param {paramType} paramName Basic parameter description. * @param {String|Number} paramName Can be one of these types. * @param {String} [paramName] Parameter is optional. * @param {String[]} paramName Parameter is an array of strings. * @param {function(string)} paramName Parameter is a function callback. * @param {String} [paramName="mathew"] Parameter has a default value of "mathew" via code. (must be optional). * @param objectName This parameter is an object with values. * @param objectName.property1 This is a property of above object. **/
/** * @class {namespace.ClassName} Defines a reference to a class. * @augments {namespace.ClassName} Define a derived base class. * @extends alias for @augments * @lends {namespace.ClassName} Declares a block to define part of a class. * @constructor Defines a function as the creator for a class. * @constructs Must be used with @lends to define the initialization function. * @private Highlights invalid references to a property in PHPStorm * @protected Highlights invalid references to a property in PHPStorm * @override Defines a function as overriding a function of the same name in a lower class. * @borrows namespace.ClassName#methodName Tells PHPStorm to use the documentation from another class/method for this method. * @memberOf namespace.ClassName Declares something outside of a class block as part of that class. **/
/** * @this {type} Defines what object "this" referrers to. * function(/**String**/arg1)Defines the parameters of an inline function. * @return{type} The return type. * @returns alias of above. **/
Backbone Model and Inheritance A typical example using a Backbone model to demonstrate how to declare inheritance.
/** * This is a model class what be will inherited. * * @class {gems.BaseModel} * @extends {Backbone.Model} **/ gems.BaseModel = Backbone.Model.extend( /** * @lends {gems.BaseModel} */ { /** * This is a property of the class. * * @type {string} **/ message: 'Hello world', /** * A constructor for this class. * * @param {Object} attributes * @param {Object} options * * @constructs */ initialize: function(attributes, options) { }, /** * This is a virtual method on the model. * * @param {string} arg An argument * @public * @returns {boolean} */ foo: function(arg) { return false; } });
Небольшой know-how.. javascript асинхронный как вы понимаете, а это налагает ряд ограничений и проблем. Например когда у вас несколько асинхронных запросов (т.е. вы незнаете когда они завершатся), а от всех их зависит последующая логика приложения, то обычное не-асинхронное приложение (например на php) решает это в лоб - последовательно выполняет операции и получается такая картинка..
Именно многопоточность даёт возможности ускорять backend на nodejs, масштабироваться на многоядерных процессорах, многомашинных обработках с map-reduce и тп. Для простого решения этой проблемы на frontend можно конечно написать костыль самому, но в jQuery уже есть Deferred объекты, с простым синтаксисом..
Набрёл на интересную бесплатную книжку Тамары Кулинкович по психологии (см. пост на хабре). Сама она занимается HR-процессами, визуализацией данных и тп. Суть в потребностях людей. Будете их удовлетворять - у вас будет хороший продукт
Я в последнее время всё больше люблю писать интеграционные (API) тесты — запускаю половину приложения, но не привязан к UI. Это золотая середина между очень медленными end-to-end тестами и очень быстрыми unit-тестами. Рассмотрим особый случай таких тестов, которые используют заготовленные данные под каждый тест.
Такие тесты приходится создавать, когда проект становится таких масштабов, что тесты с одной БД начинают конфликтовать между собой и становятся нестабильными. То список где-то постоянно растёт, то ID у ресурса требуется фиксированный а у нас autoincrement, то данные хочется удалить.
Это особенно ярко видно в e2e тестах, где приходится управлять всем жизненным циклом данных что-бы тесты оставались рабочими. Управление всем циклом из создания-операции-удаления, вынуждает тесты делать зависимыми друг от друга, а значит становится невозможно запустить тест сам по себе.
В продолжение поста про Радикальная честность, по личному опыту общения с одним любимым человечком, захотелось выписать присущие ему характеристики, а потом нашёл наглядный пример, социопата которого вы могли бы заметить в фильме Gone girl.
Социопат – это человек, лишенный совести. Возникает либо наследственно, либо из-за повреждения лобной доли мозга либо из-за психологической травмы детства (родители постарались). Это душевная инвалидность, не лечится, человека можно лишь адаптировать. Во многом схожи с нарциссами
Если вы заботитесь о качестве своего проекта и кода, то пишете unit-тесты. Но с ними всегда есть «особые случаи». Один из них - работа с файловой системой и ресурсами. Решение «в лоб» - параллельно создавать папку/дерево специально для тестов и надеяться что они не прыгнут на настоящие пути и ничего не удалят.
Более правильный подход - использование in-memory виртуальной файловой системы, vfs. А поскольку ресурсы это по сути потоки, то и название для этой мок-библиотеки - vfsStream. Ставим..
Я когда-то писал про то что в phpunit небыло возможности нормально протестировать внутренние методы класса и приходится обращаться к runkit. Незнаю, была ли это моя недоработка, либо с версии 3.8 прошло уже много времени, но в 4.5 эта возможность есть! Моки в 4.5 стали удобней — они умеют перезаписывать как весь класс, так и его части.
У меня подход к разработке эволюционный и к тестам прагматичный — тесты должны позволять постепенно улучшать качество приложения. В реальной жизни приложение уже существует в какой-то форме, а программисты ленивые и тесты не писали. Поэтому надо уметь минимальным рефакторингом добавлять автоматические тесты, постепенно приводя людей и покрытие к вере в Бога TDD. По-оправдывался, теперь к конкретике..
Jira от Atlassian — самый современный трекер задач и багов позволяющий гибко настроить workflow организации. Но если вы не доросли, не хотите использовать Bamboo, а скажем используете PHPCI для автоматического тестирования, то вам возможно будет полезно видеть результаты прогона тестов сгруппированных по фичам.
Это достаточно спорная тем, многие апологеты тестирования не понимают зачем надо связывать тесты с фичами. Такие разработчики либо придерживаются простого мировоззрения что «все тесты должны проходить всегда», либо считают это излишним усложнением не дающего ничего в итоге.
По-моему эта методика достаточно полезна для повышения прозрачности покрытия фич. Если разработчик пишет новую фичу, то это не значит что он сразу же покроет код тестами. И даже если код на 100% покрыт юнит-тестами, это не значит что ошибок нет. Надо писать интеграционные тесты. А интеграционные тесты как правило не генерируют покрытия. Поэтому видеть - написаны ли какие-то тесты для фичи полезны для уверенности. Это справедливо и если вы программист и не следуете методике TDD "test first" и если вы менеджер и для вас покрытие тестами недостаточно прозрачно, не гранулярно.
Кайдзен — по-японски означает «изменение к лучшему», это нечто типа трезвения и осознанности, полезная практика постоянного самосовершенствования (constant improvement) ради всеобщего блага. Популяризируется она благодаря тому что это фундамент, используемый на фабриках корпорации Toyota для эффективного производства.
Первый шаг это устранение потерь. Для этого надо беречь исходные материалы, время, место и работников — словно вы работаете в (после)военное время. Применимо к IT, практика так и называется — бережливая разработка, Lean (англ. скудный). Философски, это избавление от вредных зависимостей.
Современные приложения всё больше начинают напоминать полноценное desktop решение, где в итоге запускается build-процесс с помощью grunt и мы получаем один кешируемый js файл. Теперь встаёт вопрос, как бы его загрузить так что-бы показать % подгрузки?
Есть два отдельных скрипта — $script, который инжектит новые script-элементы с обратной связью и progress.js, который симпатично показывает степень загрузки. Проблема в том что оба они уже сами по себе тяжёлые, в итоговый загружаемый билд-файл их нельзя вставить, а внедрять в index.html слишком бы его раздуло.
К тому же $script не поддерживает степень загрузки, а progress.js не использует % от ширины страницы. Поэтому я написал более простенький вариант.
$script = function (path, callback, sizeInChars) { var head = document.getElementsByTagName('head')[0]; var body = document.getElementsByTagName('body')[0]; var div = document.createElement('div'); div.setAttribute('style', 'background-color:#3498db; width:0px;height:3px;transition: width 0.5s linear;'); body.insertBefore(div, body.firstChild); var src = path + (path.indexOf('?') === -1 ? '?' : '&'); var oReq = new XMLHttpRequest(); oReq.addEventListener("progress", function (e) { div.style.width = (100 * ( e.lengthComputable ? (e.loaded / e.total) : (e.loaded / sizeInChars))) + '%'; }, false); oReq.onreadystatechange = function (e) { if (oReq.readyState != 4) return; var el = document.createElement('script'); el.text = oReq.responseText; head.insertBefore(el, head.lastChild); callback(); window.setTimeout(function () { div.remove(); }, 500); }; oReq.open('GET', src, true); oReq.send(null); }
Итого — js файлы подгружаются как текст через ajax, внедряются (без eval), вызывается callback, при этом показывается div отражающий степень загрузки. Единственная проблема — вы должны знать размер загружаемого файла, что в случае nginx + gzip не приходит с заголовками. Поэтому приходится использовать например так..
$script('/js/app.js', function () { angular.bootstrap(document, ['myApp']); }, 4879435);
Порой надо написать какой-то установочный скрипт, который требует одновременный запуск нескольких задач, из которых некоторые достаточно долгие — сервер, билд проекта. Это не то же самое что последовательный запуск с «&&» или «;» разделителями. Shell-скрипты могут и это с помощью комманды trap.
Комманда работает на прерывании процесса (второй параметр). В данном случае это EXIT и TERM. Основная комманда - убиение запущенных этим терминалом в фоне процессов (jobs -p). Сами они перечисляются дальше с группировкой фигурными скобками.
Первый trap таким образом вызывает последующие вызовы. Заметьте что второй вызов с инициализацией базы mongo стоит с задержкой в 3 секунды, что-бы сервер успел подняться (который уже будет бежать бесконечно)
На практике получается впрочем так, что комманды бегут бесконечно, т.е. mongod запускается, вы нажимаете Ctrl+C, а он всё ещё работает.. jobs ничего не показывает, но если возникает соединение - контекст возвращается. Приходится использовать killall mongod
Бесконечный скролл, т.е. постепенный показ и подгрузка данных как реакция на действия пользователя, закрепился в SPA-приложениях. Я работаю с angular, но в целом это может и к backbone относиться..
Технически, данные на frontend хранятся в какой-то коллекции/сервисе. То что показывается пользователю — подмножество этих данных, т.е. может быть отфильтрованным, отсортированным и изменяемым на лету. Таких подмножеств в приложении может быть несколько, в зависимости от отображения (view).
В одном месте у вас товары одного бренда, в другом месте у вас заказанные товары, в третьем - товары конкретного пользователя.
Это значит, что вопрос подгрузки новых данных - нетривиальная задача.
Сначала я подумал что лучше всего запрашивать данные по времени. У каждого ряда данных будет timestamp, как у твиттера, соответсвенно запрос будет высылаться "подгрузи 20 рядов данных, старше X", а время будет браться у последнего ряда.
Но тут возникает несколько проблем.
Что если у данных одно и то же время? Если на backend добавить вторую сортировку по ID, и передавать ID последнего ряда вместе со временем, то это всё-равно не спасает положения, т.к. значительно усложняется вычисление OFFSET в SQL запросе.
Если запрос зависит от последнего показанного ряда, то получается что запрос формируется на основе подмножества, т.е. зависит от view. Это плохо, т.к. завязывает модель, контроллер и view в единое целое. Альтернатива - дублировать фильтрацию которая происходит для view в модели.
Что если данные обновились со времени последнего запроса? Надо ли их добавлять в начало списка?
Что делать если данные отсортированы не по времени, а по алфавиту? И они вдруг поменялись?
Вобщем приходишь к тому, что для универсального решения, надо по-прежнему работать с OFFSET и ORDER BY, как то было с постраничной навигацией. При этом offset высылается в зависимости от нужды view и он равен размеру подмножества.На случай обновившихся данных, я использую socket.io, который оповещает что надо обновить. Добавившиеся данные я сразу внедряю в список, а update происходит автоматически
ngInfiniteScroll - простенькая ангулярная директива для ангуляра, которая триггерит нашу функцию подгрузки если вы достаточно проскроллили вниз (scrollTop() близок к высоте элемента). Проблемы в ней две - она привязана ко всему окну (window). Обычно же у нас может быть несколько контейнеров на странице где надо подгружать данные. Есть и более простой пример
И вторая проблема следующая из неё - если вы запросили слишком мало данных за запрос (LIMIT 5), и/или их высота динамическая и меньше чем высота контейнера, то ваш scroll никогда не появится и не запустит постоянное дополнение списка. Пришлось это поисправлять, каждый раз расчитывая высоту содержания контейнера.
Тут главное не заDDOSить сервер из-за неправильного расчёта высоты элементов и рекурсии. По-хорошему вызов подгрузки надо начинать до достижения нижнего края.
Есть небольшая проблема, если у вас есть фильтрация коллекции с поиском. Я решаю эту проблему отдельным мини-кэшем и отдельными запросами. Если вы ищете что-то по ключевому слову, то результаты не добавляются в основную коллекцию списка, поскольку это повлияет на offset при нормальном режиме. Search и отфильтрованные данные у меня подгружаются без постраничной навигации, т.е. их количество ограничено
Бесконечный скролл напрямую зависит от скролла. Замечали сайты на которых открываешь в телефоне страничку с контактами, а там google map на всю высоту экрана? И не добраться до текста с телефонами ниже.. Бесконечный скролл, если он достаточно высокий, аналогично блокирует данные ниже себя.
Во-вторых, бесконечность дезориентирует и лишает контроля. Вы не знаете сколько вы уже проскроллили, дажа примерно глядя на размер бегунка, не можете понять сколько осталось до конца. Аналогично, вы не можете перепрыгнуть в "значительно более далёкие" версии данных.
В-третьих, вы не можете скинуть URL-ссылку на это место другому человеку и соответственно, если вы уйдёте с бесконечной ленты по ссылке и потом попробуете вернуться назад, вы попадёте на начало ленты. Т.е. такой скролл требует изменения поведения при навигации и пользователь должен открывать ссылки в новом окне.
Само собой, если у вас SPA, то могут быть проблемы с индексацией поисковиками. Гугл предлагает гибридный вариант, где данные таки будут доступны и по страницам тоже.
Наконец, если у вас что-то случилось с ajax-запросом и нет хорошей обработки ошибок, бесконечный скролл ломает глубину просмотра. А если сервер достаточно нестабильный, то риск что пользователь никогда не сможет добраться до нужных данных увеличивается с каждым запросом. Например если вероятность ответа сервера HTTP 500 оценивается в 5%, то после 15ти последовательных запросов вероятность что лента будет сломана уже (100 - 100 × 0.9515) = 55%.
Тем не менее, если вы делаете крутой динамический сайт, который может обновляется блоками с нестатичными размерами и с нестандартной навигацией вверх/вниз и вправо/влево, постепенная подгрузка musthave.
Interstellar, новый фильм Кристофера Нолана эпичен, масштабен, грандиозен и со своими чёрными дырами. В сущности фильм затрагивает три темы — любовь и эгоизм, судьбу человечества, время и смерть. Хорошо балансирует между космической одиссеей и опытом-чувствами, которые становятся большей ценностью для нашего общества. По качеству:
Хороший набор актёров
Отличный визуальный ряд
Классная, атмосферная музыка Ханса Циммера
Закрученный сюжет, хоть и со своими неровностями
Продуманная научная тема
Обязательно посмотрите, а теперь честность на 100% и сплошь спойлеры..
Часто сталкиваюсь с желанием поменять что-то на сайтах, добавив свой функционал, сделав их удобней или автоматизировав многие вещи. Во многих случаях это становится хорошим лайфхаком. К счастью для браузеров есть расширения, добавляющие возможность запуска user-скриптов. Для firefox это greasemonkey, а для chrome - tampermonkey.
Движки "обезьянок" позволяют подключить внешние библиотеки (например jquery) и напрямую манипулировать страницей. Это даёт широкий простор по изменению layoutа и взаимной интеграции сайтов. Например
Посмотрел вчера Lucy. Средне. Хорошие спец-эффекты, вставки как из Baraka под псевдо-философию, что-бы дать видимость глубины мысли, там где её нет. Дальше спойлеры.
Фильм об обожении, но как и большинство последних голливудских фильмов затрагивающих внутреннее развитие и трансцендентность, он слишком материалистичен. Главная героиня с одной стороны становится сверх-умна, теряет чисто человеческие качества ограниченности сознания (страх, боль, страдание), но при этом не отказывается от насилия и поглощения всего наркотика. Полное раскрытие разума даёт способности по управлению и пониманию всего вокруге, но не ведёт ни к чему продуктивному.
Если у вас есть веб-приложение и вы задались тем что-бы идеально его покрыть тестами, то вот что у вас должно быть:
unit-тесты бэкэнда — в основном покрываются модели, генерируется покрытие — получаете необходимость изолировать модели (заодно single responsibility principle выполняется)
unit-тесты frontend — карма + phantomjs прокрутят все ваши angular-сервисы и backbone-модели — тоже приходится изолировать код
e2e (сценарные, системные) тесты — наверняка основанный на selenium (protractor, selenide). Медленно тестируется функционал работающей системы из UI — приходится задумываться о том что пользователь вообще делает (use cases)
db/entity тесты миграций — "с нуля" запускают изменения в БД и когда всё готово - сравнивают с entity/record классами для синхронизации кода с БД (так находятся лишние свойства и недостающие )