php

Модуль PHP на Go

Потешную штуку показали на днях — php-go, позволяет писать модули для ПХП на Гоу. У меня как-то такой потребности не возникало, но попробовать-то интересно!

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

Ссылка давно протухла, но «Вебархив», к счастью, имеет копию страницы — статья называется «Ridiculous UTF-8 character counting».

Программа там на Си, я её минимально подпилил, чтобы она нормально компилировалась, завернул через cgo в Гоу, а оттуда при помощи php-go протащил как модуль ПХП.

Скриншот терминала с результатами тестирования производительности функций strlen: sse2_strlen (13.23), mb_strlen (4.74), iconv_strlen (252.72), strlen (0.15).

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

Зато попробовал php-go, вещь предельно простая, в случае нужды можно будет что-то быстренько набросать с её использованием.

5 комментариев

Функция sleep и сигналы

Век живи, век учись. Вот уж не знал, что при получении сигнала программой на ПХП, моментально прерывается выполнение функции sleep (и родственных ей time_nanosleep и usleep):

$ php -r 'pcntl_signal(SIGUSR1, "_"); var_dump(sleep(PHP_INT_MAX));' &
[1] 3177
$ kill -USR1 3177
$ int(4294967288)

Вообще в Си так же, но где ПХП и где Си. Как и в Си, назад возвращается количество секунд, которые функция не «доспала» (time_nanosleep возвращает массив из секунд и наносекунд, а usleep — NULL). Правда под Виндой есть особенность — sleep при «недосыпе» всегда возвращает 192 (значение константы WAIT_IO_COMPLETION).

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

function bulletproof_sleep(int $seconds): bool
{
    $period = ['seconds' => $seconds, 'nanoseconds' => 0];

    while (array_sum($period) > 0) {
        $period = time_nanosleep($period['seconds'], $period['nanoseconds']);

        if (is_bool($period)) {
            return $period;
        }
    }

    return true;
}

4 комментария

Heredoc и nowdoc в PHP

Не очень-то люблю использовать heredoc и nowdoc в ПХП из-за того, что они портят форматирование (а оно важно в больших проектах). Хотя вещь удобная — внутри можно использовать оба вида кавычек без экранирования, это бывает актуально в эскуэль-запросах.

В Перле давным-давно можно нормально смещать эту конструкцию вправо, она сама обрежет лишние пробелы, а теперь и в ПХП сделали то же — с версии 7.3 можно будет делать вот так:

$values = [<<<END
      a
     b
    c
    END];

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

Оставить комментарий

Негативное кеширование NFS

В реализациях сетевой файловой системы NFS есть так называемое «негативное кеширование». Эта штука портит много крови программистам, а суть её проста, как чихание — если вы обратились за файлом и его на момент обращения не было, то какое-то время этот результат будет закеширован.

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

Теоретически этот кеш можно отключить (опцией lookupcache), практически же не всегда на оборудовании заказчика удаётся это сделать.

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

function clear_nfs_cache(string $filename): bool
{
    $tmpname = tempnam(dirname($filename), 'clear-cache'));
    return $tmpname === false ? false : unlink($tmpname);
}

3 комментария

Баг в PHP 7.2 с output_add_rewrite_var

Для того, чтобы не подставлять в каждую ссылку и форму глобальные для проекта параметры запроса (идентификатор сессии, например), в ПХП используется техника, называемая Url Rewriting. У нас в проекте так передаётся токен против XSRF — благо есть возможность указать собственные параметры, которые надо передать и даже задать список тегов с атрибутами в которых это будет работать.

И при переходе на ПХП 7.2 словили неприятный баг, который уже поправлен в 7.1.9, но ещё почему-то не влит дальше: если урл не содержит ничего, кроме якоря, то параметры добавляются неправильно. Код для повторения бага такой:

<?php
output_add_rewrite_var('foo', 'bar');
?>
<a href="index.php">This is link</a>
<a href="#place">This is anchor</a>

В первую ссылку «foo=bar» добавится нормально, а во второй это будет не к месту, якорь будет испорчен:

