2017-09-17

О MongoDB

После долгого перерыва я снова столкнулся с MongoDB. Не по своей воле.
В этом перерыве я тыкал разное. InfluxDB, чтобы понять, что Graphite, точнее Whisper, нифига не устарел, и вполне имеет право на жизнь. ClickHouse, чтобы окончательно решить, что для такого рода данных, когда нужно хранить разовые события, привязанные ко времени, а потом делать по ним разную хитрую аналитику, я буду использовать только ClickHouse. Но больше всего возился со старым добрым PostgreSQL, интенсивно заюзывая его модный jsonb.
MongoDB logo
А вот теперь пришлось снова тыкать MongoDB. В которую пришлось складывать эти самые разовые события, привязанные ко времени, и делать аналитику.
Историческими судьбами в этой Монге сложилась странная схема данных. Есть три коллекции. В одну события складываются как есть, но живут они там только четыре часа. Появилась в Монге такая возможность, индексы с TTL, что сильно удобнее старых capped collections.
В другую коллекцию складываются типа агрегированные данные, сгруппированные по одному из измерений, и собранные за пять минут. На каждые пять минут, на каждое уникальное значение этого измерения — создаётся новый документ. А в нём массив, который пополняется новыми событиями, которые в течение этих пяти минут возникнут.
В третью коллекцию складываются типа агрегированные данные, сгруппированные по другому измерению. Но собранные за неделю. Так же, в массиве. Почему за неделю, неизвестно. Видимо, потому, что размера монгодокумента хватает, чтобы хранить события за всю неделю для каждого уникального значения измерения.
В Монге по-прежнему искусственно ограничен размер документа. И это по-прежнему 16 мегабайт. Помните, зачем? Потому что операции над документами атомарны. И при передаче по сети шестнадцати мегабайт ещё можно сделать вид, что это происходит «атомарно».
Я сразу забраковал эту схему, потому что она подразумевает постоянные апдейты документов. А старый движок Монги, который теперь называют MMAPv1, модифицировал документы «на месте». Если было место, дописывал на месте. Если не было места, ему приходилось копировать документ в конец коллекции и дописывать там. А чтобы место было, он отводил для документа на диске место с запасом. Поэтому этот движок был чудовищно жаден до диска. И очень не любил, когда документы не просто модифицировались, а ещё и увеличивались в размере. Ну прям наш случай.
Над этим движком смеялись все кому не лень. Все разработчики настоящих баз данных. Ибо это были просто memory mapped файлы. В плане скорости это давало некоторые преимущества. Ибо на диске это BSONы, ОС маппит эти BSONы в память, а движку БД остаётся только кидать BSONы из памяти в сокеты и обратно. Но в плане надёжности сохранения данных это было никак. Там, конечно, прикрутили потом журнал. Но это не особо помогло.
И вот теперь, уже вполне солидно, официально и по дефолту у Монги работает другой движок — WiredTiger. Он уже вполне похож на настоящую БД. Он — версионник, MVCC. Это значит, что он никогда не апдейтит «на месте», для него, что insert, что update — одна фигня. Он жмёт данные на диске. Очень хорошо жмёт, кстати. У него есть свой самостоятельный кэш, а не только дисковый кэш ОС.
У WiredTiger тоже есть журнал. Впрочем, журнал всё равно сбрасывается на диск по таймеру. И если в операции записи вы выражаете желание дождаться записи в журнал, ваш запрос действительно будет ждать следующего тика скидывания журнала на диск, и только потом завершится.
«Связанный тигр» — существенный прогресс MongoDB как настоящей базы данных. Рекомендую, если приходится иметь дело с Монгой.
WiredTiger advantages
BSON. Продолжаю восхищаться этим изобретением ребят из 10gen. Все разработчики БД, которые, на волне хайпа по NoSQL, добавляют поддержку JSON. Не надо, прошу вас. Добавляйте поддержку BSON.
JSON — это текстовая фигня с сомнительной эффективностью хранения и парсинга, и совсем никакой поддержкой нормального набора типов данных. Там нет целых чисел, и тем более long. Там совсем-совсем невозможно нормально хранить бинарные данные. А бинарные данные у вас всегда будут. Оно вам надо, каждый раз эту фигню парсить и сериализовывать на входе и на выходе, даже если у вас внутри это как-то мегоэффективно хранится, как в jsonb?
BSON — это нормальный эффективный бинарный формат. Тут для каждого значения хранится длина, а значит, можно эффективно пробежаться по документу и извлечь только то, что надо. Тут есть нормальные int и long, и массивы байтов, и timestamp. Как правильно замечает Википедия, какой-нибудь ProtoBuf может быть эффективнее. Но в ProtoBuf нужна схема. А BSON гибок как JSON.
BSON example
Кажется, Монга научилась использовать несколько индексов одновременно. Но, похоже, предпочитает по старинке брать лишь один индекс для выполнения данного запроса. Дело в том, что индексы, точнее планы запросов, по-прежнему выбираются методом честного соревнования. Если Монга не помнит, какой план запроса был лучшим, она гоняет все возможные планы, и выбирает самый быстрый, и запоминает его на будущее. Просто. Тупо. Эффективно?
Aggregation framework возмужал. Когда-то это была лишь упрощённая и ограниченная замена map-reduce. Но теперь это довольно мощная штука. Заметно мощнее обычных запросов, но почти такая же эффективная. Судя по тому, с какой скоростью в новых версиях Монги добавляют сюда новые возможности, скоро с aggregation framework можно будет делать всё.
Чаще всего aggregation pipeline собирается из $match, $project и $group.
$match — это аналог where. Здесь можно задать условия выборки документов из коллекции. Тут работают индексы, если они есть.
$project — это аналог выражений после select, проекция. Тут всё значительно мощнее, чем в проекции обычного find(). Можно не только включать и исключать поля. Можно высчитывать любые выражения по содержимому документа. Например, можно взять, и сделать $filter и даже $map по содержимому массива в документе.
$group — это аналог group by. По данному ключу (а в частности, по ключу null, т.е. для всех документов) можно собрать агрегацию: сумму, минимум, максимум, всё такое.
Есть и другие интересные операторы. $unwind позволяет развернуть массив в виде последовательности документов. Примерно так, как это может делать PostgreSQL со своими массивами. $lookup позволяет вытащить из другой коллекции целый документ и воткнуть сюда. По сути, это join.
Все эти милые операторы собираются именно что в pipeline. Выхлоп предыдущего шага является входом следующего шага. Можно строить длинные цепочки преобразований, чтобы в конце получить лишь одно число, как оно часто и нужно в этих агрегациях :)
Всё это достаточно мощно, что люди всерьёз пишут конверторы из SQL в монговские aggregation pipeline.
Aggreration pipeline
Схема данных, как всегда, имеет значение. Не обольщайтесь schemaless, это вовсе не thoughtless для разработчика. В данном случае засада оказалась в количестве «горячих» документов.
С коллекцией номер раз всё понятно. Мы туда просто инсертим, и всё. Никакие документы не модифицируются.
С коллекцией номер два сложнее. Здесь в течение пяти минут интенсивно обновляются одни и те же документы. Сколько их, определяется мощностью множества значений того измерения, по которому происходит группировка. В данном случае — сотни. Практические замеры показали, что за пару часов модифицируются лишь тысячи документов. Несмотря на относительно большой размер этих документов — десятки килобайт — они все прекрасно помещаются в кэш размером меньше полугигабайта. Поэтому проблем с апсертами в эту коллекцию не было.
Проблемы случились с коллекцией номер три. Агрегаты за неделю, по второму измерению. Средний размер документа тут маленький — килобайты. Хотя встречаются документы, вплотную приближающиеся к заветным 16 мегабайтам. Но вот мощность множества значений этого второго измерения — миллионы. Миллионы «горячих» документов, килобайтного размера. Это, по-хорошему, уже нужен кэш в несколько гигабайт.
Памяти добавили, ядер добавили, IOPS добавили. А не помогало.
Оказалось, что я избаловался акторами и прочими прелестями асинхронной обработки. Даже наша фиговина, которая должна была в эту самую Монгу пихать события, хоть и принимала эти события через один сокет в одном потоке, потом вовсю юзала ThreadPoolExecutor, раcпихивала задания по очередям, а потом выполняла в столько потоков, в сколько нужно.
А вот MongoDB, как, впрочем, и большинство других баз данных, включая PostgreSQL, — не такая. Она — синхронная. Одно подключение — один поток, который выполняет все запросы в этом подключении строго последовательно.
И оказалось, что апдейты этой третьей коллекции по неизвестной причине (возможно из-за необходимости распаковывать и запаковывать документы) очень жрут CPU на сервере. Вставляем через одну коннекцию — упираемся в один поток на сервере, который выжирает лишь одно ядро CPU. И всё медленно.
Ок. Сделаем четыре потока на нашей клиентской стороне, нам же всё равно, у нас же ExecutorService. Получаем четыре коннекции к Монге. А там — четыре потока на четырёх ядрах CPU, которые занимаются тем, чем они хотят заниматься. Работает? Работает. Проблема решена. Тупой подход: «Тормозит? Добавь потоков!» — сработал.
Ну, конечно, есть нюансы, которые решились по ходу дела. По-хорошему, нужно не давать разным потокам на Монге лезть в одни и те же документы. Чтобы не порождать конфликтов и блокировок. Делается это переупорядочиванием входящих событий и грамотным созданием заданий в ExecutorService.
ClickHouse logo
И всё-таки, для этой задачи я бы взял ClickHouse. Прям руки чешутся. Зачем нужны все эти предварительные агрегации? Зачем хранить данные в трёх экземплярах? Если можно за приемлемое время быстренько прогрепать всё, что было за указанный период.
Для сравнения, те же самые события мы, ради Истории, решили записывать в логи и складывать в архив. CPU совсем прохлаждается, памяти не нужно, IOPSов тоже не нужно, ибо последовательная запись в один поток. Нужно место на диске. И тут по компактности WiredTiger побеждает gzip :) Подозреваю, ClickHouse уделает обоих. А извлекать данные будет сильно удобнее, чем с помощью zgrep.

