Извлекая полезный опыт IT-компаний
Я работал уже больше 12 лет в разных IT-компаниях и захотел в порыве ностальгии выписать разницу между ними, лучшие стороны, что-бы вы тоже могли научиться у них. Поехали..
Я работал уже больше 12 лет в разных IT-компаниях и захотел в порыве ностальгии выписать разницу между ними, лучшие стороны, что-бы вы тоже могли научиться у них. Поехали..
Команды программистов из 3-7 человек это идеальная машина по быстрому созданию качественного продукта. Слишком много - и все погрязнут в бесконечных обсуждениях, слишком мало - будет сбиваться ритм и снижаться продуктивность и качество.
Я мало что понимаю в менеджменте, поэтому меня больше интересуют вопросы конфликтов и улучшение инспекции кода для улучшения продукта и сплочения команды.
Год назад мы взяли в кредит дом. Старенький, с деревянными продуваемыми окнами, без утепления и с основным дровяным отоплением. Коллеги брали наоборот новые, рядные дома где всё уже готово. Я расчитывал что так я выиграю в цене, гибкости, а в итоге и в качестве.
Заказав 16 м³ дров за ~700€ и протопив сезон, я понял что это вам не квартирные удобства. Хотя дрова самые дешёвые и экологические в отоплении, с ними неудобно
Надо дровник и тратить пару дней на их переноску и выкладывание
Дрова бывают разные и по-разному разгораются (берёза лучше осины)
Зимой надо минимум 2-3 вязанки дров за вечер что-бы натопить. Таскать их в мороз и складировать у плиты — то ещё удовольствие.
Топить через маленькую плиту, подбрасывая дрова каждые пол-часа/час. Это слишком отвлекает от домашнего быта.
Температура неравномерна и не постоянна
Когда топишь то возле плиты воздух может быть 30˚C уже, тогда как где-то в дальнем углу ещё 18˚C.
Приходишь домой — дома 15˚C, надо ждать 2 часа покуда прогреется часть этажа. Встаёшь утром - тоже холодно, душ принимать становится просто опасным для здоровья — легко простудиться.
Кроме того, есть теплонасос воздух-воздух, но его мощности хватало только на четверть дома, он достаточно шумный и при морозах его мощность сильно падала. Поэтому приходилось дополнительно топить электрическими конвекторами. Но электричество достаточно дорогое — примерно +50€/мес при отоплении только спальни. Отапливать весь дом электрическими обогревателями влетело бы в ~300 €/мес.

Автономность (1-2 недель беспрерывной работы, что-бы можно было съездить в отпуск зимой)
Равномерность отопления дома
Достаточная мощность при морозах в -30˚C (объём дома ~ 350м³ без внешнего утепления)
Уже исходя из требований, выходит что минимальная мощность источника отопления при зимних тепло-потерях — 20 квт
Я рассматривал разные источники тепла:
Для равномерного прогревания дома, система тёплых полов самая идеальная. Но для этого трубки должны быть встроены в бетон, а у меня увы, перекрытия деревянные.
Я получил 3 предложения от 5 фирм, в итоге одно из них выбрал и чуть-чуть поменял.
Выбрал я в итоге пеллетный котёл от датской NBE, RTB30 (30квт). Степень сгорания более 91%, что значит что для смены бака надо сжечь порядка тонны пеллетов (т.е. менять золу надо будет раз в пару месяцев)
Вместе с котлом поставляется компрессор, который подключён к горелке. После прогорания, горелка автоматически чистится воздухом под давлением

На самом котле есть dashboard где показано подключение и состояние в целом. Внутри - меню offline управление.
Котёл подключается по wi-fi к интернету к порталу stokkercloud.dk, где можно посмотреть как публичную информацию разных котлов в мире, так и залогиниться в админку своего котла (об этом ниже). Админка кстати написана на ангуляре, а бэкэнд на php




Теперь что можно делать в интернете..




Отапливается всё 10 радиаторами соединённых в два контура (верхний и нижний этажи). Отдельный насос на этаж в случае неполадки позволит отключить только часть дома, даст более точное управление теплораздачей за счёт регулируемого давления (скорости движения воды). Не уверен, но думаю что разница в высоте как-то влияет на давление в системе.
В дополнение к этому, старый маленький электрический бойлер для горячей воды я решил тоже сменить на 150л и подсоединить к котлу. Зимой это позволит сэкономить на электричестве.
Сеть из труб достаточно интересная и полна нюансов.
Что-бы понять нюансы советую послушать специалиста...

