|(require component)||package: component-lib|
This library helps you manage the life-cycle of stateful components in your application. It ensures that they are started, linked together and, finally, stopped in the correct order.
By writing programs in this style, you trade some flexibility for clarity around how and when the individual parts of your application are initialized.
Let’s assume that you’re writing a web application that emails users when they sign up. Your components might be:
the user manager, which depends on the database, and
the http frontend, which depends on the mailer and the user manager.
Given those components, your system might look like this:
(define-system prod [db make-database] [mailer make-mailer] [users (db) (lambda (db) (make-user-manager db))] [http (mailer users) (lambda (m um) (make-http m um))]) (system-start prod-system) (system-stop prod-system)
The system is made up of a list of component declarations where each one is made up of the unique id of a component in the system, an optional list of dependencies (other component ids) and a function that can be used to construct that component from its dependencies. There are no constraints on the ids of the components in the system and you can have multiple components of the same type (for example, read-only and read-write databases).
The define-system form creates a value that represents the system and its internal dependency graph but does not start it.
The call to system-start starts the db and the mailer first (one or the other may be started first since neither has any dependencies), then the user-manager and finally the http server.
Finally, the call to system-stop stops all the components in the system in the reverse order that they were started in.
Components that have no dependencies and no dependents are never started.
Here’s a component that doesn’t do anything except flip a flag when it gets started and stopped:
> (struct mailer (started?) #:transparent #:methods gen:component [(define (component-start _) (mailer #t)) (define (component-stop _) (mailer #f))])
> (define m (mailer #f)) > (mailer-started? m)
> (mailer-started? (component-start m))
And here’s what a component that encapsulates a database connection pool might look like:
> (struct db (connector custodian pool) #:transparent #:methods gen:component [(define (component-start the-db) (define custodian (make-custodian)) (struct-copy db the-db [custodian custodian] [pool (parameterize ([current-custodian custodian]) (connection-pool (db-connector the-db)))])) (define (component-stop the-db) (custodian-shutdown-all (db-custodian the-db)) (struct-copy db the-db [custodian #f] [pool #f]))])
> (define (make-db connector) (db connector #f #f))
> (component-start (make-db (lambda () (sqlite3-connect #:database 'temporary))))
(db #<procedure> #<custodian> (object:connection-pool% ...))
When a system is started, its components are started in dependency order (if a depends on b which depends on c then c is started first, then b then a) and injected into their dependents’ factory functions (c is passed to b which is finally passed to a).
When a system is stopped, its components are stopped in the reverse order that they were started in.
(define-system id component ...+)
component = [component-id factory-expr] | [component-id (dependency-id ...) factory-expr]
factory-expr : (-> any/c ... any/c)
(define-system prod [db make-db] [app (db) make-app])
defines a system called prod-system.
The first variant attempts to look up id from the current-system, failing if one isn’t installed.
Manually passing components around can be painful in highly-dynamic applications so the library provides an escape hatch for those use-cases. When a system is started, current-system is parameterized to point to the system itself. That way, components’ start and stop functions as well as any threads started by components are able to directly reference the system they are a part of.
(current-system s) → void? s : (or/c false/c system?)
|(require component/testing)||package: component-lib|
When integration testing components, you often need to put together a subset of components, start them up and reference them from within your tests. The system-test-suite form provides a convenient way to do this.
(system-test-suite id (component ...+) maybe-before maybe-after body-expr ...+)
(component-id factory-expr) (component-id (dependency-id ...) factory-expr) maybe-before =
| #:before before-expr maybe-after =
| #:after after-expr
> (struct mailer () #:methods gen:component )
> (struct app (mailer) #:methods gen:component )
> (run-tests (system-test-suite app ([a (m) app] [m mailer]) (test-case "instantiated" (check-true (system? app-system)) (check-true (app? a)) (check-true (mailer? (app-mailer a))))))
1 success(es) 0 failure(s) 0 error(s) 1 test(s) run
This library draws inspiration from Stuart Sierra’s "component" library for Clojure.