2017-09-03

О JWT

А как вы ограничиваете доступ к вашему API?
Понятно, что в нашей аутсорсной разработке, когда это приватное API какого-то сервиса внутри конторы заказчика, можно понаставить огненных заборов, и вообще сделать API доступным только из приватной сети.
А если это публичное, да и к тому же многопользовательское API? Как вы аутентифицируете и авторизуете пользователей? Это ведь API, тут нет формы логина.
API
Тупой, но вполне для начала работающий способ: Basic Authentication. Можно даже взгромоздить проверку паролей на тот же Nginx.
Но, простите, о каких пользователях идёт речь? Это же API. Это не пользователи. Это некие программные агенты стучатся к вам в интересах пользователя.
Поэтому, чтобы не смущать себя эфемерными пользователями и их паролями, клиентам API выдают некие токены. Токен, в данном случае, некая строка, идентифицирующая данного клиента, а иногда и права, выданные клиенту.
Login form
Вопрос первый: где и как получать токены?
На эту тему сломано немало копий. Но, похоже, в случае веб-сервисов, побеждают разные варианты OAuth.
В OAuth для выдачи токена требуются явное участие и согласие юзера, в интересах которого агент будет действовать. Для этого задействуется веб-браузер, а пользователя явно просят в этом браузере по-человечески авторизоваться на сервисе, который будет предоставлять API. Т.е. на вашем сервисе.
Веб-браузер — хорошо. Для агентов, которые сами являются веб-сервисами. Для агентов, которые локальные приложения, вызов веб-браузера выглядит странно, особенно для консольных приложений, но вполне работает.
Если у вас не публичный веб-сервис, а просто бэкенд какого-то вебдванольного приложения, можно, конечно, поступить проще. Просто сделайте метод, который за логин-пароль юзера будет выдавать токен.
OAuth 2.0
Вопрос второй: как передавать токен?
HTTP предоставляет уйму способов. Есть заголовки запроса. Есть параметры запроса. Есть тело запроса (в тот же JSON самого запроса токен добавить). Все варианты имеют место быть.
Более-менее стандартным таки является заголовок Authorization. Причём у него есть тип.
Authorization: <type> <credentials>
Для базовой аутентификации тип — это «Basic». Для передачи токенов в OAuth 2.0 тип — это «Bearer». Ничто не мешает придумать свой «тип», только сначала напишите для этого RFC.
Понятно, что в любом случае токен передаётся открытым текстом. И тот, кто перехватит токен, сможет им воспользоваться, выдав себя за чужого агента. Поэтому, только HTTPS. Шифруемся.
Если у вас не HTTP/HTTPS или не только HTTP/HTTPS, то ничего другого не остаётся, кроме как включить токен одним из параметров запроса (то бишь, в тело запроса). Почему бы и нет.
Authorized
Вопрос третий: где хранить и как проверять токены?
На клиентской стороне, если это браузер, уверенно побеждает LocalStorage. Храните токены там, подсовывайте в запросы к API, и будет вам счастье.
Интереснее, что происходит на серверной стороне. Что API делает с токенами?
Очевидный подход. Пусть токен — это просто случайная строка достаточной длины. Сохраним токен в таблицу (или коллекцию) БД, запишем дату его выдачи (или дату окончания действия), и свяжем с нужными правами нужного пользователя. Всё. Работает. Просто, понятно.
Но на больших масштабах у этого подхода проявляются недостатки. На каждый запрос к API, требующий авторизации, независимо от сути самого запроса, требуется просмотр этой таблицы БД на предмет поиска и проверки токена. Если разных API у нас несколько, и сами API горизонтально отмасштабированы на кучу серверов, обеспечить для всех них актуальный доступ к одной таблице становится не очень простой задачей. К тому же, при миллионах пользователей, токенов может быть ещё больше. Сама таблица может получиться очень не маленькой.
Можно обойтись без всего этого. Пусть сам токен хранит в себе всё, что нужно для идентификации пользователя, выяснения и проверки его прав. Тогда большая таблица не понадобится. Не нужно будет делать в неё запрос. А каждый сервер с API может проверить токен самостоятельно.
А чтобы хитрый пользователь не подделал токен, мы будем его подписывать. В криптографическом смысле. Ключом, известным только серверу (серверам).
JWT and friends
Слава богу, ничего на этом поприще изобретать не надо. Ибо есть стандарт JWT — JSON Web Tokens.
JSON, потому что всякие служебные данные и сведения о пользователе представлены в виде JSON.
Типичный JWT токен выглядит так:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
Расшифровать можете на упомянутом jwt.io.
Это три «слова» в base64 кодировке, разделённые точкой.
Первое «слово» — заголовок. Описывает, какие алгоритмы мы используем для подписи и какого типа содержимое токена используем.
$ echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | base64 -d
{"alg":"HS256","typ":"JWT"}
В данном случае «HS256» означает HMAC SHA256, алгоритм подписи такой. А «JWT» означает, что тело токена — действительно JSON в понятиях JWT.
Второе «слово» — тело токена.
$ echo "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9" | base64 -d
{"sub":"1234567890","name":"John Doe","admin":true}
Это JSON, который содержит некоторые свойства, которые в JWT называют claims, т.е. утверждения.
Некоторые claims определены стандартом JWT. Они, для компактности, трёхбуквенные. iss, issuer — идентификатор того, кто выдал токен. sub, subject — кому выдан токен, например, это идентификатор пользователя. exp, expiration time — время жизни токена, в виде unix timestamp в секундах. iat, issued at — момент выдачи токена, в виде unix timestamp в секундах.
Остальные claims зависят от данного API. Можно их зарегистрировать, чтобы ни с кем не пересечься. А можно просто набросать любых слов или uri.
Третье «слово» — подпись, в формировании которой используется некоторый секрет.
$ openssl dgst -sha256 -mac HMAC -macopt key:secret -binary \
  <( echo -n "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9"; ) \
  | base64
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ=
Надо же, openssl умеет.
JWS token structure
В мире Java с JWT токенами можно работать, например, с помощью jjwt.
Генерировать токен можно примерно так.
fun generateToken(username: String): String {
    return Jwts.builder()
            .setSubject(username)
            .setExpiration(Date(System.currentTimeMillis() + EXPIRATION_TIME))
            .signWith(SignatureAlgorithm.HS512, SECRET)
            .compact()
}
Поместите это в какой-нибудь контроллер, который отвечает за логин и выдачу токенов. Можно добавить и любые другие Claims при необходимости.
Проверять токены можно с помощью Spring Security, который нужно соответствующим образом сконфигурить.
@Configuration
@EnableWebSecurity
open class WebSecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
                // And filter other requests to check the presence of JWT in header
                .addFilterBefore(JWTAuthenticationFilter(),
                        UsernamePasswordAuthenticationFilter::class.java)
    }

}
Проверять заголовок «Authorization» будет соответствующий фильтр.
class JWTAuthenticationFilter: GenericFilterBean() {