<a href="index.php?foo=bar">This is link</a>
<a href="#place/?foo=bar">This is anchor</a>

Наверняка со стандартными сессиями ПХП, передаваемыми в урле, будет тот же результат, но я не пробовал.

Либо надо ждать обновления, либо поставлять вместо якоря текущий полный урл.

Оставить комментарий

PHP8 ★

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

Пока всё прогрессивное человечество переходит на ПХП7, а менее прогрессивное до сих пор чахнет над каким-нибудь 5.4, я решил разведать что там обещается в восьмой версии и когда её обещают.

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

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

Или вот похожий пример с константой:

class Sample
{
    const test = 'const';
    public static $test = 'variable';
}

$test = 'test';
// что ты такое?
var_dump(Sample::$test);
// на самом деле вернётся «variable», очевидно; константу придётся получить так:
var_dump(constant("Sample::$test"));

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

// так можно было бы получать константу по имени в переменной (не работает!)
var_dump(Sample::($text));

Самым серьёзным внутренним изменением нового релиза, вероятно будет добавление JIT, Дмитрий Стогов, человек, которому мы обязаны фантастическим ускорением в «семёрке», ещё в позапрошлом году написал в рассылку php.internals, что работа над динамической компиляцией начата. Получившийся к тому времени прототип показал 20% прироста на синтетических тестах и снижение производительности на реальных приложениях.

Кстати, это не первая попытка совместить динамическую компиляцию и ПХП, в языке HHVM, диалекте ПХП от «Фейсбука», такая компиляция даёт хороший прирост в скорости. Да и Дмитрий сотоварищи ещё в 14-м году делали попытку прикрутить JIT тогда ещё пятой версии ПХП. Тогда ускорение составило всего около пары процентов и эта неудача заставила Дмитрия исследовать интерпретатор на предмет «бутылочных горлышек», который и были устранены в седьмой версии.

Столбчатая диаграмма «Скорость выполнения» сравнивает версии 7.0.13 (синий), 7.1.0 (оранжевый) и JIT (серый): значения 0,670/0,495/0,208; 4,056/3,327/2,185; 15,562/9,228/4,680.
Результат сравнения разных версий, JIT убедительно выигрывает на фоне PHP7 и PHP7.1 на синтетических тестах

Более свежий тест того, что будет в ПХП8 показывает, что на трёх тестах (два стандартных зендовских теста и построение множества Мандельброта) динамическая компиляция уделывает версию 7.1.0 примерно вдвое. Это отличный результат! Но понятно, что в реальной жизни всё будет скромнее.

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

Оставить комментарий

Переезд на PHP7

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

Если кому интересно, загруженность серверов, где крутится ПХП, действительно упала, примерно в 1,5—1,7 раза.

Вообще говоря, у ПХП есть довольно детальные описания того, что может пойти не так при смене версий, достаточно заглянуть в раздел «Изменения, ломающие обратную совместимость» нужной версии. Там можно сразу прикинуть что придётся поменять в коде, если вы хорошо его знаете.

Ниже мой собственный опыт.

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

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

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

find . -type f \( -name '*.php' -o -name '*.inc' \) -exec php -l {} \; |
fgrep -v 'No syntax errors'

У нас, к слову, было несколько таких мест — например, был объявлен класс с разными полезными штуками для обработки строк, который назывался String. Так больше нельзя — зарезервированное слово.

Посложнее с обработкой исключений — класс Exception перестал быть базовым и если требуется ловить все исключения, то в «семёрке» лучше всего перехватывать всё, что реализует интерфейс Throwable.

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

try {
    // … какой-то код
} catch (Exception $e) {
   DI::log->get('exception')->error($e);
   throw $e;
}

Из-за пространства имён такие места простым поиском найти непросто, но специализированные редакторы неплохо справляются.

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

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

Далее в дело вступили тестеры и программисты — в основном были исправления, связанные с тем, что в «семёрки» некоторые вещи устарели, ничего сверх этого я не припомню.

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

2 комментария

Две раздражающие частности в PHP

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

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

Первый относится к функции exec. Свой второй параметр она получает по ссылке и выводит туда массив строк, которые выдаёт запускаемая команда. Беда возникает в следующем коде:

