Copyright | (c) Roman Gonzalez 2017-2018 |
---|---|
License | MIT |
Maintainer | open-source@roman-gonzalez.info |
Stability | experimental |
Safe Haskell | None |
Language | Haskell2010 |
Why use ComponentM
?
ComponentM
values wraps vanilla IO
sub-routines whose responsibility is to
allocate resources that your application may need (e.g. database connections,
tcp sockets, etc). Your program will execute these ComponentM
sub-routines at
the beginning of it's lifecyle, building an environment that your main
application needs in order to work as intended.
By using ComponentM
sub-routines your program will automatically:
- Compose the cleanup sub-routines of all your allocated resources
- Keep track of initialization time for each resource needed in your application
- Keep track of teardown time for each resources needed in your application.
- Isolate the teardown of each resource in your application, ensuring no thrown exception will affect the cleanup of resources.
- Initialize resources concurrently when using
Applicative
notation - Build a dependency graph of your application resources when using
Applicative
orMonad
notation; and then guarantees the execution of cleanup operations in a topological sorted order - Make sure that previously allocated resources are cleaned up when a resource throws an exception on initialization
- Report all exceptions thrown on each resource teardown
- Document (through types) what is the purpose of some of the
IO
sub-routines in your program
These properties are crucial when applications need to run for long periods of time and they are reloaded (without a process restart). It also ensures that resources are cleaned tightly when doing REPL driven development through GHCi.
- buildComponent :: Text -> IO a -> (a -> IO ()) -> ComponentM a
- buildComponent_ :: Text -> IO a -> ComponentM a
- data ComponentM a
- runComponentM :: Text -> ComponentM a -> (a -> IO b) -> IO b
- runComponentM1 :: (ComponentEvent -> IO ()) -> Text -> ComponentM a -> (a -> IO b) -> IO b
- data ComponentError
- data ComponentBuildError
- = DuplicatedComponentKeyDetected !Description
- | ComponentAllocationFailed !Description !SomeException
- | ComponentErrorThrown !SomeException
- | ComponentIOLiftFailed !SomeException
- data ComponentEvent
- data Build
- buildElapsedTime :: Build -> NominalDiffTime
- buildFailure :: Build -> Maybe SomeException
- data BuildResult
- toBuildList :: BuildResult -> [Build]
- data TeardownResult :: *
- = BranchResult {
- resultDescription :: !Description
- resultElapsedTime :: !NominalDiffTime
- resultDidFail :: !Bool
- resultListing :: ![TeardownResult]
- | LeafResult {
- resultDescription :: !Description
- resultElapsedTime :: !NominalDiffTime
- resultError :: !(Maybe SomeException)
- | EmptyResult {
- resultDescription :: !Description
- = BranchResult {
- throwM :: MonadThrow m => forall e a. Exception e => e -> m a
How to build ComponentM
values
ComponentM
values are built from vanilla IO
sub-routines that allocate resources, the
two functions provided are:
buildComponent_
- Used when a component in your application does not allocate a resource
buildComponent
- Used when a component in your application allocates a resource and requires a cleanup on teardown
Following is an example on how to
import "sqlite-simple" qualified Database.SQLite.Simple as SQLite import "componentm" Control.Monad.Component (ComponentM, buildComponent, buildComponent_) -- | App environment record data AppEnv = AppEnv { appDb :: !SQLite.Connection } -- | Configuration record data Config = Config { dbPath :: !String } readConfig :: IO Config readConfig = -- NOTE: Here we would have a more sophisticated algorithm for fetching -- configuration values for our app return (Config ":memory:") configComponent :: ComponentM AppConfig configComponent = buildComponent_ Config $ do readConfigFile ".resourcesconfig.yml" dbComponent :: FilePath -> ComponentM SQLite.Connection dbComponent dbPath = buildComponent Database (SQLite.open dbPath) SQLite.close buildAppEnv :: ComponentM AppEnv buildAppEnv = do config <- configComponent AppEnv $ dbComponent (dbPath config)
In the previous example, we use both buildComponent_
and buildComponent
to
create different components that our application needs.
:: Text | Unique name for the component being allocated |
-> IO a | Allocation |
-> (a -> IO ()) | Cleanup |
-> ComponentM a |
Use this function when you want to allocate a new resource (e.g. Database, Socket, etc). It registers the constructed resource in your application component tree and guarantees that its cleanup sub-routine is executed at the end of your program.
This function is similar to the bracket
function with the caveat that it
expects a Text
argument which identifies the component being allocated.
NOTE: The name of your component must be unique; otherwise a
DuplicatedComponentKeyDetected
will be thrown
buildComponent_ :: Text -> IO a -> ComponentM a Source #
Transforms an IO
sub-routine into a ComponentM
sub-routine; the given
IO
sub-routine returns a resource that does not allocate any other
resources that would need to be cleaned up on a system shutdown.
This is similar to using liftIO
, with the caveat that the library will
register the given IO
sub-routine as a Component, and it will keep track
and report its initialization timespan
Making ComponentM
values useful
data ComponentM a Source #
Represents the construction of a Component in your application, components
may be composed using a Monad
or Applicative
interface.
:: Text | Name of your application (used for tracing purposes) |
-> ComponentM a | Builder of your application environment |
-> (a -> IO b) | Function where your main application will live |
-> IO b |
Constructs the environment of your application by executing the IO
sub-routines provided in the buildComponent
and buildComponent_
functions; it then executes a callback where your main application will run.
This function:
- Keeps track of initialization elapsed time for each component of your application
- Initializes components concurrently as long as they are composed using
Applicative
functions - Builds a graph of your dependencies automatically when composing your
ComponentM
values viaApplicative
orMonad
interfaces; it then guarantees the execution of cleanup operations in a topological sorted order - Guarantees the proper cleanup of previously allocated resources if the creation of a resource throws an exception on initialization
- Guarantees best-effort cleanup of resources on application teardown in the scenario where a cleanup sub-routine throws an exception
- Keeps track of teardown elasped time for each component of your application; and reports what exceptions was thrown in case of failures
If you want to trace the behavior of your application on initialization and
teardown, use runComponentM1
instead
:: (ComponentEvent -> IO ()) | Callback function to trace |
-> Text | Name of your application (used for tracing purposes) |
-> ComponentM a | Builder of your application environment |
-> (a -> IO b) | Function where your main application will live |
-> IO b |
Enhances runComponentM
with a callback function that emits
ComponentEvent
records. These events are a great way of tracing the
lifecycle and structure of your application.
Error Records
There are two possible failures that the runComponentM
functions can thrown
ComponentBuildFailed
- This error happens when allocation of some component's resource fails
ComponentRuntimeFailed
- This error happens when there is an exception thrown from our main application callback
data ComponentError Source #
Exception thrown by the runComponentM
family of functions
ComponentRuntimeFailed | Failure raised when the Application Callback given to a |
| |
ComponentBuildFailed | Failure raised when execution of |
|
data ComponentBuildError Source #
Exception raised on the execution of IO
sub-routines used when
constructing ComponentM
values (e.g. buildComponent
)
DuplicatedComponentKeyDetected !Description | Failure thrown when using the same component key on a Component composition |
ComponentAllocationFailed !Description !SomeException | Failure thrown when the allocation sub-routine of a Component fails with an exception |
ComponentErrorThrown !SomeException | Failure thrown when calling the |
ComponentIOLiftFailed !SomeException | Failure thrown when calling |
ComponentM
tracing accessors
data ComponentEvent Source #
An event record used to trace the execution of an application initialization and teardown
Contains metadata about the build of a resource from a ComponentM
value
buildElapsedTime :: Build -> NominalDiffTime Source #
Elasped time in the allocation of a component resource
buildFailure :: Build -> Maybe SomeException Source #
Error thrown in the allocation of a component resource
data BuildResult Source #
Wraps a collection of Build
records
toBuildList :: BuildResult -> [Build] Source #
Re-exports
data TeardownResult :: * #
Result from a Teardown
sub-routine
BranchResult | Result is composed by multiple teardown sub-routines |
| |
LeafResult | Result represents a single teardown sub-routine |
| |
EmptyResult | Represents a stub cleanup operation (for lifting pure values) |
|
throwM :: MonadThrow m => forall e a. Exception e => e -> m a #
Throw an exception. Note that this throws when this action is run in
the monad m
, not when it is applied. It is a generalization of
Control.Exception's throwIO
.
Should satisfy the law:
throwM e >> f = throwM e