    private val SECRET = "secret"

    override fun doFilter(request: ServletRequest,
                          response: ServletResponse,
                          filterChain: FilterChain) {
        val authentication = getAuthentication(request as HttpServletRequest)

        SecurityContextHolder.getContext().authentication = authentication
        filterChain.doFilter(request, response)
    }

    private fun getAuthentication(request: HttpServletRequest): Authentication? {
        val token = request.getHeader("Authorization")
        if (token != null) {
            val user = Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token.replace("Bearer ", ""))
                    .body
                    .subject

            return if (user != null)
                UsernamePasswordAuthenticationToken(user, null, listOf())
            else
                null
        }
        return null
    }

}
Ну а добраться до аутентифицированного пользователя можно стандартными для Spring Security средствами.
@RestController
open class UserController {

    @RequestMapping("/user")
    fun user(): UserResponse {
        val authentication = SecurityContextHolder.getContext().authentication
        val username = (authentication.principal as? UserDetails)?.username ?: authentication.principal.toString()
        return UserResponse(username)
    }

}
Проверяем.
$ curl http://localhost:8080/user
{"timestamp":1504417496948,"status":403,"error":"Forbidden","message":"Access Denied","path":"/user"}

$ curl http://localhost:8080/login
{"token":"eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTUwNDQyNDcxNn0.a9P1SroxiSTUraFdhYwUMjY0tFdhehIRa-R3oOfuW5Ov0INjKH0bS3b57PLF8rhj3uEYXzcPtplID-ncJkWJZg"}

$ curl http://localhost:8080/user \
    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTUwNDQyNDcxNn0.a9P1SroxiSTUraFdhYwUMjY0tFdhehIRa-R3oOfuW5Ov0INjKH0bS3b57PLF8rhj3uEYXzcPtplID-ncJkWJZg'
{"usename":"admin"}
Больше примеров и кода можно найти тут и здесь.
Spring
Spring Security, конечно же, уродлив. С нормальным middleware всё выглядит значительно красивее (потому что больше магии).
from flask_jwt import JWT

app = Flask(__name__)

app.config['JWT_AUTH_URL_RULE'] = '/api/v1/auth'
app.config['JWT_EXPIRATION_DELTA'] = timedelta(hours=2)

from app.models import User

def authenticate(username, password):
    # тут отрабатывается /api/v1/auth, по логину паролю извлекаем юзера
    user = User.query.filter(User.username == username).first()
    if user and check_password_hash(user.password, password):
        return user

def load_user(payload):
    # тут извлекаем юзера по телу токена
    user = User.query.get(payload['identity'])
    # тут конечно плохо, что за юзером лезем в базу, лучше его всего в токен засунуть
    return user

jwt = JWT(app, authenticate, load_user)


from flask_jwt import jwt_required, current_identity as user

@app.route('/users/get_user_info')
@jwt_required()
def get_user_info():
    # тут магическим образом юзер (точнее current_identity) стали доступны
    return jsonify(
        user_name=user.username,
        user_id=user.id
    )
Flask
Обратите внимание, что JWT токен не зашифрован. Он только подписан. Такие токены называют JWS — JSON Web Signature. Владелец токена, т.е. сам юзер, вполне может прочитать, что вы там про него храните.
Если вы не хотите лишний раз смущать пользователя, или же хотите уменьшить последствия компроментации токена (одно дело, получить доступ к API и узнать ID данного пользователя, другое дело, узнать email пользователя или даже пароли от других сервисов), то можно и зашифровать тело токена. Такие токены называют JWE — JSON Web Encryption.
Скучные подробности можно почитать тут.
JWE token structure
jjwt не умеет JWE. А вот какой-нибудь сишарпный Jose — умеет.
private string EncodeToken(User user)
{
    var payload = new TokenData
    {
        Id = user.Id,
        Email = user.Email,
        Name = user.DisplayName,
        exp = NowTimestamp() + _tokenTtl
    };
    return Jose.JWT.Encode(payload, _tokenEncryptSecret,
        JweAlgorithm.PBES2_HS512_A256KW, JweEncryption.A256CBC_HS512, JweCompression.DEF);
}

private User DecodeToken(string token)
{
    var payload = Jose.JWT.Decode<TokenData>(token, _tokenEncryptSecret);
    if (payload.exp < NowTimestamp())
    {
        throw new Jose.IntegrityException("Token expired");
    }
    return new User
    {
        Id = payload.Id,
        Email = payload.Email,
        DisplayName = payload.Name
    };
}
Явисты, не расстраивайтесь. Для Явы тоже всё есть, просто другая библиотека.
JOSE — это Javascript Object Signing and Encryption. Общий термин, объединяющий JWT, JWS, JWE, а также JWK (JSON Web Key). Сначала добавляли буковку S — Simple, потом X — eXtensible, теперь, похоже, наступила мода на J.
JWT is JWS and JWE
Ещё раз. JWT — это самодостаточные токены, которые не требуют обращения к какой-либо БД, чтобы удостовериться, что этот токен был выдан определённому пользователю. Соответственно, их самих нет нужды хранить в какой-либо БД и делать по ним поиск.
Из этого проистекает недостаток: JWT токены почти невозможно отозвать. Ведь нет БД, с которой можно сверяться по вопросам валидности.
Конечно, есть тяжёлая артиллерия. Достаточно сменить ключ, которым подписываются и проверяются токены, на всех серверах. И все имеющиеся в ходу токены автоматически станут невалидными. В крайних случаях можно (и, пожалуй, нужно) поступать именно так.
А для уменьшения последствий компроментации в обычной жизни JWT токены нужно делать короткоживущими. Несколько часов, максимум дней.
Короткоживущие токены — это неудобно. Ведь юзеру придётся при протухании токена перелогиниваться.
Поэтому в OAuth используют два токена. Основной access token, короткоживущий, который может быть JWT, сам в себе содержит нужные данные и может быть проверен без доступа к БД. И refresh token, долгоживущий, может храниться и проверяться в БД, используется для обновления и выдачи нового access token.
Получается, мы можем получить все преимущества самодостаточности JWT, аутентифицируя тысячи запросов без лишнего обращения к БД, и при этом получить долгую, удобную для юзеров, сессию, с временем жизни refresh токена. Если, конечно, обновлять токены будем автоматически.

2017-08-20

О Зенбуках