exec('1st command', $out, $ret);
// … проверяем $ret, делаем что-то полезное с $out
exec('2nd command', $out, $ret);
// … проверяем $ret, делаем что-то полезное с $out

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

Такое поведение описано в руководстве и возможно это полезно для цикла с накоплением, но в общем случае контринтуитивно.

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

var_dump(array_unique([1,1,2]));
/* array(2) {
  [0]=>
  int(1)
  [2]=>
  int(2)
} */

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

Самый замысловатый баг, который я помню был таким. Результирующий массив превращался в джейсон-строку и передавался микросервису на Гоу. Чаще всего значения были уникальными и всё работало как ожидается.

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

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

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

24 комментария

Правда выигрывает

Скриншот задачи «return true to win» с PHP-функцией foo($x), возвращающей true, если $x('gehr') === 'true', и полем ввода для вызова foo() с текущим счётом 9.

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

Мне пока не удалось победить уровнь №6 (30 символов против 22) и 10 (29 против 27), не хватило свободного времени на работе, чтобы додумать, попробую в выходные.

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

10 комментариев

Особенность PHP 7.2 (и 7.1)

Потихоньку смотрю как наш продукт запускается на ПХП версии 7.2 — у нас сейчас используется 7.0, но очень хочется двинуться дальше. В хитросплетениях кода нашёл очень странный баг интерпретатора, который был разбросан по разным строчкам кода, а в сконцентрированном виде он выглядит так:

$arr = [[1]];
array_walk($arr, function(){});
array_map('array_shift', $arr);
var_dump($arr);

В ПХП 7.0 массив выведется в неизменном виде, а в версии 7.2 (и 7.1, как оказалось) единица исчезнет. Очевидно, что array_walk создаёт какие-то ссылки внутри массива, из-за чего array_shift начинает получать внутренний массив по ссылке и сдвигать. Но никаким другим способом (например, прямым созданием массива со ссылками) мне такое поведение получить не удаётся.

5 комментариев

DevelNext

Понадобилось тут по работе быстро накидать графическую утилиту под Виндоуз, пока шли совещания погуглил инструменты и случайно наткнулся на «ДевелНекст» — развитие «ДевелСтудио», о которой я писал восемь лет назад.

Скриншот IDE DevelNext с окном приложения Pinger, показывающим логи проверки домена (MAK, Хост, ОС) и код PHP для обработки HTTP-ответа сервера.

Внутри у неё свой диалект ПХП (JPHP), написанный на Джаве. Отличия от обычного интерпретатора, в основном, в стандартных функциях — они присутствуют не все, но чаще всего есть какие-то аналоги. Актуальная на текущий момент версия поддерживает синтаксис ПХП 7.1.

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

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

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

Причём как оказалось, работать надо не просто в отдельном треде, так ещё и изменения интерфейса делать разрешается только через специальную обёртку — функции uiLater и uiLaterAndWait. Пример из документации:

$this->label->text = 'Поток выполняется...';

$thread = new Thread(function () {
    sleep(3); // ждем 3 сек.

    uiLater(function() {
        $this->label->text = 'Поток выполнен.';
    });
});

$thread->start();

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

Диалоговое окно с сообщением: «This application requires a Java Runtime Environment 1.8.0» и кнопкой «OK».

К сожалению, сразу нашёлся баг — если на компьютере пользователя установлена Джава младше версии 1.8, программа запускаться отказывается. Автор «ДевелНекста» вызвался помочь исследовать эту проблему, надеюсь скоро исправит.

Разобрался: ларчик просто открывался! Я так был уверен, что должен получиться всего один файл, что не понял, что папку jre (где и лежит Джава) надо тоже копировать. Это уже менее интересно, конечно.

8 комментариев

Дизассемблируй это ★

Скриншот игры: вид от первого лица в тёмном подземелье с синим полом и красным объектом; внизу интерфейс с здоровьем 100%, бронёй 0%, патронами 50 и портретом персонажа.
«ДУМ», скомпилированный с использованием одних только команд MOV

