• Щели в интерфейсе

    Если вы дизайните интерфейс, помните о следующем правиле: в интерфейсе не должно быть щелей. Всякие панели, сайдбары, палитры должны быть прижаты плотно. Между не должно быть зазора.

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

    Вторая беда щелей в том, что они “сквозят”. Так я называю эффект, когда перематываешь контент, и сквозь щель что-то мелькает. Через три пикселя ты все равно ничего не разглядишь, но мельтешение отвлекает.

    Кажется, в каком-то обновлении Фигмы сделали боковые панели, парящие в воздухе. Это было так плохо, что их вернули на место. Скриншота я не сохранил.

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

    Что в ней плохого? Хотя бы то, что теперь она занимает больше места. Щель снизу бесполезна: ты же не будешь читать сообщение сквозь нее? (Хотя технически это возможно – настолько она большая.) Нормальный человек промотает вверх.

    Какая-то загадка: почему новый интерфейс требует на один-два сантиметра больше? Откуда это? Я не понимаю.

  • Рейтинг пользователей на чистом SQL

    Решил написать небольшой мануальчик по SQL. В нем рассматривается задача, которую я подсмотрел у Кирилла Мокевнина. Возможно, кому-то из вас ее дадут на собеседовании.

    Задача следующая: сделать на сайте систему рейтинга. Пользователям начисляют баллы за разные достижения, и нужно вывести топ-100 пользователей. Позиция в рейтинге должна изменяться мгновенно. Например, кто-то получил 200 баллов и сразу оказался на вершине таблицы. Предполагается, что имеется только реляционная база данных, все остальное – по желанию.

    Эта задача мне очень понравилась. Она лаконичная, но ее можно наращивать очень долго. Удивился тому, в комментариях – а у Кирилла довольно большая аудитория – почти никто не решил ее силами SQL. Большинство предлагали какие-то кэши, редисы-кафки, Firebase и прочую ахинею. Дело не в том, что это плохие инструменты. Наоборот, в нужных местах Редис и Кафка изумительны. Просто в данном случае их выбор ничем не обоснован.

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

    Подготовим таблицу пользователей:

    create table users(
        id serial primary key,
        name text not null
    );
    

    Вставим в нее 100 тысяч случайных людей:

    insert into users
    select n, format('User %s', n)
    from generate_series(1, 100000) as seq(n);
    

    Создадим таблицу для начислений баллов. Она хранит код пользователя, баллы и поле reason – опциональный комментарий за что эти баллы.

    create table user_points(
        user_id integer not null,
        points integer not null dеfault 0,
        reason text
    );
    

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

    insert into user_points(user_id, points, reason)
    select
        n / 10 + 1,
        (random() * n)::int % 100,
        format('iteration %s', n)
    from
        generate_series(1, 1000000) as seq(n);
    

    Объявим материализованную вьюху. Ее запрос выбирает сумму баллов с группировкой по коду пользователя:

    create materialized view mv_user_rating as
    select
        user_id, sum(points) as total_points
    from user_points
    group by user_id;
    

    Обновим ее:

    refresh materialized view mv_user_rating;
    

    Добавим индекс по убыванию общего числа баллов и обновим его:

    create index idx_mv_total_points on mv_user_rating
        using btree (total_points desc);
    
    analyze mv_user_rating;
    

    Теперь к сути: выберем топ-100 записей из вьюхи по убыванию суммы баллов. Подцепим джоином пользователей, выберем их имена. Запрос:

    select
        u.id as user_id,
        u.name as user_name,
        mv.total_points as total_points
    from
        mv_user_rating mv,
        users u
    where
        mv.user_id = u.id
    order by
        mv.total_points desc
    limit
        100;
    

    Частичный результат:

    ┌─────────┬────────────┬────────┐
    │ user_id │ user_name  │ points │
    ├─────────┼────────────┼────────┤
    │      96 │ User 96    │   1225 │
    │      45 │ User 45    │   1185 │
    │      85 │ User 85    │   1169 │
    │      10 │ User 10    │   1140 │
    │      33 │ User 33    │   1138 │
    │      80 │ User 80    │   1135 │
    │      48 │ User 48    │   1133 │
    │      53 │ User 53    │   1121 │
    │      11 │ User 11    │   1119 │
    │      91 │ User 91    │   1099 │
    │      79 │ User 79    │   1096 │
    │      56 │ User 56    │   1090 │
    │      72 │ User 72    │   1089 │
    │      31 │ User 31    │   1088 │
    │      40 │ User 40    │   1072 │
    │      97 │ User 97    │   1070 │
    │      65 │ User 65    │   1068 │
    │      42 │ User 42    │   1058 │
    │      43 │ User 43    │   1057 │
    │      78 │ User 78    │   1054 │
    │      63 │ User 63    │   1053 │
    │      54 │ User 54    │   1049 │
    │      93 │ User 93    │   1031 │
    │      29 │ User 29    │   1029 │
    │      51 │ User 51    │   1028 │
    │     100 │ User 100   │   1027 │
    │      98 │ User 98    │   1021 │
    │      69 │ User 69    │   1014 │
    │      28 │ User 28    │   1003 │
    │      67 │ User 67    │    994 │
    │      60 │ User 60    │    990 │
    │      21 │ User 21    │    987 │
    │      58 │ User 58    │    986 │
    │      26 │ User 26    │    984 │
    

    Будет ли он тормозить? Посмотрим план:

    explain analyze
    select
        u.id as user_id,
        u.name as user_name,
        mv.total_points as total_points
    from
        mv_user_rating mv,
        users u
    where
        mv.user_id = u.id
    order by
        mv.total_points desc
    limit
        100;
    
    ├────────────────────────────────────────
    │ Limit  (cost=0.58..38.88 rows=100 width
    │   ->  Nested Loop  (cost=0.58..38292.36
    │         ->  Index Scan using idx_mv_tot
    │         ->  Index Scan using users_pkey
    │               Index Cond: (id = mv.user
    │ Planning Time: 0.286 ms
    │ Execution Time: 0.368 ms
    └────────────────────────────────────────
    

    Обе таблицы попадают в индексы, стоимость – копейки. Поэтому ответ – не будет.

    Теперь дело за тем, как обновлять вьюху. У материализованных вьюх особенность: во время обновления она недоступна. Можно обновлять их параллельно при помощи refresh materialized view concurrently. Это медленней, зато не блокирует чтение. Попытаемся:

    refresh materialized view concurrently mv_user_rating;
    
    ERROR:  cannot refresh materialized view "public.mv_user_rating" concurrently
    HINT:  Create a unique index with no WHERE clause
           on one or more columns of the materialized view.
    

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

    create unique index idx_uq_mv_user_rating_user_id
        on mv_user_rating(user_id);
    

    Теперь параллельное обновление работает:

    refresh materialized view concurrently mv_user_rating;
    

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

    create extension pg_cron;
    

    Добавьте задачу на расписание:

    SELECT cron.schedule(
        'cron_job_refresh_user_rating',
        'refresh materialized view concurrently mv_user_rating;'
    );
    

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

    Это был прогрев. Напомню, что мы сделали послабление: обновляем рейтинг каждый час, а не мгновенно. Пора это исправить.

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

    create table user_total_points(
        user_id integer primary key,
        total_points integer not null dеfault 0
    );
    

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

    На этом месте говорят “триггеры”, и зря. Триггеры – слишком сложный инструмент. Они слишком строгие, и порой это неудобно, например в разработке, в тестах, в моделировании ситуаций. Ниже мы решим задачу без триггеров.

    Предположим, пользователю 999 начислено 100 баллов. Первый запрос будет таким:

    insert into user_points(user_id, points)
    values (999, 100);
    

    Второй – посложнее. Это UPSERT в таблицу рейтинга, который либо вставляет данные, либо обновляет их:

    insert into user_total_points(user_id, total_points)
      values (999, 100)
      on conflict(user_id)
      do update
      set total_points = user_total_points.total_points
        + excluded.total_points;
    

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

    begin;
    -- query 1;
    -- query 2;
    commit;
    

    Выполним транзакцию два раза. В истории баллов будет две записи по 100, а в таблице рейтинга – их сумма 200.

    select * from user_points;
    ┌─────────┬────────┬────────┐
    │ user_id │ points │ reason │
    ├─────────┼────────┼────────┤
    │     999 │    100 │ <null> │
    │     999 │    100 │ <null> │
    └─────────┴────────┴────────┘
    
    select * from user_total_points;
    ┌─────────┬──────────────┐
    │ user_id │ total_points │
    ├─────────┼──────────────┤
    │     999 │          200 │
    └─────────┴──────────────┘
    

    Транзакцию можно опустить, если переписать запрос на CTE. У таких запросов особенность – все они видят один снимок данных и выполняются атомарно. Это удобно, потому что транзакцию begin/end легко потерять, а в случае CTE это невозможно.

    with
    step_1 as (
        insert into user_points(user_id, points)
        values (999, 100)
    )
    insert into user_total_points(user_id, total_points)
        values (999, 100)
    on conflict(user_id)
        do update
        set total_points = user_total_points.total_points
            + excluded.total_points;
    

    Выполним его три раза, и в таблице рейтинга окажется значение 500:

    ┌─────────┬────────┬────────┐
    │ user_id │ points │ reason │
    ├─────────┼────────┼────────┤
    │     999 │    100 │ <null> │
    │     999 │    100 │ <null> │
    │     999 │    100 │ <null> │
    │     999 │    100 │ <null> │
    │     999 │    100 │ <null> │
    └─────────┴────────┴────────┘
    
    select * from user_total_points;
    
    ┌─────────┬──────────────┐
    │ user_id │ total_points │
    ├─────────┼──────────────┤
    │     999 │          500 │
    └─────────┴──────────────┘
    

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

    create or replace function func_add_points
        (user_id integer, points integer)
    returns integer
    as $$
    with step_1 as (
        insert into user_points(user_id, points)
        values (user_id, points)
    )
    insert into user_total_points(user_id, total_points)
        values (user_id, points)
    on conflict(user_id) do update
        set total_points = user_total_points.total_points
            + excluded.total_points
    returning
        total_points
    $$
    language sql strict parallel safe;
    

    Теперь клиенты вызывают функцию select func_add_points(1234, 500), а что внутри – их не касается. Функцию удобно вызывать из psql, если это баш-скрипт.

    Вот как накинуть баллы – и одновременно изменить рейтинг – некоторым пользователям из диапазона:

    select
        n as user_id,
        func_add_points(n, n * 5)
        as total
    from
        generate_series(25, 50) seq(n);
    
    ┌─────────┬───────┐
    │ user_id │ total │
    ├─────────┼───────┤
    │      25 │   125 │
    │      26 │   130 │
    │      27 │   135 │
    │      28 │   140 │
    │      29 │   145 │
    │      30 │   150 │
    │      31 │   155 │
    │      32 │   160 │
    │      33 │   165 │
    │      34 │   170 │
    │      35 │   175 │
    │      36 │   180 │
    │      37 │   185 │
    │      38 │   190 │
    │      39 │   195 │
    │      40 │   200 │
    │      41 │   205 │
    │      42 │   210 │
    │      43 │   215 │
    │      44 │   220 │
    │      45 │   225 │
    │      46 │   230 │
    │      47 │   235 │
    │      48 │   240 │
    │      49 │   245 │
    │      50 │   250 │
    └─────────┴───────┘
    

    Функцию можно вызывать параллельно. Давайте накинем баллов пользователю 1003 и посмотрим на результат:

    select
        user_id,
        points,
        func_add_points(user_id, points)
        as total
    from (values
        (1003, 3),
        (1003, 2),
        (1003, 1),
        (1003, 5),
        (1003, 7))
        as vals(user_id, points);
    
    ┌─────────┬────────┬────────┐
    │ user_id │ points │ total  │
    ├─────────┼────────┼────────┤
    │    1003 │      3 │      3 │
    │    1003 │      2 │      5 │
    │    1003 │      1 │      6 │
    │    1003 │      5 │     11 │
    │    1003 │      7 │     18 │
    └─────────┴────────┴────────┘
    

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

    Теперь делаем то же самое, что и со вьюхой. Добавим индекс по убыванию баллов:

    create index idx_total_points on user_total_points
        using btree (total_points desc);
    

    Выберем из таблицы рейтинга топ-100 записей, приклеим пользователей и готово:

    select
        u.id as user_id,
        u.name as user_name,
        total.total_points as total_points
    from
        user_total_points total,
        users u
    where
        total.user_id = u.id
    order by
        total.total_points desc
    limit
        100;
    
    ┌─────────┬───────────┬───────┐
    │ user_id │ user_name │ total │
    ├─────────┼───────────┼───────┤
    │     999 │ User 999  │  1500 │
    │      50 │ User 50   │   250 │
    │      49 │ User 49   │   245 │
    │      48 │ User 48   │   240 │
    │      47 │ User 47   │   235 │
    │      46 │ User 46   │   230 │
    │      45 │ User 45   │   225 │
    │      44 │ User 44   │   220 │
    │      43 │ User 43   │   215 │
    │      42 │ User 42   │   210 │
    │      41 │ User 41   │   205 │
    │      40 │ User 40   │   200 │
    │      39 │ User 39   │   195 │
    │      38 │ User 38   │   190 │
    │      37 │ User 37   │   185 │
    │      36 │ User 36   │   180 │
    │      35 │ User 35   │   175 │
    │      34 │ User 34   │   170 │
    │      33 │ User 33   │   165 │
    │      32 │ User 32   │   160 │
    │      31 │ User 31   │   155 │
    │      30 │ User 30   │   150 │
    │      29 │ User 29   │   145 │
    │      28 │ User 28   │   140 │
    │      27 │ User 27   │   135 │
    │      26 │ User 26   │   130 │
    │      25 │ User 25   │   125 │
    └─────────┴───────────┴───────┘
    

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

    select func_add_points(49, 5);
    
    ┌─────────────────┐
    │ func_add_points │
    ├─────────────────┤
    │             250 │
    └─────────────────┘
    

    Их место будет неоднозначно: мы сортируем по убыванию суммы баллов, но если значения равны, порядок записей не гарантируется. Чтобы участники не спорили, кто на втором, а кто на третьем месте, вычислим плотный ранг. Это оконная функция, которая учитывает поля с одинаковым значением. Обычный ранг нумерует записи подряд, а плотный – с учетом повторов. Запрос:

    select
        u.id as user_id,
        u.name as user_name,
        total.total_points as total_points,
        dense_rank() over w as rank
    from
        user_total_points total,
        users u
    where
        total.user_id = u.id
    window
        w as (order by total_points desc)
    order by
        total_points desc
    limit
        100;
    

    Результат:

    ┌─────────┬───────────┬───────┬──────┐
    │ user_id │ user_name │ total │ rank │
    ├─────────┼───────────┼───────┼──────┤
    │     999 │ User 999  │  1500 │    1 │
    │      50 │ User 50   │   250 │    2 │
    │      49 │ User 49   │   250 │    2 │
    │      48 │ User 48   │   240 │    3 │
    │      47 │ User 47   │   235 │    4 │
    │      46 │ User 46   │   230 │    5 │
    │      45 │ User 45   │   225 │    6 │
    │      44 │ User 44   │   220 │    7 │
    │      43 │ User 43   │   215 │    8 │
    │      42 │ User 42   │   210 │    9 │
    │      41 │ User 41   │   205 │   10 │
    │      40 │ User 40   │   200 │   11 │
    │      39 │ User 39   │   195 │   12 │
    │      38 │ User 38   │   190 │   13 │
    │      37 │ User 37   │   185 │   14 │
    │      36 │ User 36   │   180 │   15 │
    │      35 │ User 35   │   175 │   16 │
    │      34 │ User 34   │   170 │   17 │
    

    Теперь участники 49 и 50 делят второе место, и неоднозначность ушла.

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

    Любите Постгрес! Учите Пострес! Всем любви и добра.

    Весь исходный код

  • На чем писать

    Существует расхожее мнение о том, что на чем писать. Так, почему-то многие уверены, что банк должен быть написан на Джаве. Потому что банк должен быть надежным, а Джава – надежная. При этом в чем именно надежность, не уточняется.

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

    Я работаю в крупном финтехе. Таком большом, что подобно муравью, вижу лишь крохотную его часть. При этом у нас Кложа, все динамическое – и работает. Не припомню, когда в последний раз я ожидал число, а пришла строка. Может, и было такое пару лет назад, это не важно. Гораздо больше других проблем: потоки данных и зависимости сервисов. Что нужно кому, кто откуда что получает и куда складывает. В какое время данные появляются и до какого момента нужно записать новые, чтобы другие службы подхватили.

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

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

    Скажем, Postgres написан на Си, и это одна их стабильнейших вещей на свете. А недавно чудаки в Cloudflare написали конфиг-парсер на Расте и уронили половину интернета. Вот она, ваша пресловутая надежность.

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

  • Впечатления от читалки Kindle десять лет спустя

    Почти десять лет назад я купил читалку Kindle Paperwhite. О ней я писал первую заметку, через два года — вторую.

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

    Интересно взглянуть на читалку современным взглядом. Первое, что бросается в глаза — устройство крайне молчаливо. Оно просто работает! Закинул — прочил, закинул — прочитал. Никаких нотификаций, попапов, выпадашек. Читалка ничего не спрашивает — никогда и ни при каких условиях.

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

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

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

    Устройство удобно в плане коммуникации. Воткнул в комп и видишь файловую систему. Но еще удобней слать статьи на почту: отправил письмо с документами на username@kindle.com — и через минуту они на устройстве.

    Для отправки веб-страниц служит расширение Push to Kindle (сервис Five Filters). Оно очень качественное и выдает красивую верстку. Формально действует ограничение: не более 10 статей в месяц, но при переходе в анонимный режим счетчик сбрасывается.

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

    По сегодняшним меркам Kindle Paperwhite — это устройство из другого мира. Оно слишком качественное, простое, функциональное. Пользуешься — и буквально чувствуешь его простоту и бесхитростность. В его дизайне на первое место ставили интерес пользователя — бывает же!

    Изумительная вещь.

  • Как все успевать

    Иногда попадаются статьи с заголовком “Как все успевать?”. Все они сводятся к тому, что некий гуру утверждает: я успешен, я все успеваю, делайте так-то и так-то. А еще зайдите на мой канал, бусти, купите книгу и подпишитесь на рассылку.

    Я понял вот что: успевать все невозможно. Однако можно успевать хоть что-то. Если это соблюдать – постоянно успевать хоть что-то – получается вполне продуктивно.

    Вопрос, как успевать хоть что-то? Дело здесь не в приложении или технике GDT. Может быть, кому-то помогает то и другое, но я уверен, что они – туфта. Это технические средства, которые не должны затенять главное.

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

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

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

    Иногда можно устроить “выходной” – день без дополнительных дел. Знаете, какой самый легкий день для меня? Когда я прихожу в офис и просто работаю. Не пишу книгу, не контрибучу опен-сорс, не ищу маме ноты, не покупаю родственникам билеты, не записываюсь в поликлинику и так далее. Просто поработать – уже праздник!

    Не старайтесь законичить все дела. Список дел – это и есть жизнь. Берите из него понемножку и делайте, не уставая. И тогда, пусть даже вы ничего не успеваете, вас спросят: как ты все успеваешь?

  • Git 3

    Поздравляю читателей со скорым релизом Git 3, который по умолчанию использует ветку main. Требуется действие: откройте глобальный файл ~/.gitconfig и впишите следующее:

    [init]
     dеfaultBranch = master
    

    Все, вы в безопасности.

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

    git branch -m main master
    git fetch origin
    git branch -u origin/master master
    git remote set-head origin -a
    

    Разумеется, ребятам, что все это начали, не пришла в голову очевидная мысль. Должна быть одна команда (или хотя бы алиас) на случай переименования. Одна – а не блок из четырех.

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

    Шлю лучи добра авторам библиотеки JSoup. Обновил ее – проект не собирается. Оказалось, это клоуны переименовали класс Whitelist в Allowlist, и ты вынужден исправлять. Если в других проектах указана старая версия, обновить и там. Насыпали работы на ровном месте.

    Вспоминается история с Микрософтом. Они выкатили в опен-сорс какой-то проект, и люди заметили: все “обидные” слова заменены звездочками. Тупая автозамена по словарю: фразы race condition или red-black tree написаны как **** condition и red-***** tree. Как бы чего не вышло…

    Термины – вообще спорная вещь. Скажем, по всему миру люди пользуются Гитом, не зная, что на британском сленге git означает “ушлепок” (если не уебок, простите). Линус Торвальдс был молод, дерзок и с лексикой не церемонился. И ничего, название никому не мешает. Зато master/slave и white/black, видите ли, не подходят.

    Мне очевидно следующее: не всегда ошибки нуждаются в исправлении. Какие-то из них можно оставить в качестве музейного экспоната. Делайте форки, пишите свои системы контроля версий. Пусть вместо master/slave у вас будут pony/rainbow. Но не наваливайте мне работы просто ради убеждений – которых, к тому же, я не разделяю.

    И так проблем хватает.

  • Сны-фильмы (2)

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

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

    Итак, сюжет. Молодой человек Артем приезжает из Питера в Москву на какую-то конференцию. Там он знакомится с красивой девушкой Еленой. Артем гостит в Москве несколько дней, за это время они встречаются и влюбляются. Поскольку они – подростки, то интересуются бывшими отношениями друг друга. У Артема не было серьезных отношений; Елена говорит, что у нее был ухажер, но потом он ее бросил. Артем не понимает, как можно бросить такую девушку. Еще Елена говорит, что в ее окружении есть некий Филипп, который добивается ее руки, но он ей не нравится.

    В день отъезда Елена приглашает Артема в свою компанию. Там он замечает Филиппа – он красив, но держится замкнуто. Артем пытается с ним поговорить, но Филипп грубит. Артем понимает – ревность. Ему пора на вокзал. Елена хочет его проводить, но Артем уговаривает ее остаться на вечеринке. Они страстно прощаются, и Артем отправляется на вокзал.

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

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

    Утром Елена присылает ему фотографии с вечеринки, и на многих он видит Филиппа. Выходит, он был в двух местах одновременно – и на вечеринке, и вместе с тем преследовал Артема по всей Москве. Артем звонит Елене и между делом выясняет, что Филипп никуда не уходил. О происшествии с полицией он умалчивает.

    Идут дни, Артем и Елена созваниваются. Под разными предлогами Артем вытягивает из Елены информацию о Филиппе. Он странный, отличается дурацкими привычками, неуживчив. Его пытались травить, но со всеми, кто это делал, случались неприятности. Окружающие уловили эту связь и отстали от него, но вместе с тем и не приняли. Компания Лены – исключение, потому что ей его жалко. В конце разговора Елена говорит, что очень ждет Артема в Москве.

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

    Вместе они разрабатывают план: Елена скажет друзьям, что поехала к родственникам, а на самом деле сбежит к Артему в Питер. Он приезжает за ней, и вместе они следуют на вокзал. Однако по пути Артем замечает Филиппа. Пара садится в ночной поезд, почти пустой.

    В поездке Елена засыпает. Артем идет по вагонам, чтобы проверить, нет ли Филиппа рядом. Затем он понимает ошибку: нельзя оставлять Елену одну. Он разворачивается, но сзади ждет Филипп. Он рассказывает свою историю: с малых лет он был странным, мало спал, остро чувствовал чужие эмоции. Со временем он развил паранормальную способность: мог произвести свою призрачную копию, которая подчинялась его воле. Но он так и не придумал, как использовать ее во благо, а кроме того, боялся ее и не мог находиться с ней рядом. От своей ненормальности он устал и был несчастен, ощущая себя чужим. Своим спасением он считал Елену, добиваясь ее любви. От ее прошлого ухажера он избавился похожим образом: подкараулил в поезде, запугал и заставил выпрыгнуть из окна.

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

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

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

    Вот такой сон. Увижу еще – напишу.

  • Твиты DDH

    Недавно человек с ником DHH (или DDH, каждый раз путаюсь) твитнул, что микросервисы — скам. Видимо, он знаменитость, потому что эту новость обсуждали всем интернетом. Фанатов DDH несколько огорчило то, что во втором твите он признался: первый написал за него чат-гпт. Странный подход к работе с аудиторией: обычно люди читают человека, потому что верят ему. Писать при помощи нейросетей, а потом вскрывать этот факт — немного странно.

    Однако важно другое. Каждый раз меня удивляет факт: почему, чтобы осознать простую вещь, нужен твит знаменитости? Разве у людей нет головы на плечах? Если бы речь шла о голове! Неужели не хватает банальных ощущений и восприятия? Когда сел на кактус и больно — разве мы думаем, что делать? Но с микросервисами история противоположна. Людям неудобно — но они глубже садятся задницей на кактус. Обещают себе и другим, что еще немного — и станет легче. Нет, не станет. Ни через три месяца, ни через три года.

    Без всяких ГПТ я расскажу, что такое микросервисы на практике. И прежде всего: желаю их фанатикам попасть в проект, где микросервисов не три, а тридцать. Бойтесь своих желаний, уважаемые, потому что вы заговорите по-иному. Одно дело — рисовать круги и стрелки на маркерной доске, другое — отлаживать цепочку из пяти вызовов.

    Я варюсь в подобном аду уже два с половиной года и накопил негативный опыт. Он тоже важен: поможет не вестись на провокации, когда кто-то заливает про микросоверсы, Гугл и так далее.

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

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

    Передача данных по сети — целая вечность по сравнению с передачей указателя. Сериализация данных — тоже нетривиальная задача, где сплошные компромиссы. JSON читается человеком, но избыточен и поддерживает мало типов. Protobuf — быстр, но порог входа высокий, нужен обвес. gRPS быстрее обычного REST API, но это фреймворк с высоким порогом. К тому же REST API так или иначе остается для браузера.

    Если делать микросервисы по уму, то не хватит никакого воображения, чтобы охватить масштаб. Судите сами: во-первых, одних лишь сервисов недостаточно, им нужен API-гейтвей — согласно бест-практис. Этот гейтвей отвечает за глобальный роутинг, лимиты и многое другое.

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

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

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

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

    Сервисы могут быть написаны очень плохо. Я уже год борюсь с тем, что важный сервис проглатывает ошибки. Возвращает пустой результат {"data": []} и пишет в лог. Не читаешь логи? Твои проблемы.

    Владение сервисом может быть как благом, так и проклятьем. У него может быть неудобная апишка, но владельцев это не интересует. Заведи задачу — может быть, через три месяца возьмем в работу. А пока что так, у нас другие дела.

    Я смотрю на это и думаю: похоже, все эти идеальные микросервисы бывают только в раю. Столько требований, столько ограничений, столько бест-практис, что кажется — нужно быть богом или супер-мозгом, чтобы организовать все правильно. При этом — в разумное время и бюджет(!), и еще параллельно делать бизнес-фичи(!!).

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

    Еще ни разу я не видел задачи, которая бы не решалась монолитом и Постгресом. Ни разу — без преувеличений. Только не рассказывайте про Гугл. Во-первых, это другая весовая категория, а во-вторых — в том же OpenAI один мастер-инстанс Postgres. Вполне можно выйти на уровень Faang за счет простых технологий.

  • Вайб-кодинг и новички

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

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

    Ресурсы вроде StackOverflow для того и созданы, чтобы люди общались и передавали знания. Кроме SO, сейчас полно тематических Слак, Дискордов, каналов в Телеграме. Заходи, спрашивай.

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

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

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

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

  • Обсуждение с AI

    В разговорах про AI люди говорят: обсуждаю с ним задачу. Я даже недавно спрашивал, как вы программируете с AI, и большинство ответили – обсуждаю. Именно это мне непонятно – что именно обсуждают с моделью?

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

    Все это записывается на бумажку, а потом каждый пункт детализируется. Таблицы с такими-то полями в такой-то схеме. Апишки принимают то и возвращают это. Тестировать так-то. Задеплоить туда-то.

    Когда более-менее ясно, созваниваешься с тем, кто принимает работу. Так пойдет? Пойдет, только добавь это и вот это. Окей. Далее пишешь миграции, добавляешь апишки, тесты и так далее.

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

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

    Как-то посмотрел два видоса про кодинг с моделью. Если честно, ничего не понял. Человек в VS Code постоянно трындит, и я терялся, к кому он обращается: к зрителям, к гостю или к модели. Без конца куда-то кликает, переключает буферы, словом – хаос. Я даже не понял, какую задачу он ставил, не говоря о том, достиг ли ее.

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

Страница 1 из 104