Я влюблён в Зенбуки. Которые Asus Zenbook. Которые когда-то назывались ультрабуками, а теперь, когда все обычные ноутбуки стали такими же компактными, стали называться просто классическими ноутбуками. Классическими, потому что есть ещё всякие с сенсорным экраном или выворачивающиеся наизнанку.
Zenbook lid
Предыдущий Зенбук служил мне верой и правдой почти четыре года. Теперь я обзавёлся новым. Вот их сейчас и буду сравнивать. А ещё несколько Зенбуков я купил и нарекомендовал родственникам и знакомым.
Старичок. Asus Zenbook UX32VD. 2012 год. Вес: 1.4 кг (если верить моим напольным весам).
Экран. 13.3 дюйма, FullHD, честные 1920×1080 точек, матовый. Экспериментально я выяснил, что для меня это оптимальный размер и разрешение. Больше размер — тяжелее таскать. Меньше размер — ничего не помещается. Всё, конечно, становится мелковато, но абсолютно ничто не мешает поставить шрифты покрупнее.
UX32VD screen
Клавиатура. Хорошая, удобная, с подсветкой. Широкие шифты. Большой, хоть и не высокий, Enter. Правильный Ctrl, ближе к краю, чем Fn. Поначалу не хватало всяких Home/End/PgUp/PgDown, но быстро привык к их альтернативам с Fn. Новичков пугает отсутствующий Insert (он есть только с Fn) и кнопка выключения питания прямо на клавиатуре. Но я особых неудобств от этого не ощутил.
UX32VD keyboard
За годы ношения в рюкзаке клавиатура заметно отпечаталась на экране. Надо было таскать прокладку-тряпочку, что шла с завода. Но не факт, что помогло бы. Видел подобные полосы от пробела и на экранах старых макбуков.
UX32VD screen abrasions
Тачпад. Он есть. Он нормальный. Жесты множеством пальцев и блокировка тачпада, когда печатаешь, чтобы курсор куда не надо не прыгал, решаются исключительно софтом. Убунта справляется.
Камера, микрофон, всё путём.
Проц. Мне достался Core i7-3517U, Ivy Bridge, 22 нм, 1.9 ГГц, два ядра, четыре потока (Hyper-threading), 4 Мб кэш, 17 Вт. Топовая зверушка для этой линейки Зенбуков.
Память. DDR3. От рождения было 4 гигабайта. Причём два распаяно на материнке, а два засунуто в единственный SO-DIMM слот. В этот слот я сначала воткнул четыре, а потом и восемь гигабайт. В итоге получил 10 гигабайт ОЗУ. Максимум для данной конфигурации.
Диск. От рождения был снова распаянный на материнке SSD на 24 гига, и SATA HDD на 500 гигов. Я долго добирался до того, чтобы приспособить SSD как кэш к HDD, и оно получилось с помощью bcache. А потом встроенный SSD сдох, я решил, что это знак, выкинул HDD и поставил вместо него SSD на 480 гигов. Всё залетало, и я стал счастлив.
Видюха. Да, в этом ноуте есть какой-то GeForce. Я про него вспоминал три раза. Сначала, когда пришлось поставить Bumblebee, чтобы отключить эту видюху нафиг, ибо время работы от батареи, когда Nvidia включена, драматически снижалось раза в два. Более поздние Убунты научились отключать видюху самостоятельно. И последующие два раза, когда я ставил Steam и думал, что вот сейчас таки поиграю хоть чуть-чуть на ноуте. Не сложилось, не поиграл. Так видюха и была постоянно выключена, а работала встроенная интеловая графика.
Батарея. 6520 мА·ч. От рождения хватало на три-четыре часа. Сейчас может внезапно вырубиться минут через 20. Надо бы менять, но пока нет острой необходимости.
Корпус. Металлический. С иллюзией малой толщины. Динамики в передней части по бокам, смотрят вниз, громкие. Сзади снизу есть вентиляционные отверстия. Дует, как на макбуках, в щель между корпусом и крышкой.
UX32VD bottom
Разбирается достаточно легко. Главное найти отвёртку нужного размера звёздочкой. Двенадцать винтов снизу, и вся нижняя крышка снимается целиком. Тут вам сразу память и аккумулятор, и пыль сдуть с кулеров (там два вентилятора). SATA диск поменять? Нужно сначала открутить и слегка приподнять аккумулятор. Всё просто.
UX32VD internals
Слева. Один USB 3.0. И слот для SD карты. Всё. Слот такой, что SD карта не защёлкивается, но наполовину торчит. Немного непривычно, но вполне достаточно, чтобы через переходник скинуть что-нибудь на micro-SD из телефона.
UX32VD left side
Справа. Разъём блока питания. Никаких магнитных разъёмов, извините, это вам не мак. Два USB 3.0. Полноразмерный HDMI. Совершенно нестандартный порт для дисплея, в комплекте есть переходник на VGA. Совмещённый mini jack на наушники и микрофон.
UX32VD right side
Всего, получается, три USB. И все 3.0. Внешние жёсткие диски подрубать — милое дело. Работают как ураган.
HDMI справа доставляет хлопот, если постоянно приходится подключать внешний монитор. А я даже рекомендую подключать. Два монитора минимум в два раза удобнее, чем один. Провод справа мешает елозить мышкой. Я решил это покупкой нестандартного HDMI кабеля, изогнутого на 90 градусов.
Разъём для дисплея и переходник на VGA — странные. Без танцев с бубном не выйдет вывести что-то, кроме 1024x768. Как будто там не все провода развели, и дисплей не может сообщить видюхе, какие разрешения он умеет (я про DDC).
Совмещённый jack доставляет хлопот. Подходят гарнитуры от сотовых. А вот «компьютерных» гарнитур с таким подключением фиг найдёшь. Впрочем USB гарнитуры вполне работоспособны в Убунту. Да и встроенные динамики и микрофон работают вполне прилично, если нужно громкую конференцию устроить.
Ещё в комплекте. USB сетевуха, работает под Linux. Чехольчик для сетевухи и переходника VGA. Замечательный тряпошный «конвертик» для самого ноутбука.
UX32VD accessories
UX32VD case
Виндой я на этом ноутбуке никогда не пользовался. Сразу ставил Ubuntu, которая дефолтная, с почившим Unity. Поначалу пришлось руками прикручивать Bumblebee, чтобы отрубить Nvidiaвскую дискретную видеокарту. И ставить ядро от следующей версии Убунту, чтобы работали кнопочки регулировки яркости экрана. Со следующей Убунтой и её LTS версиями вообще проблем с железом не было.
Новичок. Asus Zenbook UX310UA. 2016 год. Вес: 1.4 кг (а субъективно вроде чуть полегче).
Экран. Те же 13.3 дюйма. Есть экраны FullHD, я именно такой и хотел. Но не получилось. Взял с разрешением QHD+, которое 3200x1800 точек. Только что понял, что я окончательно запутался в этих 4K разрешениях. На телевизоре-то у меня Ultra HD, т.е. 3840×2160.
UX310UA screen
С разрешением вышло несколько засад. GRUB hidpi понимает и рисует большие буквы. В текстовом терминале поставить шрифт побольше поможет sudo dpkg-reconfigure console-setup. Свежие KDE нормально позволяют настроить масштабирование.
Только вот дробный скейлинг, а я поставил поначалу 1.7, далеко не все переваривают. Половина софта, которые используют WebView, отображают этот WebView весь размытый в квадратных пикселях. Некоторые, как ни странно, Qtшные, программы вообще не масштабируются, или даже рисуют интерфейс 1:1 в уголочке отмасштабированного окна.
Целый скейлинг в 2.0 переварили почти все. И выглядело всё красиво, хотя чуток крупновато.
Но вот скейлинг, мало того, что требует перелогина, так ещё и штука, глобальная для всех мониторов. Поэтому с внешним монитором с обычным dpi это нифига не работает. Нужен один масштаб на оба монитора, и надо что-то выбирать.
Я пока остановился на FullHD на обоих мониторах. Без скейлинга. Как ни странно, несмотря на то, что на QHD+ FullHD должно отображаться с искажениями, я их не замечаю. Сильно мелкие пиксели. И в лупу я их не разглядываю.
Как бы надо внешний 4K монитор :)
Клавиатура. Абсолютно такая же. Ну разве что значочки погрубее нарисованы, да логотип Венды более новой версии.
UX310UA keyboard
Тачпад. В обзорах иногда ругают. В биосе (ну который EFI Setup) действительно чувствительность его чересчур задрана. Но в KDE для тачпада дофига настроек. И по умолчанию все они делают всё нормально. Даже отключение при печатании есть. Я лишь включил прокрутку по всем направлениям двумя пальцами и изменил направление этой прокрутки.
Камера, микрофон. Есть. Вроде добавился ещё датчик освещённости, но я с ним не игрался.
Проц. Достался Core i5-7200U, Kaby Lake, 14 нм, 2.5 ГГц, два ядра, четыре потока, 3 Мб кэш, 15 Вт. Норм. Вообще в линейке имеются ещё и Зенбуки с i7-7500U, но я такой не нашёл.
Память. DDR4. Комплектации с 16 Гб от рождения не нашёл в продаже, хотя такие, теоретически, есть. Пришлось взять с 8 Гб. Оказалось, что эти 8 Гб уже распаяны на плате, а SO-DIMM стоит пустой. Я туда воткнул ещё 8 Гб. Всего получилось 16. Хватает. На несколько докеров, пару виртуалок, пару Идей и пару десятков вкладок Хрома. И половина ОЗУ ещё дисковым кэшем занята. Так как npm я запускаю редко, 32 гигабайта, надеюсь, не понадобятся ещё в ближайшие года четыре.
Диск. По максимуму предлагают 512 Гб SSD (в M.2), плюс 1 Тб HDD в SATA. Я такое опять не нашёл, да и нафиг мне HDD. Поэтому в моём имеется 256 Гб M.2 SSD, и я воткнул вдогонку 480 Гб в SATA. Чтобы SATA пустым не стоял, и диска побольше было. На второй диск я засунул самые большие каталоги из моего хомяка с помощью mount --bind и pam_mount. Получилось, что ОС и всякий инструментарий у меня на одном диске, а всякие проекты, которые этим инструментарием собирать, — на другом. Почти просторно.
Видюха. А нет её. То, что вшито в Kaby Lake, то и есть. Существует, конечно, UX310UQ, который тоже с дискретной видеокартой от Nvidia. Но я сознательно от дискретной карты отказался. Ибо не пользуюсь. А лишняя грелка и батареепожиралка не нужна.
Батарея. 4210 мА·ч. Вроде меньше батарея, но хватать должно на 4-5 часов, если верить показаниям. Испытания не проводил. Мне достаточно знать, что эта штука не умрёт внезапно в течение часа-полутора, если её отнять от розетки.
Корпус. Металлический. С иллюзией малой толщины. На углах круглее, и в целом как-то помонолитнее, чем раньше. Дисплей открывается на чуть меньший угол, что огорчает. Динамики спереди внизу, очень громкие, но без искажений. Снизу вентиляционных отверстий теперь нет, всё дует в щёлку между корпусом и крышкой.
UX310UA bottom
Lid open angle
Пока сам не разбирал. Чтобы не терять в гарантии, отдал установку памяти и второго SSD официальному сервисному центру. Но теоретически всё так же, нижняя крышка снимается целиком. Только вот пара винтов теперь запрятана под резиновые ножки.
Разъёмы в этой серии Зенбуков переставили местами. То, что было слева, теперь справа, и наоборот. И это хорошо. Теперь HDMI кабель мышке не мешает.
Слева. Разъем питания, точно такой же, как был. Только сам БП теперь менее мощный, потому что нет дискретной видеокарты. Один USB. Полноразмерный HDMI. Один USB Type-C. Фиг знает, что он там умеет, мне пока нечего в него воткнуть. Совмещённый mini jack на наушники и микрофон.
UX310UA left side
Справа. Два USB. Слот для SD карты, такой же, как был. Ха, хоть разъем питания уехал налево, лампочки-индикаторы зарядки и включения остались справа.
UX310UA right side
Всего получается три обычных USB. 3.0 или 2.0 — непонятно. Синеньким они не помечены. Внешние жёсткие работают быстро. Официальная спецификация говорит, что два из них всё же 2.0. Если правда, то это огорчительно.
HDMI теперь слева. Хорошо. И в комлекте идёт переходник HDMI-VGA. Ещё не проверял, но надеюсь, он будет поумнее. Впрочем, пока нет необходимости его использовать. Везде, куда надо подключаться, есть HDMI.
В комлекте снова USB сетевуха. Но чехольчика для всех этих переходников нет. А «конвертик» снова на месте. Не такой клёвый и отвратительно чёрный, но вполне функциональный.
Accessories
UX310UA case
Ввиду кончины Unity я решил воскресить своё увлечение KDE, которое прервалось с выходом KDE 4. И поставил KDE neon. Это та же Ubuntu LTS, но со свеженькой Plasma (5.10 на текущий момент). Плазма — не падает. А вот панельки — бывает.
В плане железа вообще никаких проблем нет. Всё работает. Хотя не всё ещё проверить довелось.
В плане софта поймал баг в SDDM (Simple Desktop Display Manager). Эта та штука, которая в современном KDE пароль на вход (и не только) запрашивает. Оно не запускается, когда в pam_mount подрублены зашифрованные тома LUKS. Пока ограничился вводом ещё одного пароля при загрузке, LUKS раздел прописан в /etc/crypttab.
Долго мучался в попытке воссоздать поведение верхней панели Unity. Там ведь не только глобальное меню, а ещё и заголовок распахнутого окна, и они сменяют друг друга. Сделать так в KDE в настоящий момент нельзя. Разве что кто-нибудь таки напишет правильный плазмоид.
Хотя само глобальное меню работает. Правда не из Java. Jayatana и в Unity глючила, а в KDE вообще не работает.
Я убрал глобальное меню в кнопочку в заголовке окна (в свежей Плазме можно и так). А всякие индикаторы задвинул вправо наверх, на плавающую поверх всех окон панельку. Получилась почти Unity. Местами удобнее, местами нет.
В KDE оказалось, что на каждом экране панельки свои, и так просто они на все экраны клонироваться не хотят. Впрочем, фиг с ним, с KDE. Мы же о Зенбуках.
Кроме вот таких вот Zenbook, которые обычные ноутбуки, но маленькие, без CD/DVD и прочих езернетов, у Asus есть ещё и другие. Zenbook Pro, с 15 дюймовым экраном. Transformer Book, у которого экран отсоединяется от клавиатуры и является, по сути, планшетом. Или Transformer Book Flip, где экран можно вывернуть наизнанку и снова получить планшет.
Да и самих классических Зенбуков весьма много. Вот этих UX310U имеется две «подсерии»: UX310UA, без дискретной видюхи, и UX310UQ, с дискретной Nvidia. В рамках каждой «подсерии» доступны конфигурации с двумя разными экранами: FullHD и QHD+, с разными процами: от i3 до i7, с разным объемом памяти и разным набором и объемом дисков.
Явно не хватает конфигуратора. Чтобы выбрать, заказать и подождать то, что нужно. Я бы взял UX310UA с i7, 16 гигами ОЗУ и SSD на 512 гигов. И готов был бы подождать даже пару месяцев. Но такой конфигурации просто не было в продаже. Нигде в России. Проц помощнее и памяти побольше почему-то ставили в UX310UQ, но мне не нужна дискретная видюха. Пришлось собирать из кусочков: добивать памяти и диска.
В общем, живу, не болею. Продолжаю любить Зенбуки.
Two cases
Two laptops