В моей жизни последовательно сменяли друг друга три ассемблера. Первый, для компьютера «Радио-86РК», я выучил по комментариям к ассемблерному коду в журнале «Радио» — другой литературы не было, а до моего знакомства с интернетом оставалось несколько лет. Потом были последовательно ассемблеры для «Спектрума» и интеловских процессоров.

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

В ассемблере есть такая команда — MOV (в некоторых ассемблерах — LD), записывает содержимое одного аргумента в другой. Сейчас набор комманд разросся, аргументом может быть почти что угодно — регистр, ячейка в памяти, сумма некоторого числа, одного регистра и другого, умноженного на число, но по сути это всегда присваивание.

И вот оказалось, что эта команда — полная по Тьюрингу. Звучит невероятно, но это так. Некие ребята заморочились и сделали компилятор, который компилирует любую программу на Си в последовательность команд MOV. Причём им даже ДУМ удалось скомпилировать, правда один кадр рисуется семь часов. Кстати, такая программа неуязвима для горюшка века — Мелтдауна и Спектра.

Есть небольшая (на 156 страниц и 90% воды) презентация, достаточно популярно объясняющая как этого удалось достичь, но для её чтения надо знать ассемблер, поэтому я позволю себе раскрыть детали трансляции двух инструкций, чтобы пояснить принцип для тех, кто ассемблера не знает или ленится причитать.

Например, сравнение двух чисел делается при помощи следующего псеводокода:

mov [X], 0
mov [Y], 1
mov R, [X]

У нас есть два числа в аргументах «X» и «Y», результат сравнения которых попадает в «R» — там будет ноль, если числа не равны и единица в противном случае. Как же это работает?

Первой командой ноль записывается в ячейку по адресу «X». Это ассемблер, у нас тут всё — число, остальное — человеческие интерператации, поэтому записанное в «X» мы используем как адрес. Второй командой единица записывается в ячейку по адресу «Y». Третьей командой мы читаем значение по адресу «X» и если значения в «X» и «Y» совпадают, то ноль перетрётся единицей (и она попадёт в R), если нет, то в ячейке по адресу «X» ноль останется (который попадёт в R).

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

Возьмём, например, логическое «ИЛИ» («OR»), тут чуточку сложнее:

OR_ADDRS: dd OR_0, OR_1
OR_0: dd 0, 1
OR_1: dd 1, 1
; …
mov eax, X
mov edx, [OR_ADDRS + eax]
mov eax, Y
mov eax, [eax + edx]
mov R, eax

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

Что тут происходит? В регистр (переменную, с которыми работает процессор) «eax» записывается значение «X» (возможные входные значения у нас тут — ноль или единица, численное представление булевых значений).

Далее в регистр «edx» записывается число из адреса, который является суммой адреса массива OR_ADDRS и содержимого регистра eax. Таким образом в eax попадёт OR_0 или OR_1, в зависимости от того былы записаны в eax ноль или единица. Эти значения — тоже числа и являются адресами двух других массивов из двух элементов.

Далее в eax мы записываем аргумент Y, его значение складывается с адресом полученным на предыдущем шаге и из получившегося адреса мы читаем записанное там значение. В переводе на ПХП получается следующее:

function mov_or(int $X, int $Y): int
{
    define('OR_0', [0, 1]);
    define('OR_1', [1, 1]);

    define('OR_ADDRS', [OR_0, OR_1]);

    $R = OR_ADDRS[$X][$Y];

    return $R;
}

Кстати, интересно, что у знаменитого дисассемблера «ИДА» от полученной таким образом программы крепко уносит крышу — при попытке отладки диссасемблер не видит никаких ветвлений и падает на анализе кода. Получился бы неплохой метод защиты от анализа, если бы не производительность.

5 комментариев

Memcached и persistent connections

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

Упомянутый модуль имеет малозаметную возможность, которая помогает экономии ресурсов в проектах с большой нагрузкой — если в конструктор передать строку, то она станет идентификатором для так называемого «персистент коннекта» — соединения, которое открывается процессом интерпретатора ПХП и живёт между запуском обрабатываемым этим процессом программ.

