Safe Haskell | None |
---|---|
Language | Haskell2010 |
Synopsis
-
data
RegistryClosedException
=
forall
m.
IOLike
m =>
RegistryClosedException
{
- registryClosedRegistryContext :: !(Context m)
- registryClosedCloseCallStack :: ! PrettyCallStack
- registryClosedAllocContext :: !(Context m)
- data ResourceRegistryThreadException
- bracketWithPrivateRegistry :: ( IOLike m, HasCallStack ) => ( ResourceRegistry m -> m a) -> (a -> m ()) -> (a -> m r) -> m r
- registryThread :: ResourceRegistry m -> ThreadId m
- withRegistry :: ( IOLike m, HasCallStack ) => ( ResourceRegistry m -> m a) -> m a
- data ResourceKey m
- allocate :: forall m a. ( IOLike m, HasCallStack ) => ResourceRegistry m -> (ResourceId -> m a) -> (a -> m ()) -> m ( ResourceKey m, a)
- allocateEither :: forall m e a. ( IOLike m, HasCallStack ) => ResourceRegistry m -> (ResourceId -> m ( Either e a)) -> (a -> m Bool ) -> m ( Either e ( ResourceKey m, a))
- release :: ( IOLike m, HasCallStack ) => ResourceKey m -> m ( Maybe (Context m))
- releaseAll :: ( IOLike m, HasCallStack ) => ResourceRegistry m -> m ()
- unsafeRelease :: IOLike m => ResourceKey m -> m ( Maybe (Context m))
- unsafeReleaseAll :: ( IOLike m, HasCallStack ) => ResourceRegistry m -> m ()
- cancelThread :: IOLike m => Thread m a -> m ()
- forkLinkedThread :: ( IOLike m, HasCallStack ) => ResourceRegistry m -> String -> m a -> m ( Thread m a)
- forkThread :: forall m a. ( IOLike m, HasCallStack ) => ResourceRegistry m -> String -> m a -> m ( Thread m a)
- linkToRegistry :: IOLike m => Thread m a -> m ()
- threadId :: Thread m a -> ThreadId m
- waitAnyThread :: forall m a. IOLike m => [ Thread m a] -> m a
- waitThread :: IOLike m => Thread m a -> m a
- withThread :: IOLike m => ResourceRegistry m -> String -> m a -> ( Thread m a -> m b) -> m b
- data Thread m a
-
data
TempRegistryException
=
forall
m.
IOLike
m =>
TempRegistryRemainingResource
{
- tempRegistryContext :: !(Context m)
- tempRegistryResource :: !(Context m)
- allocateTemp :: ( IOLike m, HasCallStack ) => m a -> (a -> m Bool ) -> (st -> a -> Bool ) -> WithTempRegistry st m a
- modifyWithTempRegistry :: forall m st a. IOLike m => m st -> (st -> ExitCase st -> m ()) -> StateT st ( WithTempRegistry st m) a -> m a
- runInnerWithTempRegistry :: forall innerSt st m res a. IOLike m => WithTempRegistry innerSt m (a, innerSt, res) -> (res -> m Bool ) -> (st -> res -> Bool ) -> WithTempRegistry st m a
- runWithTempRegistry :: ( IOLike m, HasCallStack ) => WithTempRegistry st m (a, st) -> m a
- data WithTempRegistry st m a
- closeRegistry :: ( IOLike m, HasCallStack ) => ResourceRegistry m -> m ()
- countResources :: IOLike m => ResourceRegistry m -> m Int
- unsafeNewRegistry :: ( IOLike m, HasCallStack ) => m ( ResourceRegistry m)
- data ResourceRegistry m
Documentation
data RegistryClosedException Source #
Attempt to allocate a resource in a registry which is closed
When calling
closeRegistry
(typically, leaving the scope of
withRegistry
), all resources in the registry must be released. If a
concurrent thread is still allocating resources, we end up with a race
between the thread trying to allocate new resources and the registry trying
to free them all. To avoid this, before releasing anything, the registry will
record itself as closed. Any attempt by a concurrent thread to allocate a new
resource will then result in a
RegistryClosedException
.
It is probably not particularly useful for threads to try and catch this
exception (apart from in a generic handler that does local resource cleanup).
The thread will anyway soon receive a
ThreadKilled
exception.
forall m. IOLike m => RegistryClosedException | |
|
Instances
data ResourceRegistryThreadException Source #
Registry used from untracked threads
If this exception is raised, it indicates a bug in the caller.
Creating and releasing the registry itself
bracketWithPrivateRegistry Source #
:: ( IOLike m, HasCallStack ) | |
=> ( ResourceRegistry m -> m a) | |
-> (a -> m ()) |
Release the resource |
-> (a -> m r) | |
-> m r |
Create a new private registry for use by a bracketed resource
Use this combinator as a more specific and easier-to-maintain alternative to the following.
'withRegistry' $ \rr -> 'bracket' (newFoo rr) closeFoo $ \foo -> (... rr does not occur in this scope ...)
NB The scoped body can use
withRegistry
if it also needs its own, separate
registry.
Use this combinator to emphasize that the registry is private to (ie only used by and/or via) the bracketed resource and that it thus has nearly the same lifetime. This combinator ensures the following specific invariants regarding lifetimes and order of releases.
o The registry itself is older than the bracketed resource.
o The only registered resources older than the bracketed resource were allocated in the registry by the function that allocated the bracketed resource.
o Because of the older resources, the bracketed resource is itself also registered in the registry; that's the only way we can be sure to release all resources in the right order.
NB Because the registry is private to the resource, the
a
type could save
the handle to
registry
and safely close the registry if the scoped body
calls
closeA
before the bracket ends. Though we have not used the type
system to guarantee that the interface of the
a
type cannot leak the
registry to the body, this combinator does its part to keep the registry
private to the bracketed resource.
See documentation of
ResourceRegistry
for a more general discussion.
registryThread :: ResourceRegistry m -> ThreadId m Source #
The thread that created the registry
withRegistry :: ( IOLike m, HasCallStack ) => ( ResourceRegistry m -> m a) -> m a Source #
Create a new registry
See documentation of
ResourceRegistry
for a detailed discussion.
Allocating and releasing regular resources
data ResourceKey m Source #
Resource key
Resource keys are tied to a particular registry.
Instances
Generic ( ResourceKey m) Source # | |
Defined in Ouroboros.Consensus.Util.ResourceRegistry from :: ResourceKey m -> Rep ( ResourceKey m) x Source # to :: Rep ( ResourceKey m) x -> ResourceKey m Source # |
|
IOLike m => NoThunks ( ResourceKey m) Source # | |
Defined in Ouroboros.Consensus.Util.ResourceRegistry |
|
type Rep ( ResourceKey m) Source # | |
Defined in Ouroboros.Consensus.Util.ResourceRegistry |
:: forall m a. ( IOLike m, HasCallStack ) | |
=> ResourceRegistry m | |
-> (ResourceId -> m a) | |
-> (a -> m ()) |
Release the resource |
-> m ( ResourceKey m, a) |
Allocate new resource
The allocation function will be run with asynchronous exceptions masked. This means that the resource allocation must either be fast or else interruptible; see "Dealing with Asynchronous Exceptions during Resource Acquisition" http://www.well-typed.com/blog/97/ for details.
:: forall m e a. ( IOLike m, HasCallStack ) | |
=> ResourceRegistry m | |
-> (ResourceId -> m ( Either e a)) | |
-> (a -> m Bool ) |
Release the resource, return
|
-> m ( Either e ( ResourceKey m, a)) |
Generalization of
allocate
for allocation functions that may fail
release :: ( IOLike m, HasCallStack ) => ResourceKey m -> m ( Maybe (Context m)) Source #
Release resource
This deallocates the resource and removes it from the registry. It will be the responsibility of the caller to make sure that the resource is no longer used in any thread.
The deallocation function is run with exceptions masked, so that we are guaranteed not to remove the resource from the registry without releasing it.
Releasing an already released resource is a no-op.
When the resource has not been released before, its context is returned.
releaseAll :: ( IOLike m, HasCallStack ) => ResourceRegistry m -> m () Source #
Release all resources in the
ResourceRegistry
without closing.
See
closeRegistry
for more details.
unsafeRelease :: IOLike m => ResourceKey m -> m ( Maybe (Context m)) Source #
Unsafe version of
release
The only difference between
release
and
unsafeRelease
is that the latter
does not insist that it is called from a thread that is known to the
registry. This is dangerous, because it implies that there is a thread with
access to a resource which may be deallocated before that thread is
terminated. Of course, we can't detect all such situations (when the thread
merely uses a resource but does not allocate or release we can't tell), but
normally when we
do
detect this we throw an exception.
This function should only be used if the above situation can be ruled out or handled by other means.
unsafeReleaseAll :: ( IOLike m, HasCallStack ) => ResourceRegistry m -> m () Source #
This is to
releaseAll
what
unsafeRelease
is to
release
: we do not
insist that this funciton is called from a thread that is known to the
registry. See
unsafeRelease
for why this is dangerous.
Threads
cancelThread :: IOLike m => Thread m a -> m () Source #
Cancel a thread
This is a synchronous operation: the thread will have terminated when this function returns.
Uses
uninterruptibleCancel
because that's what
withAsync
does.
:: ( IOLike m, HasCallStack ) | |
=> ResourceRegistry m | |
-> String |
Label for the thread |
-> m a | |
-> m ( Thread m a) |
Fork a thread and link to it to the registry.
This function is just a convenience.
:: forall m a. ( IOLike m, HasCallStack ) | |
=> ResourceRegistry m | |
-> String |
Label for the thread |
-> m a | |
-> m ( Thread m a) |
Fork a new thread
linkToRegistry :: IOLike m => Thread m a -> m () Source #
Link specified
Thread
to the (thread that created) the registry
waitThread :: IOLike m => Thread m a -> m a Source #
Wait for thread to terminate and return its result.
If the thread throws an exception, this will rethrow that exception.
NOTE: If A waits on B, and B is linked to the registry, and B throws an
exception, then A might
either
receive the exception thrown by B
or
the
ThreadKilled
exception thrown by the registry.
:: IOLike m | |
=> ResourceRegistry m | |
-> String |
Label for the thread |
-> m a | |
-> ( Thread m a -> m b) | |
-> m b |
Bracketed version of
forkThread
The analogue of
withAsync
for the registry.
Scoping thread lifetime using
withThread
is important when a parent
thread wants to link to a child thread /and handle any exceptions arising
from the link/:
let handleLinkException :: ExceptionInLinkedThread -> m () handleLinkException = .. in handle handleLinkException $ withThread registry codeInChild $ \child -> ..
instead of
handle handleLinkException $ do -- PROBABLY NOT CORRECT! child <- forkThread registry codeInChild ..
where the parent may exit the scope of the exception handler before the child terminates. If the lifetime of the child cannot be limited to the lifetime of the parent, the child should probably be linked to the registry instead and the thread that spawned the registry should handle any exceptions.
Note that in
principle
there is no problem in using
withAync
alongside a
registry. After all, in a pattern like
withRegistry $ \registry -> .. withAsync (.. registry ..) $ \async -> ..
the async will be cancelled when leaving the scope of
withAsync
and so
that reference to the registry, or indeed any of the resources inside the
registry, is safe. However, the registry implements a sanity check that the
registry is only used from known threads. This is useful: when a thread that
is not known to the registry (in other words, whose lifetime is not tied to
the lifetime of the registry) spawns a resource in that registry, that
resource may well be deallocated before the thread terminates, leading to
undefined and hard to debug behaviour (indeed, whether or not this results in
problems may well depend on precise timing); an exception that is thrown when
allocating
the resource is (more) deterministic and easier to debug.
Unfortunately, it means that the above pattern is not applicable, as the
thread spawned by
withAsync
is not known to the registry, and so if it were
to try to use the registry, the registry would throw an error (even though
this pattern is actually safe). This situation is not ideal, but for now we
merely provide an alternative to
withAsync
that
does
register the thread
with the registry.
NOTE: Threads that are spawned out of the user's control but that must still make use of the registry can use the unsafe API. This should be used with caution, however.
opaque
Thread
The internals of this type are not exported.
Temporary registry
data TempRegistryException Source #
When
runWithTempRegistry
exits successfully while there are still
resources remaining in the temporary registry that haven't been transferred
to the final state.
forall m. IOLike m => TempRegistryRemainingResource | |
|
Instances
:: ( IOLike m, HasCallStack ) | |
=> m a |
Allocate the resource |
-> (a -> m Bool ) |
Release the resource, return
Note that it is safe to always return
|
-> (st -> a -> Bool ) |
Check whether the resource is in the given state |
-> WithTempRegistry st m a |
Allocate a resource in a temporary registry until it has been transferred
to the final state
st
. See
runWithTempRegistry
for more details.
modifyWithTempRegistry Source #
:: forall m st a. IOLike m | |
=> m st |
Get the state |
-> (st -> ExitCase st -> m ()) |
Store the new state |
-> StateT st ( WithTempRegistry st m) a |
Modify the state |
-> m a |
Higher level API on top of
runWithTempRegistry
: modify the given
st
,
allocating resources in the process that will be transferred to the
returned
st
.
runInnerWithTempRegistry Source #
:: forall innerSt st m res a. IOLike m | |
=> WithTempRegistry innerSt m (a, innerSt, res) |
The embedded computation; see ASSUMPTION above |
-> (res -> m Bool ) |
How to free; same as for
|
-> (st -> res -> Bool ) |
How to check; same as for
|
-> WithTempRegistry st m a |
Embed a self-contained
WithTempRegistry
computation into a larger one.
The internal
WithTempRegistry
is effectively passed to
runWithTempRegistry
. It therefore must have no dangling resources, for
example. This is the meaning of
self-contained
above.
The key difference beyond
runWithTempRegistry
is that the resulting
composite resource is also guaranteed to be registered in the outer
WithTempRegistry
computation's registry once the inner registry is closed.
Combined with the following assumption, this establishes the invariant that
all resources are (transitively) in a temporary registry.
As the resource might require some implementation details to be closed, the function to close it will also be provided by the inner computation.
ASSUMPTION: closing
res
closes every resource contained in
innerSt
NOTE: In the current implementation, there will be a brief moment where the inner registry still contains the inner computation's resources and also the outer registry simultaneously contains the new composite resource. If an async exception is received at that time, then the inner resources will be closed and then the composite resource will be closed. This means there's a risk of double freeing , which can be harmless if anticipated.
runWithTempRegistry :: ( IOLike m, HasCallStack ) => WithTempRegistry st m (a, st) -> m a Source #
Run an action with a temporary resource registry.
When allocating resources that are meant to end up in some final state,
e.g., stored in a
TVar
, after which they are guaranteed to be released
correctly, it is possible that an exception is thrown after allocating such
a resource, but before it was stored in the final state. In that case, the
resource would be leaked.
runWithTempRegistry
solves that problem.
When no exception is thrown before the end of
runWithTempRegistry
, the
user must have transferred all the resources it allocated to their final
state. This means that these resources don't have to be released by the
temporary registry anymore, the final state is now in charge of releasing
them.
In case an exception is thrown before the end of
runWithTempRegistry
,
all
resources allocated in the temporary registry will be released.
Resources must be allocated using
allocateTemp
.
To make sure that the user doesn't forget to transfer a resource to the
final state
st
, the user must pass a function to
allocateTemp
that
checks whether a given
st
contains the resource, i.e., whether the
resource was successfully transferred to its final destination.
When no exception is thrown before the end of
runWithTempRegistry
, we
check whether all allocated resources have been transferred to the final
state
st
. If there's a resource that hasn't been transferred to the final
state
and
that hasn't be released or closed before (see the release
function passed to
allocateTemp
), a
TempRegistryRemainingResource
exception will be thrown.
For that reason,
WithTempRegistry
is parameterised over the final state
type
st
and the given
WithTempRegistry
action must return the final
state.
NOTE: we explicitly don't let
runWithTempRegistry
return the final state,
because the state
must
have been stored somewhere safely, transferring
the resources, before the temporary registry is closed.
opaque
data WithTempRegistry st m a Source #
An action with a temporary registry in scope, see
runWithTempRegistry
for more details.
The most important function to run in this monad is
allocateTemp
.
Instances
Combinators primarily for testing
closeRegistry :: ( IOLike m, HasCallStack ) => ResourceRegistry m -> m () Source #
Close the registry
This can only be called from the same thread that created the registry. This is a no-op if the registry is already closed.
This entire function runs with exceptions masked, so that we are not interrupted while we release all resources.
Resources will be allocated from young to old, so that resources allocated later can safely refer to resources created earlier.
The release functions are run in the scope of an exception handler, so that if releasing one resource throws an exception, we still attempt to release the other resources. Should we catch an exception whilst we close the registry, we will rethrow it after having attempted to release all resources. If there is more than one, we will pick a random one to rethrow, though we will prioritize asynchronous exceptions over other exceptions. This may be important for exception handlers that catch all-except-asynchronous exceptions.
countResources :: IOLike m => ResourceRegistry m -> m Int Source #
Number of currently allocated resources
Primarily for the benefit of testing.
unsafeNewRegistry :: ( IOLike m, HasCallStack ) => m ( ResourceRegistry m) Source #
Create a new registry
You are strongly encouraged to use
withRegistry
instead.
Exported primarily for the benefit of tests.
opaque
data ResourceRegistry m Source #
Resource registry
Note on terminology: when thread A forks thread B, we will say that thread A is the " parent " and thread B is the " child ". No further relationship between the two threads is implied by this terminology. In particular, note that the child may outlive the parent. We will use "fork" and "spawn" interchangeably.
Motivation
Whenever we allocate resources, we must keep track of them so that we can
deallocate them when they are no longer required. The most important tool we
have to achieve this is
bracket
:
bracket allocateResource releaseResource $ \r -> .. use r ..
Often
bracket
comes in the guise of a with-style combinator
withResource $ \r -> .. use r ..
Where this pattern is applicable, it should be used and there is no need to
use the
ResourceRegistry
. However,
bracket
introduces strict lexical
scoping: the resource is available inside the scope of the bracket, and
will be deallocated once we leave that scope. That pattern is sometimes
hard to use.
For example, suppose we have this interface to an SQL server
query :: Query -> IO QueryHandle close :: QueryHandle -> IO () next :: QueryHandle -> IO Row
and suppose furthermore that we are writing a simple webserver that allows a client to send multiple SQL queries, get rows from any open query, and close queries when no longer required:
server :: IO () server = go Map.empty where go :: Map QueryId QueryHandle -> IO () go handles = getRequest >>= \case New q -> do h <- query q -- allocate qId <- generateQueryId sendResponse qId go $ Map.insert qId h handles Close qId -> do close (handles ! qId) -- release go $ Map.delete qId handles Next qId -> do sendResponse =<< next (handles ! qId) go handles
The server opens and closes query handles in response to client requests.
Restructuring this code to use
bracket
would be awkward, but as it stands
this code does not ensure that resources get deallocated; for example, if
the server thread is killed (
killThread
), resources will be leaked.
Another, perhaps simpler, example is spawning threads. Threads too should be considered to be resources that we should keep track of and deallocate when they are no longer required, primarily because when we deallocate (terminate) those threads they too will have a chance to deallocate their resources. As for other resources, we have a with-style combinator for this
withAsync $ \thread -> ..
Lexical scoping of threads is often inconvenient, however, more so than for regular resources. The temptation is therefore to simply fork a thread and forget about it, but if we are serious about resource deallocation this is not an acceptable solution.
The resource registry
The resource registry is essentially a piece of state tracking which
resources have been allocated. The registry itself is allocated with a
with-style combinator
withRegistry
, and when we leave that scope any
resources not yet deallocated will be released at that point. Typically
the registry is only used as a fall-back, ensuring that resources will
deallocated even in the presence of exceptions. For example, here's how
we might rewrite the above server example using a registry:
server' :: IO () server' = withRegistry $ \registry -> go registry Map.empty where go :: ResourceRegistry IO -> Map QueryId (ResourceKey, QueryHandle) -> IO () go registry handles = getRequest >>= \case New q -> do (key, h) <- allocate registry (query q) close -- allocate qId <- generateQueryId sendResponse qId go registry $ Map.insert qId (key, h) handles Close qId -> do release registry (fst (handles ! qId)) -- release go registry $ Map.delete qId handles Next qId -> do sendResponse =<< next (snd (handles ! qId)) go registry handles
We allocate the query with the help of the registry, providing the registry with the means to deallocate the query should that be required. We can /and should/ still manually release resources also: in this particular example, the (lexical) scope of the registry is the entire server thread, so delaying releasing queries until we exit that scope will probably mean we hold on to resources for too long. The registry is only there as a fall-back.
Spawning threads
We already observed in the introduction that insisting on lexical scoping
for threads is often inconvenient, and that simply using
fork
is no
solution as it means we might leak resources. There is however another
problem with
fork
. Consider this snippet:
withRegistry $ \registry -> r <- allocate registry allocateResource releaseResource fork $ .. use r ..
It is easy to see that this code is problematic: we allocate a resource
r
,
then spawn a thread that uses
r
, and finally leave the scope of
withRegistry
, thereby deallocating
r
-- leaving the thread to run with
a now deallocated resource.
It is
only
safe for threads to use a given registry, and/or its registered
resources, if the lifetime of those threads is tied to the lifetime of the
registry. There would be no problem with the example above if the thread
would be terminated when we exit the scope of
withRegistry
.
The
forkThread
combinator provided by the registry therefore does two
things: it allocates the thread as a resource in the registry, so that it can
kill the thread when releasing all resources in the registry. It also records
the thread ID in a set of known threads. Whenever the registry is accessed
from a thread
not
in this set, the registry throws a runtime exception,
since such a thread might outlive the registry and hence its contents. The
intention is that this guards against dangerous patterns like the one above.
Linking
When thread A spawns thread B using
withAsync
, the lifetime of B is tied
to the lifetime of A:
withAsync .. $ \threadB -> ..
After all, when A exits the scope of the
withAsync
, thread B will be
killed. The reverse is however not true: thread B can terminate before
thread A. It is often useful for thread A to be able to declare a dependency
on thread B: if B somehow fails, that is, terminates with an exception, we
want that exception to be rethrown in thread A as well. A can achieve this
by
linking
to B:
withAsync .. $ \threadB -> do link threadB ..
Linking a parent to a child is however of limited value if the lifetime of the child is not limited by the lifetime of the parent. For example, if A does
threadB <- async $ .. link threadB
and A terminates before B does, any exception thrown by B might be send to a thread that no longer exists. This is particularly problematic when we start chaining threads: if A spawns-and-links-to B which spawns-and-links-to C, and C throws an exception, perhaps the intention is that this gets rethrown to B, and then rethrown to A, terminating all three threads; however, if B has terminated before the exception is thrown, C will throw the exception to a non-existent thread and A is never notified.
For this reason, the registry's
linkToRegistry
combinator does not link the
specified thread to the thread calling
linkToRegistry
, but rather to the
thread that created the registry. After all, the lifetime of threads spawned
with
forkThread
can certainly exceed the lifetime of their parent threads,
but the lifetime of
all
threads spawned using the registry will be limited
by the scope of that registry, and hence the lifetime of the thread that
created it. So, when we call
linkToRegistry
, the exception will be thrown
the thread that created the registry, which (if not caught) will cause that
that to exit the scope of
withRegistry
, thereby terminating all threads in
that registry.
# Combining the registry and with-style allocation
It is perfectly possible (indeed, advisable) to use
bracket
and
bracket-like allocation functions alongside the registry, but note that the
usual caveats with
bracket
and forking threads still applies. In
particular, spawning threads inside the
bracket
that make use of the
bracketed resource is problematic; this is of course true whether or not a
registry is used.
In principle this also includes
withAsync
; however, since
withAsync
results in a thread that is not known to the registry, such a thread will not
be able to use the registry (the registry would throw an unknown thread
exception, as described above). For this purpose we provide
withThread
;
withThread
(as opposed to
forkThread
) should be used when a parent thread
wants to handle exceptions in the child thread; see
withThread
for
detailed discussion.
It is
also
fine to includes nested calls to
withRegistry
. Since the
lifetime of such a registry (and all resources within) is tied to the thread
calling
withRegistry
, which itself is tied to the "parent registry" in
which it was created, this creates a hierarchy of registries. It is of course
essential for compositionality that we should be able to create local
registries, but even if we do have easy access to a parent regisry, creating
a local one where possibly is useful as it limits the scope of the resources
created within, and hence their maximum lifetimes.
Instances
Generic ( ResourceRegistry m) Source # | |
Defined in Ouroboros.Consensus.Util.ResourceRegistry from :: ResourceRegistry m -> Rep ( ResourceRegistry m) x Source # to :: Rep ( ResourceRegistry m) x -> ResourceRegistry m Source # |
|
IOLike m => NoThunks ( ResourceRegistry m) Source # | |
Defined in Ouroboros.Consensus.Util.ResourceRegistry |
|
type Rep ( ResourceRegistry m) Source # | |
Defined in Ouroboros.Consensus.Util.ResourceRegistry |