2017-08-06

О Makefile

Вы делали когда-нибудь «заклинание» ./configure && make && make install? Им собирается 80% юниксовых/линуксовых программ из исходников. configure — это Autotools, про них я не буду рассказывать. make — это GNU make.
Learn Makefile
Оригинальный make появился, если верить Википедии, в 1977 году, в недрах Bell Labs. Сейчас имеется два мейка: BSD make и GNU make. Чем они отличаются — это отдельный религиозный вопрос. Но если у вас Linux, у вас по-любому будет GNU make.
Make в качестве параметра принимает имя некоторого правила. А правила задаются в файле Makefile в текущем каталоге.
Зачем явисту может понадобится make и Makefile? Ведь у нас есть великолепнейшие системы сборки: Ant (кто-нибудь им ещё пользуется?), Maven, Gradle. Они действительно могут многое: и собрать, и прогнать тесты, и запустить (сервер локально), и задеплоить (в удалённый контейнер приложений/сервлетов или собранный артефакт в репозиторий). Но, вы всегда помните, что именно надо написать после mvn, чтобы пересобрать супер-jar, задеплоить его в Artifactory, и при этом вам зачем-то хочется пропустить запуск тестов?
А ведь ещё есть более посторонние штуки. Как запустить Docker контейнеры вашей системы локально? docker-compose up или лучше docker-compose up -d? А в каком каталоге? А надо ли перед этим ещё что-то собрать?
А вы деплоите с помощью Ansible? А вы используете роли? А в каком каталоге проекта это всё у вас лежит? А у вас отдельные inventory файлы для разных окружений? Вариантов может быть очень много, а от этого зависит команда, которую нужно выполнять для деплоя. У нас на проектах получается что-то вроде: cd ansible && ansible-playbook -i production deploy.yml. И это далеко не самый каноничный вариант.
Выходов из необходимости запоминать длинные, но необходимые команды есть несколько.
Самые ленивые команды оставляют всё как есть. В результате новичку приходится надоедать старичкам в попытке вытянуть крупицы тайных знаний о том, как собирать и работать с проектом.
Самые упорные команды пишут документацию. Да, в каком-нибудь README.md рядышком, или в вики проекта, или, упаси боже, в корпоративной вики. Описывают всё. Что куда как пойти, и какие команды выполнить. Новичку приходится долго читать, тщательно копировать команды. И молиться, что документация не устарела, а команды не содержат ошибок.
Самые продвинутые команды, которые работают в Linux и слышали, что такое shell script... Хотя ладно, батники в Windows тоже никто не отменял... Они пишут эти самые скрипты. Помещая туда самые сложные команды или последовательности команд. Беда в том, что ворох скриптов не добавляет понимания, даже наоборот. И скрипты приходится документировать.
GNU Make O'Reilly book
Maven и Gradle всегда имеют свой жизненный цикл. И вы лишь можете выбрать, какой вариант этого цикла и с какого места запустить. Дополнительные плугины, особенно в Gradle, изрядно усложняют и дополняют этот жизненный цикл. Количество возможных «слов», которые можно приписать после mvn или gradlew, просто чудовищно.
Ant работал по-другому. Там надо было явно прописывать targetы, со своими, нежно выдуманными, именами, и зависимостями между ними. И вот эти таргеты и выполнялись.
В этом смысле make подобен antу. Можно считать, что в Makefile вы пишите отдельные именованные правила сборки, тоже с зависимостями между ними, а потом запускаете. Только в make одним шагом сборки может быть любая команда или последовательность команд, доступных для выполнения в данной ОС, а не команды ограниченного набора плугинов. Make работает на том же уровне, что shell скрипты. Он может объединить систему сборки с инструментами деплоя и всем прочим.
all: build

