Some practical Haskell
As I'm nearing the end of my time with my current employer I thought I'd put together some bits of practical Haskell that I've put into production. We only have a few services in Haskell, and basically I've had to sneak them into production. I'm hoping someone will find something useful. I'd be even happier if I get pointers on how to do this even better.
Logging
I've written about that earlier in three posts:
Final exception handler
After reading about the uncaught exception handler in Serokell's article I've added the following snippet to all the services.
main :: IO () main = do ... originalHandler <- getUncaughtExceptionHandler setUncaughtExceptionHandler $ handle originalHandler . lastExceptionHandler logger ... lastExceptionHandler :: Logger -> SomeException -> IO () lastExceptionHandler logger e = do fatalIO logger $ lm $ "uncaught exception: " <> displayException e
Handling signals
To make sure the platform we're running our services on is happy with a service
it needs to handle SIGTERM
, and when running it locally during development,
e.g. for manual testing, it's nice if it also handles SIGINT
.
The following snippet comes from a service that needs to make sure that every
iteration of its processing is completed before shutting down, hence the IORef
that's used to signal whether procession should continue or not.
main :: IO () main = do ... cont <- newIORef True void $ installHandler softwareTermination (Catch $ sigHandler logger cont) Nothing void $ installHandler keyboardSignal (Catch $ sigHandler logger cont) Nothing ... sigHandler :: Logger -> IORef Bool -> IO () sigHandler logger cont = do infoIO logger "got a signal, shutting down" writeIORef cont False
Probes
Due to some details about how networking works in our platform it's currently not possible to use network-based probing. Instead we have to use files. There are two probes that are of interest
- A startup probe, existance of the file signals that the service has started as is about being processing.
- A progress probe, a timestamp signals the time the most recent iteration of processing finished1.
I've written a little bit about the latter before in A little Haskell: epoch timestamp, but here I'm including both functions.
createPidFile :: FilePath -> IO () createPidFile fn = getProcessID >>= writeFile fn . show writeTimestampFile :: MonadIO m => FilePath -> m () writeTimestampFile fn = liftIO $ do getPOSIXTime >>= (writeFile fn . show) . truncate @_ @Int64 . (* 1000)
Footnotes:
The actual probing is then done using a command that compares the saved timestamp with the current time. As long as the difference is smaller than a threshold the probe succeeds.