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

- 366
- 1
- 5
-
5Ugh. Why have they done this? So un-Haskell. – Daniel Wagner Jan 07 '21 at 16:22
-
@DanielWagner, if the use described in the answer is actually the only one, then it's probably okay-ish ... except for testing purposes. – dfeuer Jan 07 '21 at 18:21
1 Answers
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 unsafeCoerce
d 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 Vault
s are just fancy immutable HashMap
s 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.

- 45,621
- 3
- 45
- 71
-
2I'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