# Пул соединений для работы с PostgreSQL Библиотека реализует комплексное решение для взаимодействия эрланг проекта с базой PostgreSQL на базе асинхронной версии драйвера [epgsql](https://github.com/epgsql/epgsql) и пула процессов [pooler](https://github.com/seth/pooler). Она позволяет создать несколько пулов процессов, каждый со своими настройками соединения с базой. Так что разные пулы могут работать с разными базами. Внутри пула есть несколько процессов, и каждый из них устанавливает свое соединение с базой и посылает запросы к ней, независимо от остальных процессов. Чтобы выполнить запрос к базе, берется свободный процесс из пула, который сериализует запрос, посылает его базе, получает и десериализует ответ, и возвращается в пул. ## Запуск и остановка пула Для запуска пула нужно вызывать функцию **epgsql_pool:start/4** с аргументами: - имя пула _atom() | string() | binary()_ - число соединений _integer()_ - максимальное число соединений _integer()_ - настройки соединения _map()_ ``` Params = #{host => "localhost", port => 5432, username => "someuser", password => "pass", database => "main_db"}, {ok, _} = epgsql_pool:start(main_pool, 10, 20, Params), Params2 = #{host => "localhost", port => 5432, username => "someuser", password => "pass", database => "other_db"}, {ok, _} = epgsql_pool:start(other_pool, 10, 20, Params2), ``` Настройки соединения должны быть **map()** или **#epgsql_connection_params{}** (определена в include/epgsql_pool.hrl). Для остановки пула нужно вызывать **epgsql_pool:stop(PoolName)**. ## Проверка настроек Каждый процесс в пуле пытается установить соединение с базой. Если по каким-то причинам это не удается, то процесс логирует ошибку, и через короткий промежуток времени снова пытается соединиться. Если настройки соединения указаны неправильно, то такие попытки повторяются бесконечно. И если процессов в пуле много, то генерируется много сообщений об ошибках, которые создают большую нагрузку на IO и записывают много логов. Поэтому, перед запуском пула рекомендуется проверить правильность настроек. Это можно сделать вызовом **epgsql_pool:validate_connection_params/1** ``` 1> Params = #{host => "localhost", 1> port => 5432, 1> username => "test", 1> password => "test", 1> database => "testdb"}. 2> epgsql_pool:validate_connection_params(Params). ok 3> epgsql_pool:validate_connection_params(Params#{password := "123"}). {error,invalid_password} 4> epgsql_pool:validate_connection_params(Params#{database := "some"}). {error,{error,fatal,<<"3D000">>, <<"database \"some\" does not exist">>,[]}} ``` Здесь также настройки должны быть **map()** или **#epgsql_connection_params{}**. Если настройки оказались неправильными, то, вероятно, вы захотите сообщить об ошибке и остановить ноду. ## Запрос к базе данных Для отправки запроса нужно вызывать одну из функций **epgsql_pool:query/2, /3, /4** с аргументами: - имя пула _atom() | string() | binary()_ - SQL-запрос _io_list()_ - опционально, параметры запроса _[term()]_ - опционально, дополнительные настройки _[proplists:option()]_ Формат SQL-запроса, параметров к нему, возможные форматы ответа такие же, как требует драйвер [epgsql](https://github.com/epgsql/epgsql). Подробности смотрите в документации драйвера. Напрямую работать с пулом соединений не нужно, об этом заботится библиотека. ``` 5> epgsql_pool:query(my_pool, "INSERT INTO category (id, title) VALUES (1, 'My Category'), (2, 'Other Category')"). {ok,2} 6> epgsql_pool:query(my_pool, "INSERT INTO category (id, title) VALUES (3, 'Next Category') RETURNING id"). {ok,1,[{column,<<"id">>,int8,8,-1,1}],[{3}]} 7> epgsql_pool:query(my_pool, "SELECT * FROM category"). {ok,[{column,<<"id">>,int8,8,-1,1}, {column,<<"title">>,text,-1,-1,1}], [{1,<<"My Category">>}, {2,<<"Other Category">>}, {3,<<"Next Category">>}]} ``` Есть ограничение на время выполнения запроса, по умолчанию оно 10 секунд. Если за это время библиотека не получает ответ от базы, то запрос отменяется, и возвращается {error, timeout}. ``` 8> epgsql_pool:query(my_pool, "select pg_sleep(100)"). {error,timeout} ``` Процесс из пула блокируется, пока не получит ответ от базы. Если запрос выполняется долго, то процесс долго не возвращается в пул. Если послать много таких долгих запросов, то можно исчерпать весь пул. Для защиты от такой ситуации введен timeout. Вы можете изменить timeout для конкретного запроса. ``` 9> epgsql_pool:query(my_pool, "select pg_sleep(10)", [], [{timeout, 15000}]). {ok,[{column,<<"b">>,void,4,-1,0}],[{<<>>}]} ``` Или даже указать {timeout, infinity}, если вообще не хотите ограничивать время запроса. ``` 10> epgsql_pool:query(my_pool, "select pg_sleep(10)", [], [{timeout, infinity}]). {ok,[{column,<<"b">>,void,4,-1,0}],[{<<>>}]} ``` timeout задается в миллисекундах. И это пока единственная настройка запроса, которая поддерживается библиотекой. Возможно, в следующих версиях появятся и другие настройки. (Поэтому 4-й аргумент -- proplist, а не атомарное значение). Можно изменить и timeout по умолчанию, что повлияет на все запросы. Смотрите ниже раздел **Настройки**. ## Транзациии Чтобы выполнить несколько запросов в транзакции, нужно вызывать функцию **epgsql_pool:transaction/2** с аргументами: - имя пула _atom() | string() | binary()_ - ваша функция _fun()_ В функцию передается процесс из пула, и запросы к базе данных нужно выполнять, используя этот процесс вместо имени пула. ``` epgsql_pool:transaction(my_pool, fun(Worker) -> Res1 = epgsql_pool:query(Worker, Query1), ... ResN = epgsql_pool:query(Worker, QueryN) end). ``` Любое исключение, возникающее внутри функции, отменяет транзакцию. ``` epgsql_pool:transaction(my_pool, fun(Worker) -> Res1 = epgsql_pool:query(Worker, Query1), ... case SomeData of GoodResult -> do_something; BadResult -> throw(cancel_transaction) end, ResN = epgsql_pool:query(Worker, QueryN) end). ``` ## Keep Alive Иногда соединение с базой данных может оборваться, причем клиентская сторона (epgsql драйвер), может об этом и не знать. В этом случае запросы, проходящие через это соединение, будут завершаться с ошибкой {error, timeout}. Если не восстановить соединение, то в пуле окажется процесс (или несколько процессов), неспособный выполнять запросы. Поэтому каждый процесс в пуле мониторит состояние своего соединения, периодически отправляя keep-alive запросы (на самом деле это запросы "SELECT 1"). Если в течение определенного времени на такой запрос не приходит ответ от базы данных, то соединение считается потерянным, и процесс устанавливает его заново. Все это происходит внутри библиотеки, незаметно для пользователя. Но может потребоваться настройка интервала, через которые посылаются запросы (по умолчанию 60 секунд). В текущей версии библиотеки не предусмотрена возможность отключить keep-alive. Но можно установить очень долгий интервал. Планируется добавить такую настройку. ## Reconnect В некоторых случаях **epgsql_pool:query** может вернуть {error, reconnecting}. Это значит, процесс потерял соединение с базой и пытается его восстановить. Попытки установить соединение реализованы с exponential backoff, с увеличением интервала между попытками от 100 миллисекунд до 5 секунд. Число попыток не ограничено. Эти интервалы настраиваются. Можно попытаться повторить вызов **epgsql_pool:query**. Скорее всего из пула будет получен другой процесс, и он сможет выполнить запрос. ## Настройки Библиотека позволяет конфигурировать ряд параметров, касающихся работы пула, и различных интервалов. Все доступные параметры можно получить вызовом **epgsql_pool:get_settings/0**: ``` 3> epgsql_pool:get_settings(). #{connection_timeout => 10000, keep_alive_timeout => 60000, max_reconnect_timeout => 5000, min_reconnect_timeout => 100, pooler_get_worker_timeout => 10000, pooler_max_queue => 100, query_timeout => 10000} ``` И можно установить новые значения вызовом **epgsql_pool:set_settings/1**: ``` 3> epgsql_pool:set_settings( #{keep_alive_timeout => 24 * 3600 * 1000, query_timeout => 60000}). ``` При этом в map можно указать только те параметры, которые вы ходите поменять. Доступны следующие настройки: **connection_timeout** -- timeout на установку соединения для драйвера epgsql. По умолчанию 10000 миллисекунд. **keep_alive_timeout** -- с таким интервалом посылаются запросы keep alive. По умолчанию 60000 миллисекунд **max_reconnect_timeout** и **min_reconnect_timeout** -- exponential backoff параметры для восстановления соединения. Интервал между попытками восстановления начинается с **min_reconnect_timeout** и экспоненциально растет после каждой попытки до **max_reconnect_timeout**. По умолчанию 5000 и 100 миллисекунд. **pooler_get_worker_timeout** -- timeout на получение процесса из пула. Если в пуле нет свободных процессов, и они не появились за это время, то epgsql\_pool:query вернет {error, pool\_overload} По умолчанию 10000 миллисекунд. **pooler_max_queue** -- размер очереди запросов на получение процесса из пула. Если запросов будет больше, то epgsql\_pool:query вернет {error, pool\_overload}. По умолчанию 100 запросов. **query_timeout** -- timeout на время выполнения запроса к базе. Действует для всех запросов, кроме тех, где timeout явно переопределен. Смотрите раздел **Запрос к базе данных** выше. По умолчанию 10000 миллисекунд. Значения по умолчанию могут измениться в новых версиях библиотеки.