Оказалось у этого функционала есть особенность:

socket(PF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_TCP) = 4

connect(4, {sa_family=AF_INET, sin_port=htons(11211), sin_addr=inet_addr("127.0.0.1")}, 16) = -1

poll([{fd=4, events=POLLOUT}], 1, 4000) = 1 ([{fd=4, revents=POLLOUT}])
getsockopt(4, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
sendto(4, "version\r\n", 9, MSG_NOSIGNAL, NULL, 0) = 9
recvfrom(4, "VERSION 1.5.1\r\n", 8196, MSG_NOSIGNAL, NULL, NULL) = 15
sendto(4, "quit\r\n", 6, MSG_NOSIGNAL, NULL, 0) = 6
shutdown(4, SHUT_WR)                    = 0
shutdown(4, SHUT_RD)                    = -1 ENOTCONN (Transport endpoint is not connected)
close(4)                                = 0

socket(PF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_TCP) = 4
setsockopt(4, SOL_SOCKET, SO_LINGER, {onoff=1, linger=0}, 8) = 0

connect(4, {sa_family=AF_INET, sin_port=htons(11211), sin_addr=inet_addr("127.0.0.1")}, 16) = -1

poll([{fd=4, events=POLLIN|POLLOUT}], 1, 4000) = 1 ([{fd=4, revents=POLLOUT}])
getsockopt(4, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
sendto(4, "version\r\n", 9, MSG_NOSIGNAL, NULL, 0) = 9
recvfrom(4, "VERSION 1.5.1\r\n", 8196, MSG_NOSIGNAL, NULL, NULL) = 15

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

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

Решение — любым доступным способ проверять получили ли мы «свежий» объект или имеем дело с уже открытым соединением и выставлять опции только в первом случае:

$mc = new Memcached('persistent');

if (!$mc->getServerList()) {
    $mc->setOptions([
        Memcached::OPT_NO_BLOCK => true,
    ]);

    $mc->addServer('127.0.0.1', 11211);
}

Тогда соединение благополучно переиспользуется.

9 комментариев

ПХП и строгая типизация

В ПХП много странностей, ещё одна дала о себе знать в неожиданном месте. Сначала немного теории.

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

ПХП использует эту схему везде, кроме расширений и встроенных функций. Понятие «необязательный параметр» там есть, но обрабатывается иначе — у параметра указывается тип (например «строка»), необязательность и «нулабельность» (можно ли в этом параметре принимать null в качестве значения).

Последнее очень полезно для числовых и булевых типов — если «нулабельность» не указана, то null будет преобразован по правилам языка в значение указанного типа.

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

Например, у нас преспокойно работал примерно такой код:

public function put(Serialized $object, $eventName, $extraEventData, $uniqueId = null)
    {
        return DI::gearman_client()->doBackground(
            $this->queueName,
            igbinary_serialize(
                [
                    'object' => $object,
                    'event_data' => $extraEventData,
                    'event_name' => $eventName,
                ]
            ),
            $uniqueId
        );
    }

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

Сначала я недоумевал, а потом догадался, что случилось — у doBackground последний, необязательный парамер имеет тип «строка» и он не «нулабельный». То есть в строгой типизации я его должен либо не передавать вовсе, либо передавать туда исключительно строку. А null, который передавался туда до перехода на строгую типизацию более не подходит, ибо он не строка.

Пришлось переписать более уродливо:

public function put(Serialized $object, string $eventName, $extraEventData, string $uniqueId = null)
    {
        $args = [
            $this->queueName,
            igbinary_serialize(
                [
                    'object' => $object,
                    'event_data' => $extraEventData,
                    'event_name' => $eventName,
                ]
            ),
        ];

        if ($uniqueId !== null) {
            $args[] = $uniqueId;
        }

        return DI::gearman_client()->doBackground(...$args);
    }

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

Не смог найти, но я помню, что было чьё-то предложение расширить синтаксис ПХП — разрешить при вызове функции или метода использовать ключевое слово «default» для указания, что в данном месте нужно использовать значение по-умолчанию. Мне кажется тут бы оно пригодилось.

7 комментариев

PostgreSQL и PHP — слон слону не товарищ

Продолжаю серию удивительных открытий в мире перехода на «Постгрес». В документации к функции pg_execute есть малозаметное примечание к последнему параметру — в нём передаются значения для запроса:

Warning Elements are converted to strings by calling this function.

Думаю мало кто обращает на него внимания, собственно, я тоже не обращал. Прежде чем двинуться дальше, разберёмся — что же здесь написано?

Перевод такой: все значения, которые передаются, приводятся к строкам. Код, который это выполняется выглядит так (взял из ПХП 7.2):

if (num_params > 0) {
        int i = 0;
        params = (char **)safe_emalloc(sizeof(char *), num_params, 0);

        ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(pv_param_arr), tmp) {
                ZVAL_DEREF(tmp);
                if (Z_TYPE_P(tmp) == IS_NULL) {
                        params[i] = NULL;
                } else {
                        zval tmp_val;

                        ZVAL_COPY(&tmp_val, tmp);
                        convert_to_cstring(&tmp_val);
                        params[i] = estrndup(Z_STRVAL(tmp_val), Z_STRLEN(tmp_val));
                        zval_ptr_dtor(&tmp_val);
                }
                i++;
        } ZEND_HASH_FOREACH_END();
}

pgsql_result = PQexecParams(pgsql, query, num_params,
                                NULL, (const char * const *)params, NULL, NULL, 0);

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

Думаю, это связано с типизацией «Постгреса». Взять к примеру числа — два числовых типа ПХП нельзя адекватно преобразовать в россыпь типов «Постгреса», а если привести к неверному типу будут проблемы — в этой СУБД есть понятие перерузки функций, то есть функция выбирается не только по имени, но и по числу и типам параметров.

Поэтому и выбраны строки — они приведутся к нужному числовому типу сами собой, со строками это работает. К сожалению в этом преобразовании кроются и проблемы.

Ещё когда мы работали только с «Ораклом», заметили, что если вместо чисел привязывать строки, то иногда планы выполнения запросов меняются в худшую сторону. Лёгкость обращения с типами в ПХП иногда к этому приводит — переменная, используемая для хранения числа, имеет строковый тип.

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

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

Мой братишка придумал оригинальное решение — определять позиции на которых мы привязываем числа и автоматически внутри нашего фреймворка в этом месте запроса указывать тип bigint явным образом. То есть добавлять после плейсхолдера параметра конструкцию «::bigint».

Пришлось изменить несколько наших хранимых процедур, но в целом всё плошло довольно гладко.

1 комментарий

🐘 Кое-что новое об анонимных функциях PHP

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

class Tester
{
    public function returnFunc()
    {
        return function() {
            return "Function call.\n";
        };
    }

    public function returnStaticFunc()
    {
        return static function() {
            return "Static function call.\n";
        };
    }

    public function __destruct()
    {
        echo "Tester died.\n";
    }
}

$holder = (new Tester)->returnFunc();
gc_collect_cycles();
echo $holder(); // «Function call.»
unset($holder);

$holderStatic = (new Tester)->returnStaticFunc();
gc_collect_cycles(); // дважды «Tester died»
echo $holderStatic(); // «Static function call.»

Обратите внимание на последовательность «return static». Дело в том, что анонимные функции в ПХП, если они создаются в методе, неявно захватывают его контекст, то есть переменную $this. В этом нет никакой беды, если функция короткоживующая.

В моём примере другая ситуация — объект возвращает анонимную функцию, но сам он уже не нужен. Логично было бы предположить, что его при следующей сборке прибъёт сборщик мусора (который я принудительно вызываю функцией gc_collect_cycles), но нет. Ссылка на него содержится в анонимной функции, порождённой методом returnFunc, ПХП интерпретатор и понятия не имеет нужен ли будет внутри контекст объекта, поэтому он его захватывает внутри на всякий случай.

Во втором подпримере конструкция «static function» говорит интерпретатору, что в ссылке на объект мы не нуждаемся, потому вызов gc_collect_cycles убивает два объекта — как предыдущего подпримера (так как переменная $holder уничтожена), так и из текущего, так как такая анонимная функция не содержит ссылку на объект.

2 комментария

💢 OCI8: проблемы с переходом на PHP7

Ещё в копилку проблем с ПХП7: если при привязке переменных в Оракле (bind) поля типа LONG/LOB не находятся в конце списка, то вы получите ошибку ORA-24816. Все столбцы таких типов должны быть привязаны последними, в общей куче. Мы пока столкнулись с проблемой только при сохранении (тестирование пока идёт), в итоге в сохраняющем методе модели пересортировали привязки, основываясь на описании модели.

13 комментариев

💢 Проблема с переходом на PHP7: Memcached, часть вторая

В прошлый раз я писал о проблемном Мемкешд в ПХП7 и оказалось, что я не совсем прав. Проблема есть, но её корень я понимал неверно.

Я-то думал, что получение токена cas просто сломали при переезде на следующую версию ПХП, а оказалось это особенность — в ПХП7 используется третья версия модуля, а ней токен получается иначе — надо передать специальный параметр и после вызова токен будет в результирующем массиве.

Черновым кодом это выглядит примерно так (должно работать, но я его не запускал):

if (version_compare(phpversion('memcached'), '3.0.0-dev', '<')) {
    // работаем по-старому
    return $memcached;
} else {
    // возвращаем обёртку
    return new class($memcached) {
        use \Core\ProxyTrait;

        public function __construct($mc)
        {
            $this->setObject($mc);
        }

        public function get($key, callable $cache_cb = null, &$cas_token = null)
        {
            $result = $this->obj->get($key, $cache_cb, Memcached::GET_EXTENDED);

            if ($result === Memcached::GET_ERROR_RETURN_VALUE) {
                return false;
            }

            if ($result) {
                $cas_token = $result['cas'];
                return $result['value'];
            }

            return $result;
        }

        public function getMulti(array $keys, array &$cas_tokens = null, int $flag = null)
        {
            $result = $this->obj->getMulti($keys, Memcached::GET_EXTENDED | $flag);

            if ($result === Memcached::GET_ERROR_RETURN_VALUE) {
                return false;
            }

            if ($result) {
                $values = [];
                $cas_tokens = [];

                foreach ($result as $key => $d) {
                    $values[$key] = $d['value'];
                    $cas_tokens[$key] = $d['cas'];
                }

                return $values;
            }

            return $result;
        }
    }
}

Вся мякотка в последнем параметре Memcached::GET_EXTENDED, он заставляет возвращать соответствующие методы не искомое значение, а массив, содержащий в том числе и cas.

Неприятно, что в этой версии ПХП модуль для работы с Мемкешд помечен как «разработческий». В этом свете мне как-то неясна позиция тех, кто уверенно советует использовать ПХП7 в продакшне.

2 комментария

💢 Проблема с переходом на PHP7: Memcached

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

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

Оказалось, что в модуле Мемкешд для ПХП7 нет реализации получения токена cas в методах get и getMulti (наверняка нет ещё в каких-то), но мы их не используем. Это видно, например, по прототипу:

Method [ <internal:memcached> public method get ] {

  - Parameters [2] {
    Parameter #0 [ <required> $key ]
    Parameter #1 [ <optional> $cache_cb ]
  }
}

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

class MemcachedPHP7
{
    use \Core\ProxyTrait;

    public function __construct($mc)
    {
        $this->setObject($mc);
    }

    public function get($key, callable $cache_cb = null, &$cas_token = null)
    {
        switch (func_num_args()) {
            case 1:
                return $this->obj->get($key);
            case 2:
                return $this->obj->get($key, $cache_cb);
            default:
                if ($this->obj->getDelayed([$key], true) === false) {
                    return false;
                }

                $res = $this->obj->fetchAll();

                if ($res === false || !$res) {
                    if ($cache_cb !== null) {
                        if ($cache_cb($this->obj, $key, $value)) {
                            $this->obj->set($key, $value);
                        }
                    } else {
                        $value = false;
                    }
                } else {
                    $cas_token = $res[0]['cas'];
                    $value = $res[0]['value'];
                }

                return $value;
        }
    }

    public function getMulti(array $keys, array &$cas_tokens = null)
    {
        if (func_num_args() === 1) {
            return $this->obj->getMulti($key);
        } else {
            if ($this->obj->getDelayed($keys, true) === false) {
                return false;
            }
            $res = $this->obj->fetchAll();

            if ($res === false) {
                return false;
            }

            $cas_tokens = [];
            $values = [];

            $results = array_column($res, null, 'key');

            foreach ($keys as $key) {
                $cas_tokens[$key] = $results[$key]['cas'];
                $values[$key] = $results[$key]['value'];
            }

            return $values;
        }
    }
}

Трейт ProxyTrait я тут не привожу, там идея простая — он тупо проксирует всё, что получает через магические методы __get, __set, __call и прочие, setObject — метод этого трейта. Очень удобно, если надо оставить всё как есть, за исключением каких-то методов.

В остальном всё основано на том, что в методе getDelayed реализация токена cas есть, его я и использую, чтобы заткнуть эту дыру в функциональности. Работает всё так же как в ПХП 5.6, за исключением того, что в методе getMulti нет реализации последнего параметра — флага, вместо этого всё работает так, как будто он установлен, это ничему не мешает.

8 комментариев

🐘 PostgreSQL, PHP и подготовленные запросы

Одно из отличий «Постгреса» от «Оракла» — подготовленные планы запросов лежат не в разделяемом ресурсе, а в неком хранилище в рамках одного соединения. Программист, в рамках каждого соединения, должен подготовить их оператором PREPARE, в который передаётся выбранный идентификатор запроса и сам запрос, а потом выполнить его при помощи оператора EXECUTE, используя в качестве параметра переданный при подготовке идентификатор и значения параметров.

Польза подготовленных запросов в том, что можно единожды (за время жизни соединения, напоминаю) подготовить запрос, то есть попросить «Постгрес» выполнить его компиляцию в стандартное представление и оптимизацию, а после многократно его использовать, что очевидным образом поднимает производительность приложения.

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

Если план запроса больше не нужен, его можно удалить по идентификатору оператором DEALLOCATE, если же в качестве параметра передать ключевое слово ALL, то будут уничтожены все подготовленные в этом соединении планы. С закрытием соединения, планы так же исчезают.

Теме работы «Постргеса» с ПХП удивительно мало посвящено в руководстве к языку, так что пришлось произвести ряд самостоятельных исследований. Основной вопрос, на который я хотел для себя ответить — есть ли какая-то долгосрочная польза от подготовленных запросов в разрезе того, что обычная модель работы с ПХП — полная смерть скрипта после обработки запроса от клиента.

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

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

Код всего описанного выглядит вот так, я его упростил и переписал процедурно:

// открываем постоянное соединение с базой
$con = pg_pconnect($connection_string);

// загружаем подготовленные запросы
$res = pg_query("SELECT name FROM pg_prepared_statements");
$prepared = array_fill_keys(pg_fetch_all_columns($res), true);
pg_free_result($res);

// хеш от запроса, tiger — быстрый алгоритм, я люблю его использовать
$hash = base64_encode(hash('tiger160,3', $sql, true));

// смотрим — был ли уже подготовлен такой запрос
if (!isset($prepared[$hash])) {
    $prepared[$hash] = true;
    pg_prepare($con, $hash, $sql);
}

// выполняем запрос
$res = pg_execute($con, $hash, $params);

Общая идея, думаю, понятна из кода. Пока для меня остаётся открытым вопрос «просачиваются» ли в переданное соединение незакрытые транзакции или временные таблицы (предыдущий его владелец, например, мог умереть из-за нехватки памяти), эксперименты я ещё не проводил, но, думаю, да.

Надо заметить, что модуль «Постгреса» в ПХП в каждое соединение, перед его передачей в приложение, посылает команду RESET ALL, но она, насколько я понял из руководства, лишь сбрасывает различные конфигурационные переметры времени выполнения на значения по-умолчанию и не трогает транзакции и всё остальное (более того, она сама транзакционна).

7 комментариев

← 1 2 3 4 5 6 12 →