Конвейерный режим

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

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

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

Конвейерный API libpq представляет собой функциональность на стороне клиента, которая не требует специальной серверной поддержки и работает с любым сервером, поддерживающим 3-ю версию протокола расширенных запросов. Подробную информацию см. в подразделе Конвейерный режим.



Применение конвейерного режима

Для запуска конвейеров приложение должно переключить соединение в конвейерный режим, что делается с помощью функции PQenterPipelineMode. Для проверки того, включен ли конвейерный режим, можно использовать функцию PQpipelineStatus. В конвейерном режиме разрешены только асинхронные операции; командные строки, содержащие несколько команд SQL, а также команда COPY запрещены. Использование функций синхронного выполнения команд, таких как PQfn, PQexec, PQexecParams, PQprepare, PQexecPrepared, PQdescribePrepared, PQdescribePortal, является условием ошибки. После обработки результатов всех отправленных команд и получения конечного результата конвейера приложение может вернуться в обычный режим с помощью функции PQexitPipelineMode.

Примечание
Лучше всего применять конвейерный режим с libpq в неблокирующем режиме. При использовании в блокирующем режиме возможны взаимоблокировки на уровне клиент/сервер. 1


Отправка запросов

Перейдя в конвейерный режим, приложение отправляет запросы с помощью функции PQsendQuery, PQsendQueryParams или родственной им функции PQsendQueryPrepared, работающей с подготовленными запросами. Эти запросы стоят в очередь на стороне клиента, пока не сбрасываются на сервер; это происходит, когда с помощью функции PQpipelineSync в конвейере устанавливается точка синхронизации, или когда вызывается функция PQflush. В конвейерном режиме также работают функции PQsendPrepare, PQsendDescribePrepared и PQsendDescribePortal. Обработка результата описывается ниже.

Сервер выполняет операторы и в том порядке, в каком их отправил клиент, возвращает их результаты. Сервер начнет выполнять команды в конвейере немедленно, не дожидаясь окончания конвейера. Обратите внимание, что результаты буферизуются на стороне сервера; сервер сбрасывает этот буфер клиенту, когда с помощью функции PQpipelineSync устанавливается точка синхронизации или когда вызывается функция PQsendFlushRequest. Если какой-либо оператор сталкивается с ошибкой, сервер прерывает текущую транзакцию и не выполняет никакую следующую команду в очереди до следующей точки синхронизации; для каждой такой команды выдается результат PGRES_PIPELINE_ABORTED. (Это остается справедливым, даже если команды в конвейере способны откатить транзакцию.) После точки синхронизации обработка запроса возобновляется.

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


Обработка результатов

Чтобы обработать результат одного запроса в конвейере, приложение многократно вызывает функцию PQgetResult и обрабатывает каждый результат, пока PQgetResult не вернет NULL. Затем может быть получен результат следующего запроса в конвейере, тоже с помощью PQgetResult, и цикл повторяется. Результаты отдельных операторов приложение обрабатывает как обычно. После того как будут возвращены результаты всех запросов в конвейере, PQgetResult вернет результат, содержащий значение статуса PGRES_PIPELINE_SYNC.

Клиент может предпочесть отложить обработку результатов до завершения отправки всего конвейера или чередовать ее с отправкой дальнейших запросов в конвейере; см. подраздел Чередование обработки результатов и отправки запросов.

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

Функция PQgetResult ведет себя так же, как и при обычной асинхронной обработке, за исключением того, что она может содержать новые типы PGresult PGRES_PIPELINE_SYNC и PGRES_PIPELINE_ABORTED. Результат PGRES_PIPELINE_SYNC выдается ровно один раз для каждого вызова PQpipelineSync в соответствующей точке конвейера. Результат PGRES_PIPELINE_ABORTED выдается вместо обычного результата запроса для первой ошибки и всех последующих результатов до следующего PGRES_PIPELINE_SYNC; см. подраздел Обработка ошибок.

Функции PQisBusy, PQconsumeInput и т. п. при обработке результатов конвейера работают как обычно. В частности, вызов PQisBusy в середине конвейера возвращает 0, если были обработаны результаты всех запросов, выполненных на данный момент.

libpq не предоставляет приложению никакой информации о запросе, обрабатываемом в настоящий момент (за исключением того, что функция PQgetResult возвращает NULL, чтобы указать, что начали возвращаться результаты следующего запроса). Приложение должно отслеживать порядок отправки запросов, чтобы связать их с соответствующими результатами. Обычно для этого приложения используют конечный автомат или очередь FIFO.


Обработка ошибок

