Component
1 Introduction
1.1 Guide
1.2 Limitations
2 Reference
2.1 Components
gen:  component
component?
component-start
component-stop
2.2 Systems
system?
make-system
define-system
system-start
system-stop
system-ref
system-replace
system->dot
system->png
2.3 The Escape Hatch
current-system
2.4 Testing
system-test-suite
3 Acknowledgments
7.8

Component

Bogdan Popa <bogdan@defn.io>

 (require component) package: component-lib

1 Introduction

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.

1.1 Guide

Let’s assume that you’re writing a web application that emails users when they sign up. Your components might be:

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.

1.2 Limitations

Components that have no dependencies and no dependents are never started.

2 Reference

2.1 Components

Components are plain structs that implement the gen:component interface. All that’s required of a component is that it needs to know how to start and then stop itself.

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)

#f

> (mailer-started? (component-start m))

#t

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% ...))

interface

gen:component

The generic interface that components must implement.

procedure

(component? v)  boolean?

  v : any/c
Returns #t when v is a component.

procedure

(component-start c)  component?

  c : component?
Starts c.

procedure

(component-stop c)  component?

  c : component?
Stops c.

2.2 Systems

Systems group components together according to a declarative specification.

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.

procedure

(system? v)  boolean?

  v : any/c
Returns #t if v is a system.

procedure

(make-system spec)  system?

  spec : 
(listof (or/c (list/c symbol? any/c)
              (list/c symbol? (listof symbol?) any/c)))
Creates a system according to the given specification, but does not start it. A user error is raised if the spec contains any circular dependencies between components.

syntax

(define-system id component ...+)

 
component = [component-id factory-expr]
  | [component-id (dependency-id ...) factory-expr]
 
  factory-expr : (-> any/c ... any/c)
Combines define and make-system. -system is appended to the given id so

(define-system prod
  [db make-db]
  [app (db) make-app])

defines a system called prod-system.

procedure

(system-start s)  void?

  s : system?
Starts s. Parameterizes current-system so that it refers to s before any components are started.

procedure

(system-stop s)  void?

  s : system?
Stops s.

procedure

(system-ref id)  any/c

  id : symbol?
(system-ref s id)  any/c
  s : system?
  id : symbol?
Get a component by id from a system. Raises exn:fail if called before the system is started or if id refers to a nonexistent component.

The first variant attempts to look up id from the current-system, failing if one isn’t installed.

procedure

(system-replace s id factory)  system?

  s : system?
  id : symbol?
  factory : any/c
Returns a stopped copy of s with the factory for the id component replaced by factory. This is useful if you have a large system and you want to replace one of its components with a stub (eg. for a web app’s end-to-end tests).

procedure

(system->dot s)  string?

  s : system?
Generate dot notation representing a system’s dependency graph.

procedure

(system->png s output-path)  boolean?

  s : system?
  output-path : path-string?
Generate a PNG of a system’s dependency graph. Requires Graphviz to be installed and its dot command to be available on the system PATH.

2.3 The Escape Hatch

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.

parameter

(current-system)  (or/c false/c system?)

(current-system s)  void?
  s : (or/c false/c system?)

2.4 Testing

 (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.

syntax

(system-test-suite id (component ...+)
  maybe-before
  maybe-after
  body-expr ...+)
 
component = 
(component-id factory-expr)
(component-id (dependency-id ...) factory-expr)
     
maybe-before = 
  | #:before before-expr
     
maybe-after = 
  | #:after after-expr
Produces a test-suite that is associated with a custom system made up of the specified components. The system is started before the test suite runs and stopped after it finishes. During startup, each individual component is bound to an identifier in the scope of the test suite so that tests may easily reference the components.

> (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

0

3 Acknowledgments

This library draws inspiration from Stuart Sierra’s "component" library for Clojure.