Из проблем или неудобств, стоит выделить высокую минимальную мощность. Минимально настраиваемая мощность в 10% потребляет достаточно много топлива и быстро нагревает дом до 26˚C для экономии осенью проще поставить вариацию по времени (включение через каждые 2 часа), но частое включение горелки плохо сказывается на её надёжности. В этом плане идеально было бы подключить дополнительно систему воздух-вода для температур 0 — +10˚C. Ближе к зиме я настрою постоянное включение
Устанавливали всё товарищи из Küttemeister.
Первый факап - котёл должен иметь металлический дымоход, что-бы кирпичная кладка не разрушалась из-за конденсата который образуется из-за низких температур дыма. Этот конденсат стекает по трубе вниз в ведро (можно увидеть на фото). Чисто эстетически это неприятно, но факап установки в том, что установив трубу мне перекрыли дымоход из бани. Отчасти это была моя вина, прийдётся делать электрическую баню.
Второй факап более тяжёлый. Несколько месяцев я бился с регулировкой котла - мощность его обычно не превышала 30%, но отапливать весь дом он не мог почему-то. Он достаточно быстро выключался, показывая что достиг своей температуры, а обогреть два этажа так и не мог. В лучшем случае он мог хорошо обогревать только один этаж.
Я было уже начал думать что сам котёл слишком мощный вышел, а трубы и батареи хилые, но..
После того как я стал поднимать ожидаемую температуру на выходе, ситуация ещё больше усугубилась - до какой-то границы (70 градусов) температура росла, а потом резко перекрывалась , вводя котёл в экстремальные температуры в 90 градусов, провоцирующие аварийное отключение.
После экспериментальных опытов было установлено, что рабочие перепутали открытие и закрытие насосов - в настройке или сами провода. Когда насосы должны были увеличить процент открытия они закрывались и наоборот.
Из-за этого в лучшем, полуоткрытом случае вода подавалась только на 50%, показывая при этом правильные температуры. Насосы при этом надрывались на максимальной мощности, но потока было мало и нагретая вода направлялась обратно в котёл, перегревая его.
Пеллетные гранулы бывают 6мм и 8мм в длину. Разница лишь в том, что это влияет на объём который забирает hopper из бака (для распаливания 40г например надо).
Гранулы бывают разного качества. Хреновые делаются из коры деревьев и поэтому могут содержать песок, который может навредить трубопроводу или камере сгорания (закоптится). Похожие последствия бывают скажем если топить дровами из хвои.
В Эстонии заказать можно машину (2-4 тонны), а можно купить пакетами по 15-25 кг. Расчитывая расход осенью в 10кг в день (как вы видели на скриншотах выше), получается 300кг в месяц (40 евро/мес). В зимний период я думаю расход будет раза в два больше (под 100 евро/мес). Оптовые производители:
Тут всё зависит от того есть ли у вас подходящее помещение для хранения топлива. Я пока предпочитаю покупать пакеты специально для бака, зимой надо будет брать чуть чуть с запасом