build: jar docker

jar:
    mvn package

docker:
    docker-compose build

run:
    docker-compose up

clean:
    mvn clean
    docker-compose down
Синтаксис объявления правила такой.
имя-правила: зависимость-1 зависимость-2
    команда-1
    команда-2
Тут ахтунг. Makefile — это единственный известный мне синтаксис, чувствительный к табуляции. Команды в правиле выделены не просто отступом, а именно что знаком табуляции. Который ASCII 0x09, "\t", , U+0009.
Большинство программистских редакторов уже давным давно при нажатии кнопки Tab ↹ делают именно отступ, а не вставляют табуляцию. Поэтому .editorconfig вам в помощь.
[Makefile]
indent_style = tab
На самом деле, в терминах самого make, это нифига не имя правила, зависимости и команды. Это цели, пререквизиты и рецепты.
targets : prerequisites ; recipe
    recipe
    …
Цель — это не абстрактное имя, выбранное вами, а имя файла, который должен появиться в результате выполнения рецепта. Целевой файл правила.
Пререквизиты — это тоже имена (или маски) файлов, которые при этом должны быть новее (смотрится дата последней модификации), чем цель. И эти файлы нужны рецепту, чтобы создать цель.
Рецепт — это рецепт. Команды.
Всё это создавалось для компиляции программ на C. Тут принято, что результат компиляции, это файл с тем же именем в том же каталоге, но с другим расширением.
foo.o : foo.c defs.h
    cc -c -g foo.c
