3

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?

Georgi Lyubenov
  • 366
  • 1
  • 5

1 Answers1

5

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.

K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
  • 2
    I'm not sure this is so obvious. What if two threads race to create the key, and the loser inserts something into the vault that it can therefore never get back out (because the key in question has been overwritten by the winner)? (I don't actually know that this would be a problem. Please read this as a curious question that I have and think should be addressed in a good answer rather than a challenge to the correctness of this answer.) – Daniel Wagner Jan 07 '21 at 18:55
  • @DanielWagner, this specific scenario isn't a concern, since vaults are actually immutable, pure structures, so insertions by one thread are invisible to other threads. However, a race that results in `pauseTimeoutKey` returning different keys with different calls would be just as bad. As I now explain, I don't think this can happen. Anyway, my original answer was definitely lacking, so I've substantially extended it. – K. A. Buhr Jan 07 '21 at 20:39