module Ecluse.Core.Worker.Job (
JobOutcome (..),
processJob,
processBatch,
displayExceptionT,
) where
import Data.Map.Strict qualified as Map
import Katip (Severity (DebugS, ErrorS, InfoS, WarningS), katipAddNamespace, logFM, ls)
import UnliftIO (tryAny, withRunInIO)
import Ecluse.Core.Ecosystem (ecosystemName)
import Ecluse.Core.Package (pkgEcosystem, renderPackageName)
import Ecluse.Core.Queue (MirrorArtifact (maHashes), MirrorJob (jobArtifact, jobArtifactUrl, jobPackage, jobTraceContext, jobVersion), MirrorQueue (ack, extendVisibility), QueueMessage (msgJob, msgReceipt), ReceiptHandle, Seconds (Seconds))
import Ecluse.Core.Registry (PublishFault (PublishRejected, PublishUrlUnformable), RegistryClient (fetchMetadata, parseVersionList, publishArtifact))
import Ecluse.Core.Registry.Metadata (VersionEvaluation (VersionMetadataUnavailable, VersionMissing, VersionPresent))
import Ecluse.Core.Rules (evalRules)
import Ecluse.Core.Rules.Types (Decision (Admitted, Blocked, BlockedByDefault, Undecidable), EvalContext (EvalContext))
import Ecluse.Core.Telemetry.Metrics qualified as Metric
import Ecluse.Core.Telemetry.Record (WorkerMetricsPort (..), timedSeconds)
import Ecluse.Core.Telemetry.Span (JobSpanOutcome (JobSpanOutcome), WorkerTracingPort (..))
import Ecluse.Core.Version (renderVersion)
import Ecluse.Core.Worker.Fetch (fetchArtifactBytes)
import Ecluse.Core.Worker.Integrity (IntegrityResult (..), verifyIntegrity)
import Ecluse.Core.Worker.Types
processBatch :: [QueueMessage] -> WorkerM ()
processBatch :: [QueueMessage] -> WorkerM ()
processBatch = (QueueMessage -> WorkerM ()) -> [QueueMessage] -> WorkerM ()
forall (t :: * -> *) (f :: * -> *) a b.
(Foldable t, Applicative f) =>
(a -> f b) -> t a -> f ()
traverse_ QueueMessage -> WorkerM ()
processMessage
processMessage :: QueueMessage -> WorkerM ()
processMessage :: QueueMessage -> WorkerM ()
processMessage QueueMessage
message = do
metrics <- (WorkerRuntime -> WorkerMetricsPort) -> WorkerM WorkerMetricsPort
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks WorkerRuntime -> WorkerMetricsPort
wrMetrics
outcome <- processJob (msgReceipt message) (msgJob message)
liftIO (wmpMirrorJobProcessed metrics (jobResultMetric outcome))
case outcome of
JobOutcome
Succeeded -> ReceiptHandle -> WorkerM ()
ackMessage (QueueMessage -> ReceiptHandle
msgReceipt QueueMessage
message)
Dropped Text
reason -> do
Severity -> LogStr -> WorkerM ()
forall (m :: * -> *).
(Applicative m, KatipContext m) =>
Severity -> LogStr -> m ()
logFM Severity
ErrorS (Text -> LogStr
forall a. StringConv a Text => a -> LogStr
ls (Text
"dropping unrecoverable mirror job: " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
reason))
ReceiptHandle -> WorkerM ()
ackMessage (QueueMessage -> ReceiptHandle
msgReceipt QueueMessage
message)
Retried Text
reason ->
Severity -> LogStr -> WorkerM ()
forall (m :: * -> *).
(Applicative m, KatipContext m) =>
Severity -> LogStr -> m ()
logFM Severity
WarningS (Text -> LogStr
forall a. StringConv a Text => a -> LogStr
ls (Text
"leaving mirror job un-acked for retry (redelivered by a durable queue, re-mirrored on next demand by the in-memory one): " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
reason))
jobResultMetric :: JobOutcome -> Metric.MirrorResult
jobResultMetric :: JobOutcome -> MirrorResult
jobResultMetric = \case
JobOutcome
Succeeded -> MirrorResult
Metric.Published
Dropped Text
_ -> MirrorResult
Metric.Failed
Retried Text
_ -> MirrorResult
Metric.Failed
ackMessage :: ReceiptHandle -> WorkerM ()
ackMessage :: ReceiptHandle -> WorkerM ()
ackMessage ReceiptHandle
receipt = do
queue <- (WorkerRuntime -> MirrorQueue) -> WorkerM MirrorQueue
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks WorkerRuntime -> MirrorQueue
wrQueue
liftIO (ack queue receipt)
data JobOutcome
=
Succeeded
|
Dropped Text
|
Retried Text
deriving stock (JobOutcome -> JobOutcome -> Bool
(JobOutcome -> JobOutcome -> Bool)
-> (JobOutcome -> JobOutcome -> Bool) -> Eq JobOutcome
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: JobOutcome -> JobOutcome -> Bool
== :: JobOutcome -> JobOutcome -> Bool
$c/= :: JobOutcome -> JobOutcome -> Bool
/= :: JobOutcome -> JobOutcome -> Bool
Eq, Int -> JobOutcome -> ShowS
[JobOutcome] -> ShowS
JobOutcome -> String
(Int -> JobOutcome -> ShowS)
-> (JobOutcome -> String)
-> ([JobOutcome] -> ShowS)
-> Show JobOutcome
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> JobOutcome -> ShowS
showsPrec :: Int -> JobOutcome -> ShowS
$cshow :: JobOutcome -> String
show :: JobOutcome -> String
$cshowList :: [JobOutcome] -> ShowS
showList :: [JobOutcome] -> ShowS
Show)
processJob :: ReceiptHandle -> MirrorJob -> WorkerM JobOutcome
processJob :: ReceiptHandle -> MirrorJob -> WorkerM JobOutcome
processJob ReceiptHandle
receipt MirrorJob
job = Namespace -> WorkerM JobOutcome -> WorkerM JobOutcome
forall (m :: * -> *) a. KatipContext m => Namespace -> m a -> m a
katipAddNamespace Namespace
"job" (WorkerM JobOutcome -> WorkerM JobOutcome)
-> WorkerM JobOutcome -> WorkerM JobOutcome
forall a b. (a -> b) -> a -> b
$ do
Severity -> LogStr -> WorkerM ()
forall (m :: * -> *).
(Applicative m, KatipContext m) =>
Severity -> LogStr -> m ()
logFM Severity
DebugS (Text -> LogStr
forall a. StringConv a Text => a -> LogStr
ls (Text
"starting mirror job for " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> MirrorJob -> Text
renderJob MirrorJob
job))
tracing <- (WorkerRuntime -> WorkerTracingPort) -> WorkerM WorkerTracingPort
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks WorkerRuntime -> WorkerTracingPort
wrTracing
runtime <- ask
withRunInIO $ \forall a. WorkerM a -> IO a
runInIO ->
WorkerTracingPort
-> forall a.
PackageName
-> Version
-> Maybe RemoteSpanContext
-> (a -> JobSpanOutcome)
-> IO a
-> IO a
wtpMirrorJobSpan WorkerTracingPort
tracing (MirrorJob -> PackageName
jobPackage MirrorJob
job) (MirrorJob -> Version
jobVersion MirrorJob
job) (MirrorJob -> Maybe RemoteSpanContext
jobTraceContext MirrorJob
job) JobOutcome -> JobSpanOutcome
jobSpanOutcome (IO JobOutcome -> IO JobOutcome) -> IO JobOutcome -> IO JobOutcome
forall a b. (a -> b) -> a -> b
$
WorkerM JobOutcome -> IO JobOutcome
forall a. WorkerM a -> IO a
runInIO (WorkerM JobOutcome -> IO JobOutcome)
-> WorkerM JobOutcome -> IO JobOutcome
forall a b. (a -> b) -> a -> b
$
WorkerRuntime
-> forall (m :: * -> *) a.
(KatipContext m, MonadIO m) =>
m a -> m a
wrInjectTraceContext WorkerRuntime
runtime (ReceiptHandle -> MirrorJob -> WorkerM JobOutcome
reevaluateThenMirror ReceiptHandle
receipt MirrorJob
job)
where
jobSpanOutcome :: JobOutcome -> JobSpanOutcome
jobSpanOutcome :: JobOutcome -> JobSpanOutcome
jobSpanOutcome = \case
JobOutcome
Succeeded -> Text -> Maybe Text -> JobSpanOutcome
JobSpanOutcome Text
"succeeded" Maybe Text
forall a. Maybe a
Nothing
Dropped Text
reason -> Text -> Maybe Text -> JobSpanOutcome
JobSpanOutcome Text
"dropped" (Text -> Maybe Text
forall a. a -> Maybe a
Just Text
reason)
Retried Text
reason -> Text -> Maybe Text -> JobSpanOutcome
JobSpanOutcome Text
"retried" (Text -> Maybe Text
forall a. a -> Maybe a
Just Text
reason)
data ReevalOutcome
= ReevalAdmit
| ReevalDrop Text
| ReevalRetry Text
reevaluateThenMirror :: ReceiptHandle -> MirrorJob -> WorkerM JobOutcome
reevaluateThenMirror :: ReceiptHandle -> MirrorJob -> WorkerM JobOutcome
reevaluateThenMirror ReceiptHandle
receipt MirrorJob
job =
MirrorJob -> WorkerM Bool
alreadyMirrored MirrorJob
job WorkerM Bool -> (Bool -> WorkerM JobOutcome) -> WorkerM JobOutcome
forall a b. WorkerM a -> (a -> WorkerM b) -> WorkerM b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
Bool
True -> do
Severity -> LogStr -> WorkerM ()
forall (m :: * -> *).
(Applicative m, KatipContext m) =>
Severity -> LogStr -> m ()
logFM Severity
InfoS (Text -> LogStr
forall a. StringConv a Text => a -> LogStr
ls (Text
"already present at the mirror target, acking without re-publish: " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> MirrorJob -> Text
renderJob MirrorJob
job))
JobOutcome -> WorkerM JobOutcome
forall a. a -> WorkerM a
forall (f :: * -> *) a. Applicative f => a -> f a
pure JobOutcome
Succeeded
Bool
False ->
MirrorJob -> WorkerM ReevalOutcome
reevaluatePolicy MirrorJob
job WorkerM ReevalOutcome
-> (ReevalOutcome -> WorkerM JobOutcome) -> WorkerM JobOutcome
forall a b. WorkerM a -> (a -> WorkerM b) -> WorkerM b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
ReevalOutcome
ReevalAdmit -> ReceiptHandle -> MirrorJob -> WorkerM JobOutcome
mirrorArtifact ReceiptHandle
receipt MirrorJob
job
ReevalDrop Text
reason -> JobOutcome -> WorkerM JobOutcome
forall a. a -> WorkerM a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Text -> JobOutcome
Dropped Text
reason)
ReevalRetry Text
reason -> JobOutcome -> WorkerM JobOutcome
forall a. a -> WorkerM a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Text -> JobOutcome
Retried Text
reason)
alreadyMirrored :: MirrorJob -> WorkerM Bool
alreadyMirrored :: MirrorJob -> WorkerM Bool
alreadyMirrored MirrorJob
job = do
client <- (WorkerRuntime -> RegistryClient) -> WorkerM RegistryClient
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks WorkerRuntime -> RegistryClient
wrRegistry
probed <- tryAny (liftIO (fetchMetadata client (jobPackage job)))
pure $ case probed of
Left SomeException
_ -> Bool
False
Right RegistryResponse
response -> case RegistryClient -> RegistryResponse -> Either ParseError [Version]
parseVersionList RegistryClient
client RegistryResponse
response of
Left ParseError
_ -> Bool
False
Right [Version]
versions -> MirrorJob -> Version
jobVersion MirrorJob
job Version -> [Version] -> Bool
forall (f :: * -> *) a.
(Foldable f, DisallowElem f, Eq a) =>
a -> f a -> Bool
`elem` [Version]
versions
reevaluatePolicy :: MirrorJob -> WorkerM ReevalOutcome
reevaluatePolicy :: MirrorJob -> WorkerM ReevalOutcome
reevaluatePolicy MirrorJob
job = do
policies <- (WorkerRuntime -> WorkerPolicies) -> WorkerM WorkerPolicies
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks WorkerRuntime -> WorkerPolicies
wrPolicies
case Map.lookup ecosystem policies of
Maybe WorkerPolicy
Nothing ->
ReevalOutcome -> WorkerM ReevalOutcome
forall a. a -> WorkerM a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Text -> ReevalOutcome
ReevalDrop (Text
"no rule policy is configured for the " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Ecosystem -> Text
ecosystemName Ecosystem
ecosystem Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" ecosystem; refusing to mirror " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> MirrorJob -> Text
renderJob MirrorJob
job))
Just WorkerPolicy
policy -> do
evaluation <- IO VersionEvaluation -> WorkerM VersionEvaluation
forall a. IO a -> WorkerM a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (WorkerPolicy -> PackageName -> Version -> IO VersionEvaluation
wpResolveVersion WorkerPolicy
policy (MirrorJob -> PackageName
jobPackage MirrorJob
job) (MirrorJob -> Version
jobVersion MirrorJob
job))
case evaluation of
VersionEvaluation
VersionMetadataUnavailable ->
ReevalOutcome -> WorkerM ReevalOutcome
forall a. a -> WorkerM a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Text -> ReevalOutcome
ReevalRetry (Text
"could not re-fetch metadata to re-evaluate current policy for " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> MirrorJob -> Text
renderJob MirrorJob
job))
VersionEvaluation
VersionMissing ->
ReevalOutcome -> WorkerM ReevalOutcome
forall a. a -> WorkerM a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Text -> ReevalOutcome
ReevalDrop (Text
"the public upstream no longer offers " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> MirrorJob -> Text
renderJob MirrorJob
job Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"; refusing to mirror a withdrawn version"))
VersionPresent PackageDetails
details -> do
ctx <- IO EvalContext -> WorkerM EvalContext
forall a. IO a -> WorkerM a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (UTCTime -> EvalContext
EvalContext (UTCTime -> EvalContext) -> IO UTCTime -> IO EvalContext
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> WorkerPolicy -> IO UTCTime
wpNow WorkerPolicy
policy)
decision <- liftIO (evalRules ctx (wpRules policy) details)
pure (outcomeOfDecision job decision)
where
ecosystem :: Ecosystem
ecosystem = PackageName -> Ecosystem
pkgEcosystem (MirrorJob -> PackageName
jobPackage MirrorJob
job)
outcomeOfDecision :: MirrorJob -> Decision -> ReevalOutcome
outcomeOfDecision :: MirrorJob -> Decision -> ReevalOutcome
outcomeOfDecision MirrorJob
job = \case
Admitted{} -> ReevalOutcome
ReevalAdmit
Blocked Text
ruleName Text
reason ->
Text -> ReevalOutcome
ReevalDrop (Text
"current policy denies " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> MirrorJob -> Text
renderJob MirrorJob
job Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
": blocked by " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
ruleName Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" (" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
reason Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
")")
BlockedByDefault [Text]
_ ->
Text -> ReevalOutcome
ReevalDrop (Text
"current policy denies " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> MirrorJob -> Text
renderJob MirrorJob
job Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
": no rule admits it")
Undecidable Transience
_ Text
reason ->
Text -> ReevalOutcome
ReevalRetry (Text
"current policy could not be evaluated for " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> MirrorJob -> Text
renderJob MirrorJob
job Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
": " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
reason)
mirrorArtifact :: ReceiptHandle -> MirrorJob -> WorkerM JobOutcome
mirrorArtifact :: ReceiptHandle -> MirrorJob -> WorkerM JobOutcome
mirrorArtifact ReceiptHandle
receipt MirrorJob
job = do
Severity -> LogStr -> WorkerM ()
forall (m :: * -> *).
(Applicative m, KatipContext m) =>
Severity -> LogStr -> m ()
logFM Severity
DebugS (Text -> LogStr
forall a. StringConv a Text => a -> LogStr
ls (Text
"fetching artifact bytes from " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> MirrorJob -> Text
jobArtifactUrl MirrorJob
job))
fetched <- Text -> WorkerM (Either Text ByteString)
fetchArtifactBytes (MirrorJob -> Text
jobArtifactUrl MirrorJob
job)
case fetched of
Left Text
reason -> JobOutcome -> WorkerM JobOutcome
forall a. a -> WorkerM a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Text -> JobOutcome
Retried Text
reason)
Right ByteString
bytes ->
case NonEmpty Hash -> ByteString -> IntegrityResult
verifyIntegrity (MirrorArtifact -> NonEmpty Hash
maHashes MirrorArtifact
artifact) ByteString
bytes of
IntegrityMismatch Text
detail -> do
Severity -> LogStr -> WorkerM ()
forall (m :: * -> *).
(Applicative m, KatipContext m) =>
Severity -> LogStr -> m ()
logFM Severity
ErrorS (Text -> LogStr
forall a. StringConv a Text => a -> LogStr
ls (Text
"artifact integrity mismatch, refusing to publish: " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
detail))
JobOutcome -> WorkerM JobOutcome
forall a. a -> WorkerM a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Text -> JobOutcome
Dropped (Text
"integrity mismatch: " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
detail))
IntegrityResult
IntegrityVerified -> ReceiptHandle -> MirrorJob -> ByteString -> WorkerM JobOutcome
publishVerified ReceiptHandle
receipt MirrorJob
job ByteString
bytes
where
artifact :: MirrorArtifact
artifact = MirrorJob -> MirrorArtifact
jobArtifact MirrorJob
job
publishVerified :: ReceiptHandle -> MirrorJob -> ByteString -> WorkerM JobOutcome
publishVerified :: ReceiptHandle -> MirrorJob -> ByteString -> WorkerM JobOutcome
publishVerified ReceiptHandle
receipt MirrorJob
job ByteString
bytes = do
ReceiptHandle -> WorkerM ()
holdForLongPublish ReceiptHandle
receipt
client <- (WorkerRuntime -> RegistryClient) -> WorkerM RegistryClient
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks WorkerRuntime -> RegistryClient
wrRegistry
metrics <- asks wrMetrics
(result, seconds) <- timedSeconds (liftIO (publishArtifact client (jobPackage job) (jobVersion job) artifact bytes))
liftIO (wmpMirrorPublishDuration metrics seconds)
case result of
Right () -> do
Severity -> LogStr -> WorkerM ()
forall (m :: * -> *).
(Applicative m, KatipContext m) =>
Severity -> LogStr -> m ()
logFM Severity
InfoS (Text -> LogStr
forall a. StringConv a Text => a -> LogStr
ls (Text
"mirrored artifact published: " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> MirrorJob -> Text
renderJob MirrorJob
job))
JobOutcome -> WorkerM JobOutcome
forall a. a -> WorkerM a
forall (f :: * -> *) a. Applicative f => a -> f a
pure JobOutcome
Succeeded
Left (PublishRejected PublishError
err) -> do
ReceiptHandle -> WorkerM ()
releaseForRetry ReceiptHandle
receipt
JobOutcome -> WorkerM JobOutcome
forall a. a -> WorkerM a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Text -> JobOutcome
Retried (Text
"registry rejected publish: " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> PublishError -> Text
forall b a. (Show a, IsString b) => a -> b
show PublishError
err))
Left (PublishUrlUnformable UrlFormationError
urlErr) ->
JobOutcome -> WorkerM JobOutcome
forall a. a -> WorkerM a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Text -> JobOutcome
Dropped (Text
"unformable publish URL: " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> UrlFormationError -> Text
forall b a. (Show a, IsString b) => a -> b
show UrlFormationError
urlErr))
where
artifact :: MirrorArtifact
artifact = MirrorJob -> MirrorArtifact
jobArtifact MirrorJob
job
holdForLongPublish :: ReceiptHandle -> WorkerM ()
holdForLongPublish :: ReceiptHandle -> WorkerM ()
holdForLongPublish ReceiptHandle
receipt = do
queue <- (WorkerRuntime -> MirrorQueue) -> WorkerM MirrorQueue
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks WorkerRuntime -> MirrorQueue
wrQueue
_ <- tryAny (liftIO (extendVisibility queue receipt extendBy))
pass
where
extendBy :: Seconds
extendBy :: Seconds
extendBy = Int -> Seconds
Seconds Int
300
releaseForRetry :: ReceiptHandle -> WorkerM ()
releaseForRetry :: ReceiptHandle -> WorkerM ()
releaseForRetry ReceiptHandle
receipt = do
queue <- (WorkerRuntime -> MirrorQueue) -> WorkerM MirrorQueue
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks WorkerRuntime -> MirrorQueue
wrQueue
_ <- tryAny (liftIO (extendVisibility queue receipt (Seconds 0)))
pass
renderJob :: MirrorJob -> Text
renderJob :: MirrorJob -> Text
renderJob MirrorJob
job = PackageName -> Text
renderPackageName (MirrorJob -> PackageName
jobPackage MirrorJob
job) Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"@" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Version -> Text
renderVersion (MirrorJob -> Version
jobVersion MirrorJob
job)
displayExceptionT :: (Exception e) => e -> Text
displayExceptionT :: forall e. Exception e => e -> Text
displayExceptionT = String -> Text
forall a. ToText a => a -> Text
toText (String -> Text) -> (e -> String) -> e -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. e -> String
forall e. Exception e => e -> String
displayException