Как в Haskell сделать пул соединений к базе данных

10 февраля 2014

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

Для решения данной проблемы есть пакет resource-pool за авторством уже хорошо знакомого нам Bryan O’Sullivan. В пакете находится один-единственный модуль Data.Pool, который экспортирует две основных функции:

createPool
  :: IO a            -- функция создания нового ресурса
  -> (a -> IO ())    -- функция уничтожения ресурса
  -> Int             -- количество stripe-ов, см ниже
  -> NominalDiffTime -- сколько держать неиспользуемый ресурс открытым
  -> Int             -- максимальное количество ресурсов на stripe
  -> IO (Pool a)     -- возвращает пул

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

Теперь посмотрим, как производится работа с ресурсами из пула:

withResource :: MonadBaseControl IO m => Pool a -> (a -> m b) -> m b

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

Для того, чтобы в телефонной книге появился пул соединений, пришлось внести совсем немного изменений. Во-первых, был изменен модуль Phonebook.Utils. Например, функция connectAndRun стала выглядеть так:

connectAndRun fun = do
  (cs, s, t, c) <- getConnectParams
  pool <- createPool (connectPostgreSQL cs) disconnect s t c
  fun pool

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

read pool = do
  contacts <- liftIO $ withResource pool St.read
  Sc.json $ map toObj contacts
  where
    toObj :: (St.ContactId, St.Name, St.Phone) -> Value
    toObj (cid, name, phone) =
      object [ "contactId" .= cid
             , "name"      .= name
             , "phone"     .= phone
             ]

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

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

Метки: , .


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