packages feed

linear-locks-0.1.0.0: src/LinearLocks/Internal/Mutex.hs

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LinearTypes #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE RequiredTypeArguments #-}
{-# LANGUAGE NoFieldSelectors #-}
{-# OPTIONS_GHC -Wno-deprecations #-}
{-# OPTIONS_HADDOCK not-home #-}

module LinearLocks.Internal.Mutex where

import Control.Concurrent (MVar)
import Control.Concurrent qualified as MVar
import Control.Functor.Linear qualified as L
import GHC.TypeLits (Nat)
import LinearLocks.Internal
import Prelude.Linear (Ur (..))
import Prelude.Linear qualified as L hiding (IO)
import System.IO.Linear qualified as L
import System.IO.Resource.Linear (RIO)
import System.IO.Resource.Linear qualified as RIO
import System.IO.Resource.Linear.Internal qualified as Internal

-- | A deadlock-free mutex.
--
-- This implementation is lazy.
-- This means that if you place an expensive unevaluated thunk inside a t`Mutex`,
-- it will be evaluated by the thread that consumes it, not the thread that produced it.
-- To avoid this, use "LinearLocks.Mutex.Strict" instead.
data Mutex (lvl :: Nat) a = Mutex
  { var :: MVar a,
    -- | The unique ID for this mutex. It's used to ensure t'LinearLocks.LockSet's don't contain duplicate mutexes, see 'LinearLocks.newLockSet'.
    id :: LockId
  }

-- | A t`MutexGuard` represents the ownership of a mutex.
--
-- It can be used to read/write the mutex while the lock is held.
--
-- It must be released with `release`, after which the guard will be consumed and can no longer be used.
data MutexGuard a = MutexGuard
  { resource :: RIO.Resource (MutexResource a),
    -- | The latest value set by the user.
    -- This will be comitted to the MVar when the guard is released.
    newValue :: Ur a
  }

data MutexResource a = MutexResource
  { -- | The value that was read from the `MVar` when it was acquired.
    --
    -- If an exception occurs before the mutex guard is manually released, this value will be put back into the `MVar`.
    initialValue :: a,
    var :: MVar a
  }

instance Acquirable (Mutex lvl a) where
  type Guard (Mutex lvl a) = MutexGuard a
  type Level (Mutex lvl a) = lvl

  getId m = m.id

  unsafeAcquire :: forall lvl a. Mutex lvl a -> RIO (MutexGuard a)
  unsafeAcquire m = L.do
    -- Note: we have to match on `UnsafeResource` so we can extract the `guard.initialValue`
    Internal.UnsafeResource key guard <- RIO.unsafeAcquire acq rel
    L.pure
      MutexGuard
        { resource = Internal.UnsafeResource key guard,
          newValue = Ur guard.initialValue
        }
    where
      acq :: L.IO (Ur (MutexResource a))
      acq = L.do
        Ur a <- L.fromSystemIOU L.$ MVar.takeMVar m.var
        L.pure (Ur (MutexResource {initialValue = a, var = m.var}))

      -- The action to run if an exception is thrown before the guard is manually released with `release`.
      rel :: MutexResource a -> L.IO ()
      rel (MutexResource initialValue var) =
        L.void L.$ L.fromSystemIO L.$ MVar.putMVar var initialValue

instance Releasable (MutexGuard a) where
  doRelease = release

read :: MutexGuard a %1 -> RIO (Ur a, MutexGuard a)
read (MutexGuard resource (Ur newValue)) =
  L.pure (Ur newValue, MutexGuard {resource, newValue = Ur newValue})

-- | Writes a new value to the mutex, which will be committed when the guard is released.
--
-- If an exception is thrown after `write` but before `release`,
-- the mutex will be rolled back to its original state.
write :: MutexGuard a %1 -> a -> RIO (MutexGuard a)
write (MutexGuard resource (Ur _)) newValue =
  L.pure (MutexGuard {resource, newValue = Ur newValue})

-- | Releases the mutex and commits the latest value set by `write`.
release :: MutexGuard a %1 -> RIO ()
release (MutexGuard ((Internal.UnsafeResource key mr)) (Ur newValue)) = L.do
  -- Note: the resource was initially registered with a release action that puts the original value back into the MVar.
  -- That release action should be run if an exception is thrown before `release` is called,
  -- which ensures the MVar will "rollback" to its original state.
  --
  -- However, if `release` is called explicitly by the user,
  -- we want to update the release action to put `newValue` back into the MVar instead.
  -- Therefore, we must call `release'` with a _new release action_ that puts `newValue` into the MVar.
  release' (Internal.UnsafeResource key mr) L.do
    L.void L.$ L.fromSystemIO L.$ MVar.putMVar mr.var newValue

-- | Creates a new mutex with the given initial value.
--
-- The @lvl@ parameter determines the order in which this mutex can be acquired relative to other mutexes.
--
-- It does not have to be unique, multiple mutexes can have the same level.
-- Mutexes with the same level can be added to a t`LinearLocks.LockSet` and acquired with 'LinearLocks.acquireMany'.
new :: forall a. forall (lvl :: Nat) -> a -> IO (Mutex lvl a)
new _lvl a = do
  var <- MVar.newMVar a
  id <- nextLockId
  pure Mutex {var, id}