Почему происходит утечка памяти этого кода при добавлении `bracketOnError`?

Во-первых, я извиняюсь за то, что у меня нет минимального примера (я могу попытаться построить его, но пока у меня есть пример до и после):

Сначала после чего происходит утечка памяти:

protoReceiver :: RIO FdsEnv ()
protoReceiver = do
  logItS Info ["Entering FarmPCMessage protoReceiver"]
  tMap <- liftIO $ newThreadMap
  fdsEnv <- ask
  let lgr = fdsLogger fdsEnv
  loopBody <- pure $ bracketOnError
    (runResourceT $ protoServe fdsEnv tMap readFarmPCMessage)
    (\(_,w) -> do
      logLogItS Debug lgr ["Entering cleanup for protoReceiver"]
      )
    (\(server,_) -> do
      logLogItS Debug lgr ["Entering FarmPCMessage protoReceiver bracket"]
      server
        .| mapMC (liftIO . traverse_ (persistFarmEntry fdsEnv))
        .| mapMC ((logLogIt Info lgr) . pure)
        .| sinkUnits & runConduitRes
      )
  liftIO loopBody

Вот предыдущий код, который не допускает утечки памяти:

protoReceiver :: RIO FdsEnv ()
protoReceiver = do
  logItS Info ["Entering FarmPCMessage protoReceiver"]
  tMap <- liftIO $ newThreadMap
  fdsEnv <- ask
  let lgr = fdsLogger fdsEnv

  (dmgrProtoServe, tcpWorker) <- liftIO $ runResourceT
    $ protoServe fdsEnv tMap readFarmPCMessage
  liftIO $ runResourceT $ dmgrProtoServe
    .| mapMC (liftIO . traverse_ (persistFarmEntry fdsEnv))
    .| mapMC ((logLogIt Info lgr) . pure)
    .| sinkUnits & runConduit

Я сделал некоторое профилирование утечки, хотя я не уверен, что это особенно полезно (любые предложения по улучшению диаграмм профилирования приветствуются):

профилирование RTS с помощью -hc профилирование RTS с -hd


person bbarker    schedule 24.09.2020    source источник
comment
Единственная гипотеза, которая приходит мне в голову, заключается в том, что значение server сохраняется дольше, чем должно, потому что оно должно быть передано обработчику исключений в случае ошибки. Я бы попробовал взломать/поэкспериментировать только с прямым возвратом w из действия выделения и передать значение server в тело цикла, используя вместо этого MVar.   -  person danidiaz    schedule 24.09.2020
comment
@danidiaz Похоже, ваша интуиция была верна, и это действительно устранило утечку. Но, как вы сказали, это кажется немного хакерским. С удовольствием оставлю это на данный момент, но было бы хорошо понять это более глубоко.   -  person bbarker    schedule 24.09.2020
comment
Этот пост в блоге может быть связан с текущей проблемой: правильный тип. com/blog/2016/09/sharing-conduit   -  person danidiaz    schedule 24.09.2020


Ответы (1)


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

import Data.Foldable (traverse_)

main :: IO ()
main = do
    let xs = [1..]
    traverse_ print xs 
    traverse_ print xs -- commenting this statement solves the leak 

Здесь канал Source работает как своего рода ленивый список. Нам нужно сохранить ссылку на исходное исходное значение (server) даже при его использовании, поскольку в случае ошибки оно должно быть передано обработчику исключений. И все же обработчик исключений, похоже, не использует его.

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

Действие распределения вместо возврата значения (Source m r, a) может вернуть значение (MVar (Source m r), a). Затем основное вычисление будет выполняться из takeMVar для получения источника канала. Как только мы начнем потреблять исходный код, исходное значение будет удалено сборщиком мусора, потому что на него больше не останется ссылок.

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

protoReceiver :: RIO FdsEnv ()
protoReceiver = retryForever $ do
  logItS Info ["Entering FarmPCMessage protoReceiver"]
  tMap <- liftIO $ newThreadMap
  fdsEnv <- ask
  let lgr = fdsLogger fdsEnv
  loopBody <- pure $ bracket
    (runResourceT $ do
      swTup <- protoServe fdsEnv tMap readFarmPCMessage
      serverMVar <- newMVar $ fst swTup
      pure (serverMVar, snd $! swTup)
      )
    (\(_, worker) -> do
      logLogItS Debug lgr ["Entering cleanup for protoReceiver"]
      killChildThreads tMap
      cancel worker
      )
    (\(serverMVar, _) -> do
      logLogItS Debug lgr ["Entering FarmPCMessage protoReceiver bracket"]
      server <- takeMVar serverMVar
      logLogItS Debug lgr ["FarmPCMessage protoReceiver bracket: got server"]
      server
        .| mapMC (liftIO . traverse_ (persistFarmEntry fdsEnv))
        .| mapMC ((logLogIt Info lgr) . pure)
        .| sinkUnits & runConduitRes
      )
  liftIO $ retryForever $ loopBody
  where
    killChildThreads = liftIO . killThreadHierarchy
person danidiaz    schedule 25.09.2020
comment
Интересный эффект, который я заметил, заключается в том, что если я перейду от использования подхода takeMVar/putMVar, как обсуждалось в комментариях, к распределению MVar в скобках, как обсуждается здесь, я, похоже, попаду в тупик - или, по крайней мере, выход канала не будет показал - посмотрю подробнее. Однако, если я поменяю местами takeMVar на readMVar, утечка памяти вернется! Я предполагаю, что это потому, что GHC достаточно умен, чтобы знать, что если мы используем readMVar, он может безопасно сохранить ссылку на значение, обернутое в MVar: gist.github.com/bbarker/6cd3c9fe8dbbcb63ad21ec4fda80e70d - person bbarker; 25.09.2020
comment
@bbarker В связанном фрагменте вы помещаете значения server и w внутри MVar. Это создаст взаимоблокировку при вызове обработчика исключений, потому что к тому времени MVar будет пустым! Лучше помещать server только в MVar (то есть возвращать MVar в кортеже, а не кортеж внутри MVar). Таким образом, обработчику исключений не нужно будет вызывать takeMVar, он будет иметь w прямо под рукой. - person danidiaz; 25.09.2020
comment
о да, конечно. Когда я обновил это (см. обновленную ссылку выше), чтобы принять это предложение во внимание, кажется, что он снова вернулся к утечке). Я полагаю, как указано в статье, это может быть очень привередливым материалом. Я прокомментировал альтернативу, которую я могу использовать на данный момент, которая не протекает он также не использует MVars, но, с другой стороны, в настоящее время также не имеет финализатора для создания Conduit. Тем не менее, в моих тестах он кажется довольно устойчивым. - person bbarker; 25.09.2020
comment
@bbarker Вы должны выполнить явное сопоставление шаблона с результатом protoServe, а не зависеть от fst и snd для извлечения компонентов. Причина в том, что ссылка на кортеж — и, следовательно, на источник канала — скрывается за переходником snd swTup. Лень коварна. - person danidiaz; 26.09.2020
comment
Ах да, имеет смысл. В итоге я просто использовал $! для этой цели. Я позволил себе включить свой рабочий код в ваш ответ для полноты картины. - person bbarker; 28.09.2020