Thank you Marcin for the overview! The dejafu issue is indeed insightful as well.
Let me recap my understanding of unliftio - you are probably aware, but let's have it down for future reference.
(Monad)UnliftIO, the typeclass, itself is "just" an IO abstraction similar to previous ones like MonadIO or MonadBaseControl, but more restrictive: Only those instances are admitted, that play nicely with running them in plain IO, also with concurrenty. This in practice mostly means ReaderT IO and things isomorphic to it. So excluded is StateT etc - because it gets hairy to forkIO a StateT IO computation - the state is practically discarded (hm, one might be able to do something funky with async and StateT, though it would still operate on some forked state, and one would need to manually merge back isn't it..) Anyway UnliftIO's take in this case, is to be honest and use explicit mutable state (like MVars) in a concurrent setting.
Unliftio, the ecosystem (well, mostly the 'unlftio' package), is akin to lifted-base and lifted-whatever, providing operations with UnliftIO constraint.
Now, about unliftio and dejafu.. I pondered using them together (some thread lingering in
https://github.com/barrucadu/dejafu/issues/298). But one can't just program against some newtyped monad that has both a MonadConc instance (for dejafu's sake) and also an UnliftIO instance (for production running), because - and let's shelve API differences - the semantics on exception handling and cleanup differs: UnliftIO follows the safe-exceptions tradition, and a) the default catch doesn't catch async exceptions (so, as long as your own code is concerned, you can promptly kill a thread without accidentally catching the async exception with a sync-intended handler), also b) the default cleanup actions run under uninterruptibleMask (of which there's a mega-issue-thread, but the intention is to rather be correct and maybe deadlock, rather than leave resources in uncleaned state, in case the cleanup handler gets interrupted itself too).*
All in all, dejafu expects (or simulates) MonadConc according to the base behavior (unsurprisingly). So MonadConc would need to be banished into some internal layer of unliftio (in some unlfitio-concurrency package?), which sounds like a rather tedious work (but maybe not impossible).
But having this, at least when unliftio and dejafu is concerned, would be desirable - it would be worth a separate story the subtle differences I ran into when using unliftio's supposedly "just lifted" versions of some async operations, that subtly differ in exception handling / masking behavior compared to the original, and can lead to some "interesting" results. Not having dejafu available, I have to resort to random threadDelay injections when testing, which is not very nice (or fast, or comprehensive).
Having this said, I certainly see the utility of an abstraction that provides seamless mocking and testable concurrency. Mockable time sounds especially useful. And one, if really wants, can implement the safe-exceptions behavior on top of any abstraction (hm.. I guess), and avoid using oddly-behaving instances.
I hope one day I can try io-classes / io-sim. Good luck with it & thanks again!
*: I'm rather content with unliftio's choices about exception and cleanup behavior, just sad it creates such non-uniformity in tooling / mindset needed.