Search code examples
haskellyesodhaskell-warp

Is it safe to run two warp servers from the same `main`?


There seem to be some "global vars" (unsafePerformIO + NOINLINE) in warps code base. Is it safe to run two instances of warp from the same main function, despite this?


Solution

  • It appears to be safe.

    At least in warp-3.3.13, the global variable trick is used (only) to generate keys for the vault package, using code like:

    pauseTimeoutKey :: Vault.Key (IO ())
    pauseTimeoutKey = unsafePerformIO Vault.newKey
    {-# NOINLINE pauseTimeoutKey #-}
    

    Note that this is different than the "usual" global variable trick, since it does not create a global IORef that multiple threads might try to use, while each expecting to be the sole user of the reference.

    Instead, the vault package provides a type-safe, persistent "store", a Vault, that acts like a collection of mutable variables of various types, accessible through unique keys. Keys are generated in IO, effectively using newUnique from Data.Unique. The Vault itself is a pure, safe data structure. It is implemented using unsafe operations, but constructed in a manner that makes it safe. Ultimately, it's a HashMap from Key a (so, a type-annotated Integer) to an Any value that can be unsafeCoerced to the needed type a, with type safety guaranteed by the type attached to the key. Values in the Vault are "mutated" by inserting new values in the map, creating an updated Vault, so there's no actual mutation going on here.

    Since Vaults are just fancy immutable HashMaps of pure values, there's no danger of two servers overwriting values in each others' vaults, even though they're using the same keys.

    As far as I can see, all that's needed to ensure safety is that, when a thread calls something like pauseTimeoutKey, it always gets the same key, and that key is unique among keys for that thread. So, it basically boils down to the thread safety of the global variable trick in general and of newUnique when used under unsafePerformIO.

    I've never heard of any cautions against using the global variables trick in multi-threaded code, and unsafePerformIO is intended to be thread-safe (which is why there's a separate "more efficient but potentially thread-unsafe" version unsafeDupablePerformIO).

    newUnique itself is implemented in a thread-safe manner:

    newUnique :: IO Unique
    newUnique = do
      r <- atomicModifyIORef' uniqSource $ \x -> let z = x+1 in (z,z)
      return (Unique r)
    

    and I can't see how running it under unsafePerformIO would make it thread-unsafe.