После нескольких месяцев отопления, пришёл к таким заключениям..
В идеале умный дом должен в себя включать
датчики аварии (протечки воды, пожара, электроотключения)
датчики безопасности (открытия дверей, окон, движения на участке)
видео-трансляция и запись
управление освещением (умный свет внутри дома и освещение участка, гардины/жалюзи)
управление поливом газона (в зависимости от прогноза погоды)
электрические замки/ключи
Умная техника
плита которая оповещает вас если вы забыли её выключить
холодильник, в который можно посмотреть удалённо
стиралка, которую можно удалённо приостановить
Кроме того, я выступаю за то что умный дом должен быть автономным и экономным
В прошлой статье я начал изучать микросервисный подход, но до полноценного решения тогда не дошёл. Тогда я пробовал сам поднимать процессы на php, управлять ими, связывать между собой через очереди потому что монолит не мог справиться с асинхронными задачами. Но чего у меня реально нехватало, так это нормальной изоляции и деплоймента, межсервисного взаимодействия и стабильной работы.
Я сразу скажу что не знаю как настраивать production с докер-контейнерами, но сервисы в рабочем окружении поднимаются тривиально
Ставите Docker для Мака или для Windows
Ставите dry для терминала (по желанию)
Создаёте Dockerfile
Создаёте docker-compose.yml
Запускаете docker-compose up
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 описывается при каком случае надо перезапускать докер сервис
{
"apps": [
{
"name": "article-manager",
"script": "index.js",
"env": {
"PORT": 3000,
"CONSUL_IP": "192.168.10.10",
"CONSUL_PORT": 8500,
"SERVICE_NAME": "article-manager"
},
"watch": true,
"ignore_watch": [
"node_modules",
"npm-debug.log",
".idea",
".git",
"test/coverage"
],
"watch_options": {
"followSymlinks": false
}
}
]
}
Очень быстрая in-memory база для кеширования данных
version: '2'
services:
redis-master:
image: 'redis:2.8'
network_mode: bridge
volumes:
- ./data:/data
ports:
- 6379:6379
command: redis-server --appendonly yes
Для удобства, советую добавить в ваш ~/.bashrc или ~/.zshrc алиас на быстрое поднятие контейнера:
alias doc='docker-compose down && docker-compose up'
Недавно начал использовать триггеры в БД. Полезная штука.
Первый случай использования - валидация данных. Позволяет на уровне БД сохранять транзитивную целостность, т.е. более глубокую проверку, чем просто существующий foreign key.
Вот простейший пример. Есть три таблички - site, category, article. При добавлении нового ряда в article, мы хотим проверить что categoryID использует тот же siteID что и article. Забудем сейчас про нормализацию, допустим siteID нужен и там и там. Триггером валидация решается так..
DROP TRIGGER IF EXISTS trigger_article_insert;
CREATE TRIGGER trigger_advert_insert
BEFORE INSERT ON 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 insert row: site ID is different for article and category END IF; END;
Аналогично мы можем решать и вопрос обновления данных.
В обычных случаях мы могли делать сразу JOIN или подзапрос, либо уже создать VIEW, но при высоких нагрузках лучше иметь триггер..
DROP TRIGGER IF EXISTS trigger_article_insert2;
CREATE TRIGGER trigger_article_insert2
AFTER INSERT ON advert
FOR EACH ROW UPDATE category c SET
size = (SELECT count(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 с триггером потенциально опасен дедлоком. При этом уровень изоляции транзакции не влияет на вероятность его получения
В качестве основы, будем использовать CentOS 6.5
Обновляем систему:
#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
См также:
http://williamwalker.me/blog/creating-a-custom-vagrant-box.html
https://www.vagrantup.com/docs/boxes/base.html
http://aruizca.com/steps-to-create-a-vagrant-base-box-with-ubuntu-14-04-desktop-gui-and-virtualbox/
https://stefanwrobel.com/how-to-make-vagrant-performance-not-suck
Одна из проблем с которой сталкивается backend разработчик разрабатывая приложение с отложенными вычислениями это управление параллельными процессами. Например, если вы делаете загрузку картинок которые необходимо в дальнейшем обработать (уменьшить, вырезать, передвинуть на другой сервер), то для масштабирования такого решения под N -> infinity пользователей, каждый этап имеет смысл делать в отдельном процессе, число которых тоже можно масштабировать вместе с ростом N.
Я по-прежнему использую php демонов, которые рождаются кроном (если на nohup полагаться не хочется). Но поскольку мне хочется одновременно делать ресайз картинки в 5 разных размеров одновременно, я хочу управлять количеством instance'ов php процессов. Это решается простой bash-коммандой:
[ "$(ps -ef| grep -v grep | grep imageResizer|wc -l)" -lt 5 ] && php imageResizer.php > /var/logs/imageResizer.log
Заметьте что ключевая комманда -lt. Если коротко, то мы ищем процессы (ps) по их имени (grep), исключая самих себя и считаем количество найденных строк (wc), количество которых должно быть меньше (lt) количества заданных мною инстансов. Как вариант вместо -lt можно использовать -eq 0, если хочется только одного демона
При постройке качественного single-page приложения рано или поздно встаёт вопрос синхронизации данных. Это тем более актуально, когда дело касается работы разных пользователей. Приложения типа google wave, google docs, push-notifications в IOS, подняли планку ожидания пользователей и от обычных веб-систем
Увеличивает реактивность, поддерживая данные в реальном времени без обновления страницы. Типичная ситуация когда вы работаете в нескольких табах и данные не обновляются потому что закешировались
Улучшает безопасность — если пользователь выходит из одного таба, его можно выкинуть из остальных автоматически
Снижает нагрузку на сервер — можно высылать оповещение о состоянии файла, если он обрабатывается в реальном времени, подобно ютубу без какого-либо взаимодействия с пользователем или поллинга
Если вы решили что это must-have фича, прежде чем начать реализовывать её у себя, задумайтесь о последствиях:
{action:'file.added', id:1345}Не ушёл в небытие, я по-прежнему вижу как некоторые сайты настойчиво стучат в бэкэнд. Например движение общественного транспорта в Таллинне. Видимо это проще всего реализовать и проще всего использовать вместо 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)
Асинхронно — сервис как работник (демон) без публичного доступа. Слушает какой-то ресурс и генерирует новые вызовы. Это может быть
Главное что-бы подход был единый, с предсказуемым форматом данных (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 &
С демонами есть несколько особенностей
У вас может быть несколько экземпляров (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 и демон будет запускаться автоматом (раз в минуту)
#check if daemon is not running yet
#add logs
[ "$(ps -ef| grep -v grep | grep myImageProcessor|wc -l)" -eq 0 ] && php -f myImageProcessor.php > /var/www/logs/myImageProcessor.log
#up to 5 instances!
[ "$(ps -ef| grep -v grep | grep imageResizer|wc -l)" -lt 5 ] && php myImageProcessor.php > /var/logs/myImageProcessor.log
Остановить демона можно так же из шелла..
sudo /usr/bin/kill -9 `ps aux | grep 'myImageProcessor' | grep -v grep | awk '{print $2}'`
Для более могучего nodejs, существует отличный process manager, который может управлять и php-демонами! С его помощью можно сразу видеть только процессы демонов. Кроме того:
Менеджеры попроще — forever и guvnor
Как только вы начали использовать демонов, вероятней всего вы станете использовать и паттерн 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.
$queue = 'datasync';
$message = ['imageID'=>34, 'status'=>'processed'];
$message = json_encode($message);
$connection = new AMQPStreamConnection($server, $port, $login, $pass);
$channel = $connection->channel();
$channel->queue_declare($queue, false, true, false, false);
$channel->basic_publish(new AMQPMessage($message, ['delivery_mode' => 2]), '', $routeKey);
$channel->close();
$connection->close();
Более серьёзные сервисы пишутся на nodejs, а лучше на Go.
Грубо говоря, если прогнать нагрузочный тест, то с PHP вы получите 300 req/s, тогда как с node 7000 req/s, а с go 9000 req/s.
С Go можно копать в сторону go-micro
См. также
Многие в курсе что при параллельных запросах от браузера к серверу, браузер старается ограничить число активных запросов (по ~5 на домен). Но та же проблема возникает и на бэкэнде с ресурсами
Одна проблема с БД решается транзакциями. Но вторая, более явная остаётся - сессии. В моём случае эта проблема вылезла при загрузке файлов в SPA-приложении. Делаете 20 POST запросов с файлами и параллельно ходите по сайту. Выглядит всё так, что покуда файл не загрузится и сервер не ответит, следующий GET не сработает и UI не обновится
Происходит это потому что php блокирует потоки где есть session_start(). В моём случае это все запросы, потому что есть авторизация
Решается очень просто:
session_write_close();
Закрываем явно запись в сессию. Читать мы всё ещё сможем оттуда, но зато следующие запросы смогут быстро исполняться. Я это делаю ещё до того как файл начнётся обрабатываться
вторник, 1 марта 2016 г. в 11:07:40
Amazon S3 поддерживает прямой upload. Делается всё просто со сторонними библиотеками, особенно если у вас ангуляр
bower install evaporate angular-evaporate --save
Добавляете в настройках путь к серверной подписке, где с php очень просто делается подпись:
echo base64_encode(hash_hmac('sha1', $_GET['to_sign'], AWS_SECRET, true));
Добавляете 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"; //Вернуться на начало строки
Цвет терминала меняется как у шрифта так и у фона
echo "\033[0m"; //Безцветный текст
echo "\033[41m"; //Красный цвет фона
echo "\033[44m"; //Синий цвет фона
//Добавим цвет шрифта
echo "\033[0;31m"; //Красный цвет шрифта
echo "\033[0;37m"; //Белый цвет шрифта
echo "\033[0;33m"; //Желтый цвет шрифта
//Добавим вначало стиль шрифта
echo "\033[1;41;33m"; //Жирный (жёлтый на красном фоне)
echo "\033[4;41;33m"; //Подчёркнутый
echo "\033[1;4;41;33m"; //Жирный и подчёркнутый
Таким образом прогресс-бар достаточно просто можно реализовать сохранением состояния экрана и точечным добавлением символа по мере загрузки
Скопирую статью про аннотации для javascript - JSDoc. Порой бывает очень полезно для автокомплита и прыгания в нужные места в IDE..
/**
* @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.
**/
/** Define parameter types in function arguments (useful for inline functions) **/
function(/**String**/arg1) { }
/** @type {string} msg **/ Handy for unknown return types.
var msg = foo();
/**
* @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;
}
});
Теперь модель унаследовавшая BaseModel
/**
* A document model for Gems.
*
* @class {gems.DocumentModel}
* @extends {gems.BaseModel}
**/
gems.DocumentModel = gems.BaseModel.extend(
/**
* @lends {gems.DocumentModel}
**/
{
/**
* @param {string} arg An argument
* @see gems.BaseModel#foo
* @override
*
* @returns {boolean}
**/
foo: function(arg)
{
if(/**something**/)
{
return true;
}
return gems.BaseModel.prototype.foo.apply(this,[arg]);
}
});
Небольшой know-how.. javascript асинхронный как вы понимаете, а это налагает ряд ограничений и проблем. Например когда у вас несколько асинхронных запросов (т.е. вы незнаете когда они завершатся), а от всех их зависит последующая логика приложения, то обычное не-асинхронное приложение (например на php) решает это в лоб - последовательно выполняет операции и получается такая картинка..
//Backbone пример
Model1.fetch({success:function(){
Model2.fetch({success:function(){
Model3.fetch({success:function(){
Model4.fetch({success:function(){
Model5.fetch();
}});
}});
}});
}});
Именно многопоточность даёт возможности ускорять backend на nodejs, масштабироваться на многоядерных процессорах, многомашинных обработках с map-reduce и тп. Для простого решения этой проблемы на frontend можно конечно написать костыль самому, но в jQuery уже есть Deferred объекты, с простым синтаксисом..
$.when(Model1.fetch(), Model2.fetch(), Model3.fetch(), Model4.fetch(), Model5.fetch()).then(
function(){alert('success!');}
);
четверг, 19 марта 2015 г. в 02:57:12
Набрёл на интересную бесплатную книжку Тамары Кулинкович по психологии (см. пост на хабре). Сама она занимается HR-процессами, визуализацией данных и тп. Суть в потребностях людей. Будете их удовлетворять - у вас будет хороший продукт
Я в последнее время всё больше люблю писать интеграционные (API) тесты — запускаю половину приложения, но не привязан к UI. Это золотая середина между очень медленными end-to-end тестами и очень быстрыми unit-тестами. Рассмотрим особый случай таких тестов, которые используют заготовленные данные под каждый тест.
Такие тесты приходится создавать, когда проект становится таких масштабов, что тесты с одной БД начинают конфликтовать между собой и становятся нестабильными. То список где-то постоянно растёт, то ID у ресурса требуется фиксированный а у нас autoincrement, то данные хочется удалить.
Это особенно ярко видно в e2e тестах, где приходится управлять всем жизненным циклом данных что-бы тесты оставались рабочими. Управление всем циклом из создания-операции-удаления, вынуждает тесты делать зависимыми друг от друга, а значит становится невозможно запустить тест сам по себе.
В продолжение поста про Радикальная честность, по личному опыту общения с одним любимым человечком, захотелось выписать присущие ему характеристики, а потом нашёл наглядный пример, социопата которого вы могли бы заметить в фильме Gone girl.
Социопат – это человек, лишенный совести. Возникает либо наследственно, либо из-за повреждения лобной доли мозга либо из-за психологической травмы детства (родители постарались). Это душевная инвалидность, не лечится, человека можно лишь адаптировать. Во многом схожи с нарциссами

Если вы заботитесь о качестве своего проекта и кода, то пишете unit-тесты. Но с ними всегда есть «особые случаи». Один из них - работа с файловой системой и ресурсами. Решение «в лоб» - параллельно создавать папку/дерево специально для тестов и надеяться что они не прыгнут на настоящие пути и ничего не удалят.
Более правильный подход - использование in-memory виртуальной файловой системы, vfs. А поскольку ресурсы это по сути потоки, то и название для этой мок-библиотеки - vfsStream. Ставим..
composer install mikey179/vfsStream
Я когда-то писал про то что в phpunit небыло возможности нормально протестировать внутренние методы класса и приходится обращаться к runkit. Незнаю, была ли это моя недоработка, либо с версии 3.8 прошло уже много времени, но в 4.5 эта возможность есть! Моки в 4.5 стали удобней — они умеют перезаписывать как весь класс, так и его части.
У меня подход к разработке эволюционный и к тестам прагматичный — тесты должны позволять постепенно улучшать качество приложения. В реальной жизни приложение уже существует в какой-то форме, а программисты ленивые и тесты не писали. Поэтому надо уметь минимальным рефакторингом добавлять автоматические тесты, постепенно приводя людей и покрытие к вере в Бога TDD. По-оправдывался, теперь к конкретике..
Jira от Atlassian — самый современный трекер задач и багов позволяющий гибко настроить workflow организации. Но если вы не доросли, не хотите использовать Bamboo, а скажем используете PHPCI для автоматического тестирования, то вам возможно будет полезно видеть результаты прогона тестов сгруппированных по фичам.
Это достаточно спорная тем, многие апологеты тестирования не понимают зачем надо связывать тесты с фичами. Такие разработчики либо придерживаются простого мировоззрения что «все тесты должны проходить всегда», либо считают это излишним усложнением не дающего ничего в итоге.
По-моему эта методика достаточно полезна для повышения прозрачности покрытия фич. Если разработчик пишет новую фичу, то это не значит что он сразу же покроет код тестами. И даже если код на 100% покрыт юнит-тестами, это не значит что ошибок нет. Надо писать интеграционные тесты. А интеграционные тесты как правило не генерируют покрытия. Поэтому видеть - написаны ли какие-то тесты для фичи полезны для уверенности. Это справедливо и если вы программист и не следуете методике TDD "test first" и если вы менеджер и для вас покрытие тестами недостаточно прозрачно, не гранулярно.
суббота, 21 февраля 2015 г. в 21:02:54
Кайдзен — по-японски означает «изменение к лучшему», это нечто типа трезвения и осознанности, полезная практика постоянного самосовершенствования (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 секунды, что-бы сервер успел подняться (который уже будет бежать бесконечно)
echo 'db.createCollection("myCollection");' > mongoInit.js
trap 'echo Starting MongoDB with preinstalled collections; kill $(jobs -p)' EXIT
{ trap '/usr/local/bin/mongod --dbpath myMongoDBStorage' TERM; sleep 5 & wait; } &
{ trap 'sleep 3 && mongo myProjectDB mongoInit.js' TERM; sleep 5 & wait; } &
sleep 1
На практике получается впрочем так, что комманды бегут бесконечно, т.е. mongod запускается, вы нажимаете Ctrl+C, а он всё ещё работает.. jobs ничего не показывает, но если возникает соединение - контекст возвращается. Приходится использовать killall mongod
Вдохновлено stackoverflow. См. теорию - Signals & Traps
upd. как оказалось, более простой и эффективный способ - использовать одинарный &, который пушит комманды в фоновый режим и запускает дальше следующие
/usr/local/bin/mongod --dbpath myMongoDBStorage & sleep 3 && mongo myProjectDB mongoInit.js
В таком случае jobs показывает работающий mongod и вобщем то его тоже приходится убивать вручную.
Бесконечный скролл, т.е. постепенный показ и подгрузка данных как реакция на действия пользователя, закрепился в SPA-приложениях. Я работаю с angular, но в целом это может и к backbone относиться..
Технически, данные на frontend хранятся в какой-то коллекции/сервисе. То что показывается пользователю — подмножество этих данных, т.е. может быть отфильтрованным, отсортированным и изменяемым на лету. Таких подмножеств в приложении может быть несколько, в зависимости от отображения (view).
В одном месте у вас товары одного бренда, в другом месте у вас заказанные товары, в третьем - товары конкретного пользователя.

Это значит, что вопрос подгрузки новых данных - нетривиальная задача.
Сначала я подумал что лучше всего запрашивать данные по времени. У каждого ряда данных будет timestamp, как у твиттера, соответсвенно запрос будет высылаться "подгрузи 20 рядов данных, старше X", а время будет браться у последнего ряда.
Но тут возникает несколько проблем.
Вобщем приходишь к тому, что для универсального решения, надо по-прежнему работать с 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 под псевдо-философию, что-бы дать видимость глубины мысли, там где её нет. Дальше спойлеры.
Фильм об обожении, но как и большинство последних голливудских фильмов затрагивающих внутреннее развитие и трансцендентность, он слишком материалистичен. Главная героиня с одной стороны становится сверх-умна, теряет чисто человеческие качества ограниченности сознания (страх, боль, страдание), но при этом не отказывается от насилия и поглощения всего наркотика. Полное раскрытие разума даёт способности по управлению и пониманию всего вокруге, но не ведёт ни к чему продуктивному.