Более того, много правил именно для C уже неявно присутствуют в GNU make. В Makefile достаточно лишь указать название итоговой программы, и из каких модулей она должна быть собрана.
При большом желании можно повторить подобное и для Явы. Ведь Maven, в отличие от Gradle, не умеет не пересобирать проект, когда ничего не менялось. Придётся только немного извратиться, ибо в мире Ява принято рассовывать исходники и результат компиляции в разные каталоги, и к тому же сами исходники щедро раскиданы по развесистому дереву каталогов.
# правило сборки jar: для jar нужен jar в target
jar: main-project/target/*.jar

# http://stackoverflow.com/questions/4036191/sources-from-subdirectories-in-makefile
# Make does not offer a recursive wildcard function, so here's one:
rwildcard=$(wildcard $1$2) $(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2))

# определения длинных списков файлов
ALL_MAIN_JAVA := $(call rwildcard,main-project/src/,*.java)
ALL_MAIN_RESOURCES := $(call rwildcard,main-project/src/main/resources/,*)
ALL_LIB_JAVA := $(call rwildcard,lib-project/src/,*.java)
ALL_LIB_RESOURCES := $(call rwildcard,lib-project/src/main/resources/,*)

# правило сборки jar, ему нужны все эти файлы
main-project/target/*.jar: $(ALL_MAIN_JAVA) $(ALL_MAIN_RESOURCES) $(ALL_LIB_JAVA) $(ALL_LIB_RESOURCES)
        mvn package
Целями и пререквизитами могут быть маски файлов. А в данном случае с помощью грязного хака получается маска с рекурсивным поиском по подкаталогам.
Так как цели — это файлы, но некоторые цели — выдуманные имена, то может случиться конфуз, если файл или каталог с именем «jar» или «clean», или, что уже вполне вероятно, «build», окажется в текущем каталоге. Make тогда может посчитать, что сборка уже имела место, и не выполнит рецепт.
Чтобы этого избежать, цели с выдуманными именами надо явно помечать как «.PHONY».
.PHONY: jar
jar:
    mvn package

.PHONY: clean
clean:
    mvn clean
    docker-compose down
.PHONY — это тоже цель, но по специальным искусственным значением.
Как вы уже наверное догадались, если запустить make без параметров, то будет выполнена первая цель.
В Makefile можно определять переменные. А можно и переопределять переменные. Можно определить дефолтные значения в самом Makefile, а переопределить в командной строке или через переменные окружения.
VAR ?= makefile variable

print:
    echo $(VAR)
$ make
echo makefile variable
makefile variable
$ make VAR="overridden variable" print
echo overridden variable
overridden variable
$ VAR="environment variable" make print
echo environment variable
environment variable
Заметьте, что значения переменных подставляются в текст рецепта как есть, и лишь потом рецепт выполняется. В то же время рецепт, если является командой оболочки, может определять и использовать свои переменные. Нужно только правильно экранировать «$», удваивая его.
Рецепты (каждый сам по себе, если их несколько) выполняются в отдельном подпроцессе. Причём это вовсе не обязательно будет процесс оболочки. Make может просто запустить указанную команду, т.е. сам работает оболочкой. Так что если вам нужен именно bash или zsh для выполнения команды, об этом надо специально позаботиться.
shell:
    echo 1-$$$$-$$0
    sh -c "echo 2-$$$$-$$0"
$ make
echo 1-$$-$0
1-15199-/bin/sh
sh -c "echo 2-$$-$0"
2-15200-/bin/sh
Соответственно, если вы делаете cd куда-то && что-то сделать, текущий каталог меняется только для этой команды, что очень удобно. А если в этом другом каталоге есть свой Makefile, то можно выполнить его правила. Это тоже очень удобно, иметь по Makefile в каждом подпроекте, со своими правилами сборки. А на «глобальном» уровне объединять сборки компонентов под одним правилом и делать другие глобальные вещи.
build:
    cd sub1 && $(MAKE) build
    cd sub2 && $(MAKE) build
$ make
cd sub1 && make build
make[1]: Entering directory '.../dirs/sub1'
echo building sub1
building sub1
make[1]: Leaving directory '.../dirs/sub1'
cd sub2 && make build
make[1]: Entering directory '.../dirs/sub2'
echo building sub2
building sub2
make[1]: Leaving directory '.../dirs/sub2'
Здесь MAKE — это предопределённая переменная, которая содержит полный путь до текущего make. Это чтобы не зависеть от всяких путей типа /usr/bin/make.
Makefile — это не только про сборку. Например, вместо длинных объяснений и тыкания в ссылки официальной документации, можно просто дать Makefile, в котором всё есть.
Вот, например, как поставить Docker и Docker Compose на Red Hat Enterprise Linux?
DOCKER_VERSION=17.03.2.ce
DOCKER_COMPOSE_VERSION=1.11.2

.PHONY: help
help:
    @echo "make install - to install Docker and other necessary components"
    @echo "make pull    - to download and update necessary Docker images"
    @echo "..."

# docker install targets

.PHONY: install
install: add_docker_repo install_docker start_docker install_docker_compose

.PHONY: add_docker_repo
add_docker_repo:
    sudo yum install -y yum-utils
    sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    sudo yum -y makecache fast

.PHONY: enable_extras_repo
enable_extras_repo:         # it's necessary to install container-selinux which is required by Docker
    sudo yum-config-manager --enable rhel-7-server-extras-rpms
    sudo yum -y makecache fast

.PHONY: install_docker
install_docker:
    sudo yum -y --setopt=obsoletes=0 install docker-ce-$(DOCKER_VERSION)

.PHONY: start_docker
start_docker:
    sudo systemctl enable docker
    sudo systemctl start docker

.PHONY: install_docker_compose
install_docker_compose:
    sudo curl -L https://github.com/docker/compose/releases/download/$(DOCKER_COMPOSE_VERSION)/docker-compose-Linux-x86_64 -o /usr/bin/docker-compose
    sudo chmod +x /usr/bin/docker-compose
Есть в Makefile и условия, и циклы. Читайте документацию и разбирайтесь.
Старайтесь лишь не переусложнять Makefile, который вы пишите руками (Autotools генерируют Makefile, в результате он пугает). Оставьте только самые важные и часто используемые команды, чтобы было удобно.

2017-07-22

О java.time

Ну наконец-то, аж в восьмой яве, появилось отличное и правильное API для работы со временем. Теперь можно смело выкинуть java.util.Date и java.text.DateFormat. Теперь у нас есть java.time.
Java Time!
Чаще всего вам понадобится просто отметка времени, точка на временной оси. Это — java.time.Instant.
Можно получить момент времени сейчас. Можно получить момент времени из юниксовых секунд от начала эпохи. Можно получить момент времени из миллисекунд от начала эпохи, как это представляется в java.util.Date. А можно взять момент времени из строки в формате ISO 8601 с буковкой «Z» в конце, что означает, что это время в UTC, а значит, не подвержено особенностям разных таймзон.
>>> import java.time.Instant
>>>
>>> Instant.now()
2017-07-22T08:39:59.665Z
>>> Instant.ofEpochSecond(1_500_000_000)
2017-07-14T02:40:00Z
>>> Instant.ofEpochMilli(1_500_000_000_000)
2017-07-14T02:40:00Z
>>> Instant.parse("2017-07-14T02:40:00Z")
2017-07-14T02:40:00Z
Соответственно, можно и наоборот, извлечь из Instant число секунд или миллисекунд, или преобразовать в строку.
>>> val instant = Instant.now()
>>>
>>> instant.getEpochSecond()
1500713246
>>> instant.toEpochMilli()
1500713246133
>>> instant.toString()
2017-07-22T08:47:26.133Z
Эта возможность гонять из чисел и строк туда и обратно очень полезна. Сохраняйте таким образом Instant куда угодно и загружайте обратно, это будет совершенно правильно и безопасно.
Допустим, у нас есть какая-то дата вида «12/07/2017» (тут главное, сразу выяснить, где месяц, а где день месяца). Её можно распарсить с помощью java.time.format.DateTimeFormatter и получить java.time.LocalDate.
>>> import java.time.format.DateTimeFormatter
>>> import java.time.LocalDate
>>>
>>> val dateString = "12/07/2017"
>>> val dateFormat = DateTimeFormatter.ofPattern("dd/MM/yyyy")
>>> val localDate = LocalDate.parse(dateString, dateFormat)
>>> localDate
2017-07-12
LocalDate содержит сведения о годе, месяце и дне (конкретного календаря). Не больше, но и не меньше. Он ничего не знает ни о таймзоне, ни о времени вообще. Этого недостаточно, чтобы определить точку на временной оси. Но вполне достаточно, чтобы обозначить день в календаре.
>>> Instant.from(localDate)
java.time.DateTimeException: Unable to obtain Instant from TemporalAccessor: 2017-07-12 of type java.time.LocalDate
Caused by: java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: InstantSeconds
Допустим, у нас есть какое-то время вида «08:52:17» (тут надо сразу выяснить, что это действительно 24-часовой формат, иначе появляется неоднозначность). Его можно распарсить, снова с помощью DateTimeFormatter, и получить java.time.LocalTime.
>>> import java.time.LocalTime
>>>
>>> val timeString = "08:52:17"
>>> val timeFormat = DateTimeFormatter.ofPattern("HH:mm:ss")
>>> val localTime = LocalTime.parse(timeString, timeFormat)
>>> localTime
08:52:17
LocalTime содержит сведения о часах, минутах, секундах (и наносекундах). Какого-то неопределённого дня. В каком-то неопределённом часовом поясе. Очевидно, этого тоже недостаточно, чтобы определить конкретную точку на временной оси. Но достаточно, например, чтобы поставить будильник, на определённое локальное время, каждый день.
>>> Instant.from(localTime)
java.time.DateTimeException: Unable to obtain Instant from TemporalAccessor: 08:52:17 of type java.time.LocalTime
Caused by: java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: InstantSeconds
DateTimeFormatter похож на java.text.SimpleDateFormat. Да, он поддерживает такой же синтаксис шаблонов. Но его ещё можно собрать с помощью DateTimeFormatterBuilder, накидав нужные поля в нужном порядке. Ещё в нём есть куча предопределённых форматтеров вроде DateTimeFormatter.ISO_INSTANT. А ещё DateTimeFormatter, в отличие от SimpleDateFormat, потокобезопасен (ибо иммутабелен).
Итак, у нас есть LocalDate и LocalTime. Что с ними можно сделать? Можно соединить их вместе, чтобы получить определённое время определённого дня. Это будет java.time.LocalDateTime.
>>> import java.time.LocalDateTime
>>>
>>> val localDateTime = LocalDateTime.of(localDate, localTime)
>>> localDateTime
2017-07-12T08:52:17
Этого всё ещё недостаточно, чтобы получить конкретный момент времени, потому что неизвестно, где именно на планете Земля это событие происходит.
>>> Instant.from(localDateTime)
java.time.DateTimeException: Unable to obtain Instant from TemporalAccessor: 2017-07-12T08:52:17 of type java.time.LocalDateTime
Caused by: java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: InstantSeconds
Нам нужна таймзона.
Допустим, известно, что дело происходит в Мексике, где таймзона, в базе данных IANA, носит название «Mexico/General» (хотя правильнее, вообще-то, «America/Mexico_City»). По названию таймзоны получаем java.time.ZoneId, и, добавив его к LocalDateTime, получаем java.time.ZonedDateTime.
>>> import java.time.ZoneId
>>> import java.time.ZonedDateTime
>>>
>>> val timeZone = ZoneId.of("Mexico/General")
>>> val zonedDateTime = ZonedDateTime.of(localDateTime, timeZone)
>>> zonedDateTime
2017-07-12T08:52:17-05:00[Mexico/General]
Кроме ZoneId, который представляет собой таймзону в конкретной географической области, включая переходы на летнее и зимнее время, а также исторические события типа передвижения границ или смены часовых поясов, можно ещё просто задать смещение в виде java.time.ZoneOffset. Отсюда можно получить либо тот же (но немножко другой) ZonedDateTime, либо java.time.OffsetDateTime.
>>> import java.time.ZoneOffset
>>> import java.time.OffsetDateTime
>>>
>>> val zoneOffset = ZoneOffset.ofHours(-5)
>>> ZonedDateTime.of(localDateTime, zoneOffset)
2017-07-12T08:52:17-05:00
>>> val offsetDateTime = OffsetDateTime.of(localDateTime, zoneOffset)
>>> offsetDateTime
2017-07-12T08:52:17-05:00
И ZonedDateTime, и OffsetDateTime, и Instant представляют конкретную точку на оси времени.
>>> Instant.from(zonedDateTime)
2017-07-12T13:52:17Z
>>> Instant.from(offsetDateTime)
2017-07-12T13:52:17Z
>>>
>>> ZonedDateTime.from(Instant.now())
java.time.DateTimeException: Unable to obtain ZonedDateTime from TemporalAccessor: 2017-07-22T10:49:38.075Z of type java.time.Instant
Caused by: java.time.DateTimeException: Unable to obtain ZoneId from TemporalAccessor: 2017-07-22T10:49:38.075Z of type java.time.Instant
>>> ZonedDateTime.ofInstant(Instant.now(), ZoneId.systemDefault())
2017-07-22T16:51:07.317+06:00[Asia/Omsk]
>>> OffsetDateTime.ofInstant(Instant.now(), ZoneId.systemDefault())
2017-07-22T16:51:49.311+06:00
Разница будет в том, что с ними можно сделать.
Instant — это лишь точка, и больше ничего. Её можно сдвинуть в прошлое или будущее, на секунды или миллисекунды, даже на часы. Но будет ли через два часа уже завтра в конкретном Мехико-сити или Омске, Instant вам не ответит.
>>> instant
2017-07-22T08:47:26.133Z
>>> instant.plusSeconds(4)
2017-07-22T08:47:30.133Z
>>> instant.plusMillis(866)
2017-07-22T08:47:26.999Z
>>>
>>> import java.time.Duration
>>> instant.plus(Duration.ofHours(4))
2017-07-22T12:47:26.133Z
>>> instant + Duration.ofHours(4)
2017-07-22T12:47:26.133Z
OffsetDateTime — это точка, которая знает своё смещение от UTC. Но она ничего не знает о летнем и зимнем времени. Зимой в Мексике зимнее время, UTC-6, но OffsetDateTime будет считать по-прежнему в UTC-5.
>>> offsetDateTime
2017-07-12T08:52:17-05:00
>>>
>>> import java.time.Period
>>> offsetDateTime + Period.ofMonths(6)
2018-01-12T08:52:17-05:00
А вот ZonedDateTime — это точка, которая знает о своём местоположении всё (если, конечно, была создана с ZoneId, если создать с ZoneOffset, то поведение не будет отличаться от OffsetDateTime). Включая то, каким было смещение от UTC в данной местности при царе Горохе. (Кто бы знал, что такое UTC-06:36?)
>>> zonedDateTime
2017-07-12T08:52:17-05:00[Mexico/General]
>>> zonedDateTime + Period.ofMonths(6)
2018-01-12T08:52:17-06:00[Mexico/General]
>>> zonedDateTime - Period.ofYears(100)
1917-07-12T08:52:17-06:36:36[Mexico/General]
Если вам нужно просто обозначить точку на оси времени, используйте Instant. Если вам нужно манипулировать временем по всем правилам календаря, используйте ZonedDateTime с правильным ZoneId.
Когда у нас последний день текущего месяца? Тут, кстати, хватит LocalDate.
>>> LocalDate.now()
2017-07-22
>>> LocalDate.now() + Period.ofMonths(1)
2017-08-22
>>> LocalDate.now().plus(Period.ofMonths(1))
2017-08-22
>>> LocalDate.now().plus(Period.ofMonths(1)).withDayOfMonth(1)
2017-08-01
>>> LocalDate.now().plus(Period.ofMonths(1)).withDayOfMonth(1).minus(Period.ofDays(1))
2017-07-31
На самом деле все эти Instant, LocalDate, LocalTime, LocalDateTime и ZonedDateTime реализуют интерфейс TemporalAccessor. Именно объекты этого интерфейса возвращает DateTimeFormatter.parse() и принимают методы типа Instant.from(). TemporalAccessor позволяет узнать, какие TemporalField (год, месяц, день, часы, минуты и т.п.) имеются в данном объекте и запросить их значения.
>>> import java.time.temporal.ChronoField
>>>
>>> LocalTime.now().isSupported(ChronoField.HOUR_OF_DAY)
true
>>> LocalTime.now().isSupported(ChronoField.DAY_OF_MONTH)
false
>>> LocalTime.now().get(ChronoField.HOUR_OF_DAY)
17
>>> LocalTime.now().get(ChronoField.DAY_OF_MONTH)
java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth
А Duration и Period реализуют интерфейс TemporalAmount. Тут всё просто. Duration умеет дни (24 часа ровно), часы, минуты, секунды, миллисекунды, наносекунды. Period умеет года, месяцы, недели, дни. Они задают количество TemporalUnit, которые можно прибавить или отнять к/от TemporalAccessor. Можно задавать длительность в виде другой половины формата ISO 8601.
>>> LocalTime.now()
17:38:14.342
>>> LocalTime.now() + Duration.parse("PT1H30M")
19:08:17.343
>>> LocalDate.now()
2017-07-22
>>> LocalDate.now() + Period.parse("P1M8D")
2017-08-30
Получается, что TemporalAccessor хранят значения времени, TemporalAmount позволяют сдвигать значения времени. А ещё есть TemporalAdjuster, которые позволяют подкручивать время более хитрыми способами. В простейшем случае можно просто выставить какое-то TemporalField в нужное значение. В более интересных случаях можно поискать тот же конец месяца или следующий понедельник. Для этого используются методы with*.
>>> import java.time.Year
>>>
>>> LocalDate.now()
2017-07-22
>>> LocalDate.now().with(Year.of(2019))
2019-07-22
>>>
>>> import java.time.temporal.TemporalAdjusters
>>>
>>> LocalDate.now().with(TemporalAdjusters.firstDayOfMonth())
2017-07-01
>>> LocalDate.now().with(TemporalAdjusters.lastDayOfMonth())
2017-07-31
>>>
>>> import java.time.DayOfWeek
>>>
>>> LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.MONDAY))
2017-07-24
Всё сложно, но мощно.
Все методы могут кидать исключения. Особенно parse(). Базовый класс исключений тут java.time.DateTimeException. Причём он — наследник java.lang.RuntimeException. А это значит, что компилятор не попросит вас обернуть манипуляции с датой и временем в try-catch. В новейших API в Java явно прослеживается отказ от использования checked exceptions. Будьте внимательны.
А ещё есть java.time.Clock. Он является источником текущего времени (в виде Instant), а также текущей таймзоны и всего такого. Но его можно переопределить, например, для тестов. Например, чтобы время застряло на одной отметке. Если вы спроектируете свои классы так, чтобы текущие настройки и время всегда получались из Clock, то сможете легко тестировать все аспекты поведения, завязанные на время.
>>> import java.time.Clock
>>>
>>> val defaultClock = Clock.systemDefaultZone()
>>> defaultClock.instant()
2017-07-22T13:24:38.378Z
>>> defaultClock.instant()
2017-07-22T13:24:40.727Z
>>> defaultClock.instant()
2017-07-22T13:24:42.055Z
>>>
>>> val fixedClock = Clock.fixed(Instant.now(), ZoneId.systemDefault())
>>> fixedClock.instant()
2017-07-22T13:26:09.282Z
>>> fixedClock.instant()
2017-07-22T13:26:09.282Z
>>> fixedClock.instant()
2017-07-22T13:26:09.282Z
А для особо экзотических случаев у нас есть java.time.chrono.Chronology. Это другие календари. Наслаждайтесь.
>>> import java.time.chrono.IsoChronology
>>> IsoChronology.INSTANCE.dateNow()
2017-07-22
>>> import java.time.chrono.HijrahChronology
>>> HijrahChronology.INSTANCE.dateNow()
Hijrah-umalqura AH 1438-10-28
>>> import java.time.chrono.JapaneseChronology
>>> JapaneseChronology.INSTANCE.dateNow()
Japanese Heisei 29-07-22
>>> import java.time.chrono.MinguoChronology
>>> MinguoChronology.INSTANCE.dateNow()
Minguo ROC 106-07-22
>>> import java.time.chrono.ThaiBuddhistChronology
>>> ThaiBuddhistChronology.INSTANCE.dateNow()
ThaiBuddhist BE 2560-07-22
Обратите внимание, что все хронологии работают только с датой. Видимо, день — это слишком ничтожный промежуток времени, чтобы какой-то другой способ дробления его на части, кроме сложившихся в Европе двадцати четырёх часов, получил бы распространение. IsoChronology — это наш знакомый григорианский календарь.
Kotlin logo
P.S. Примеры в этой статье выполнялись в Kotlin REPL. Именно поэтому метод plus() можно было заменить на оператор +.