С точки зрения клиента, после того как функция PQresultStatus возвращает PGRES_FATAL_ERROR, конвейер помечается как прерванный. PQresultStatus будет выдавать результат PGRES_PIPELINE_ABORTED для каждой оставшейся в очереди операции в прерванном конвейере. Результат для функции PQpipelineSync выдается в виде PGRES_PIPELINE_SYNC, сигнализируя об окончании прерванного конвейера и возобновлении обычной обработки результатов.

Клиент должен обрабатывать результаты с помощью функции PQgetResult во время восстановления после ошибки.

Если конвейер использовал неявную транзакцию, то операции, которые уже были выполнены, откатываются, а операции, которые были поставлены в очередь после неудавшейся операции, полностью пропускаются. Такое же поведение имеет место и если конвейер запускает и фиксирует единственную явную транзакцию (т. е. первый оператор — BEGIN, а последний — COMMIT), за исключением того, что по окончании конвейера сеанс остается в состоянии прерванной транзакции. Если конвейер содержит несколько явных транзакций, все транзакции, зафиксированные до ошибки, остаются зафиксированными, текущая транзакция прерывается, а все последующие операции полностью пропускаются, включая транзакции. Если точка синхронизации конвейера имеет место, когда явный блок транзакции находится в прерванном состоянии, следующий конвейер немедленно прерывается, если только следующая команда не переключает транзакцию в обычный режим с помощью ROLLBACK.

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


Чередование обработки результатов и отправки запросов

Во избежание взаимоблокировок с большими конвейерами, клиент должен быть построен вокруг неблокирующего цикла событий, использующего такие средства операционной системы, как функции select, poll, WaitForMultipleObjectEx и т. д.

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



Функции, связанные с конвейерным режимом

PQpipelineStatus

Возвращает текущий статус конвейерного режима соединения libpq.

PGpipelineStatus PQpipelineStatus(const PGconn *conn);

Функция PQpipelineStatus может возвращать одно из следующих значений:

  • PQ_PIPELINE_ON
    Соединение libpq находится в конвейерном режиме.

  • PQ_PIPELINE_OFF
    Соединение libpq находится не в конвейерном режиме.

  • PQ_PIPELINE_ABORTED
    Соединение libpq находится в конвейерном режиме, и при обработке текущего конвейера произошла ошибка. Флаг прерывания сбрасывается, когда функция PQgetResult возвращает результат типа PGRES_PIPELINE_SYNC.

PQenterPipelineMode

Заставляет соединение войти в конвейерный режим, если в настоящий момент оно бездействует или уже находится в конвейерном режиме.

int PQenterPipelineMode(PGconn *conn);

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

PQexitPipelineMode

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

int PQexitPipelineMode(PGconn *conn);

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

PQpipelineSync

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

int PQpipelineSync(PGconn *conn);

Возвращает 1 при успехе. Возвращает 0, если соединение не находится в конвейерном режиме или не удалось отправить сообщение синхронизации.

PQsendFlushRequest

Отправляет серверу команду сбросить его буфер вывода.

int PQsendFlushRequest(PGconn *conn);

Возвращает 1 при успехе. Возвращает 0 при любом сбое.

Сервер сбрасывает свой буфер вывода автоматически в результате вызова функции PQpipelineSync или при любом запросе не в конвейерном режиме; эта функция полезна, когда нужно заставить сервер сбросить его буфер вывода в конвейерном режиме без установки точки синхронизации. Обратите внимание, что сам этот запрос не сбрасывается на сервер автоматически; при необходимости используйте для этого функцию PQflush.



Когда использовать конвейерный режим

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

Конвейерный режим наиболее полезен с удаленным сервером, т. е. когда сетевая задержка («время ответа») велика, а также когда много небольших операций выполняются в быстрой последовательности. Обычно применение конвейерных команд дает меньше преимуществ, когда выполнение каждого запроса занимает во много раз больше времени, чем задержка из-за передачи и подтверждения приема между клиентом и сервером. Операция из 100 операторов, выполняемая на сервере за 300 мс за вычетом этого времени, без конвейеризации заняла бы 30 секунд из-за одной только сетевой задержки; с конвейеризацией эта операция потратит не более 0,3 с на ожидание результатов от сервера.

Используйте конвейерные команды, когда ваше приложение выполняет много небольших операций INSERT, UPDATE и DELETE, которые нельзя легко преобразовать в наборы операций или в операцию COPY.

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

BEGIN;
SELECT x FROM mytable WHERE id = 42 FOR UPDATE;
-- результат: x=2
-- клиент прибавляет 1 к x:
UPDATE mytable SET x = 3 WHERE id = 42;
COMMIT;

можно гораздо эффективнее сделать с помощью:

UPDATE mytable SET x = x + 1 WHERE id = 42;

Конвейеризация менее полезна и более сложна, когда один конвейер содержит несколько транзакций (см. подраздел Обработка ошибок).


1

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