File: Defining-Services.md

package info (click to toggle)
trapperkeeper-clojure 4.0.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 964 kB
  • sloc: sh: 189; xml: 73; makefile: 25; java: 5
file content (171 lines) | stat: -rw-r--r-- 8,366 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# Defining Services

Trapperkeeper provides two constructs for defining services: `defservice` and `service`.  As you might expect, `defservice` defines a service as a var in your namespace, and `service` allows you to create one inline and assign it to a variable in a let block or other location.  Here's how they work:

## `defservice`

`defservice` takes the following arguments:

* a service name
* an optional doc string
* an optional service protocol; only required if your service exports functions that can be used by other services
* a dependency list indicating other services/functions that this service requires
* a series of function implementations.  This must include all of the functions in the protocol if one is specified, and may also optionally provide override implementations for the built-in service `Lifecycle` functions.

### Service Lifecycle

The service `Lifecycle` protocol looks like this:

```clj
(defprotocol Lifecycle
  (init [this context])
  (start [this context])
  (stop [this context]))
```

(This may look familiar; we chose to use the same function names as some of the existing lifecycle protocols.  Ultimately we'd like to just use one of those protocols directly, but for now our needs are different enough to warrant avoiding the introduction of a dependency on an existing project.)

All service lifecycle functions are passed a service `context` map, which may be used to store any service-specific state (e.g., a database connection pool or some other object that you need to reference in subsequent functions.)  Services may define these functions, `assoc` data into the map as needed, and then return the updated context map.  The updated context map will be maintained by the framework and passed to subsequent lifecycle functions for the service.

The default implementation of the lifecycle functions is to simply return the service context map unmodified; if you don't need to implement a particular lifecycle function for your service, you can simply omit it and the default will be used.

Trapperkeeper will call the lifecycle functions in order based on the dependency list of the services; in other words, if your service has a dependency on service `Foo`, you are guaranteed that `Foo`'s `init` function will be called prior to yours, and that your `stop` function will be called prior to `Foo`'s.

### Example Service

Let's look at a concrete example:

```clj
;; This is the list of functions that the `FooService` must implement, and which
;; are available to other services who have a dependency on `FooService`.
(defprotocol FooService
  (foo1 [this x])
  (foo2 [this])
  (foo3 [this x]))

(defservice foo-service
   ;; docstring (optional)
   "A service that foos."

   ;; now we specify the (optional) protocol that this service satisfies:
   FooService

   ;; the :depends value should be a vector of vectors.  Each of the inner vectors
   ;; should begin with a keyword that matches the protocol name of another service,
   ;; which may be followed by any number of symbols.  Each symbol is the name of a
   ;; function that is provided by that service.  Trapperkeeper will fail fast at
   ;; startup if any of the specified dependency services do not exist, *or* if they
   ;; do not provide all of the functions specified in your vector.  (Note that
   ;; the syntax used here is actually just the
   ;; [fnk binding syntax from the Plumatic plumbing library](https://github.com/plumatic/plumbing/tree/master/src/plumbing/fnk#fnk-syntax),
   ;; so you can technically use any form that is compatible with that.)
   [[:SomeService function1 function2]
    [:AnotherService function3 function4]]

   ;; After your dependencies list comes the function implementations.
   ;; You must implement all of the protocol functions (if a protocol is
   ;; specified), and you may also override any `Lifecycle` functions that
   ;; you choose.  We'll start by implementing the `init` function from
   ;; the `Lifecycle`:
   (init [this context]
      ;; do some initialization
      ;; ...
      ;; now return the service context map; we can update it to include
      ;; some state if we like.  Note that we can use the functions that
      ;; were specified in our dependency list here:
      (assoc context :foo (str "Some interesting state:" (function1)))

   ;; We could optionally also override the `start` and `stop` lifecycle
   ;; functions, but we won't for this example.

   ;; Now we'll define our service function implementations.  Again, we are
   ;; free to use the imported functions from the other services here:
   (foo1 [this x] ((comp function2 function3) x))
   (foo2 [this] (println "Function4 returns" (function4)))

   ;; We can also access the service context that we updated during the
   ;; lifecycle functions, by using the `service-context` function from
   ;; the `Service` protocol:
   (foo3 [this x]
     (let [context (service-context this)]
       (format "x + :foo is: '%s'" (str x (:foo context))))))
```

After this `defservice` statement, you will have a var named `foo-service` in your namespace that contains the service.  You can reference this from a Trapperkeeper bootstrap configuration file to include that service in your app, and once you've done that your new service can be referenced as a dependency (`{:depends [[:FooService ...`) by other services.

### Multi-arity Protocol Functions

Clojure's protocols allow you to define multi-arity functions:

```clj
(defprotocol MultiArityService
   (foo [this x] [this x y]))
```

Trapperkeeper services can use the syntax from clojure's `reify` to implement these multi-arity functions:

```clj
(defservice my-service
   MultiArityService
   []
   (foo [this x] x)
   (foo [this x y] (+ x y)))
```

## `service`

`service` works very similarly to `defservice`, but it doesn't define a var in your namespace; it simply returns the service instance.  Here are some examples (with and without protocols):

```clj
(service
   []
   (init [this context]
     (println "Starting anonymous service!")
     context))

(defprotocol AnotherService
   (foo [this]))
```

## Optional Services

_Introduced in Trapperkeeper 1.2.0_

When defining a service, it is possible to mark certain other services your service depends on as being optional. This is useful, for example, when composing your service against services that you might not need during development or for certain deployment scenarios. You can write the same code whether or not an optional service has been included in your bootstrap.cfg or not.

To mark a dependency as optional, you use a different form to specify your dependencies:

```clj
(defprotocol HaikuService
  (get-haiku [this] "return a lovely haiku"))
(defprotocol SonnetService
  (get-sonnet [this] "return a lovely sonnet"))

;; ... snip the definitions of HaikuService and SonnetService ...

(defservice poetry-service
  PoetryService
  {:required [HaikuService]
   :optional [SonnetService]}
  (haiku [this]
    (get-haiku HaikuService))
  (sonnet [this]
    (if-let [sonnet-svc (tk-svc/maybe-get-service this :SonnetService)]
      (get-sonnet sonnet-svc)
      "insert moving sonnet here"))
```

In the above example, we use a map of the form `{:required [...] :optional [...]}` to split up our required and optional dependencies. When we run this service in TK, our code will call `(get-sonnet)` if an implementation of `SonnetService` has been included in the `bootstrap.cfg`. Otherwise, we'll return the placeholder string `"insert moving sonnet here"`.

**Warning** Because of a [limitation](https://github.com/plumatic/plumbing/issues/114) in Plumatic Schema, you can't use the destructuring `[:SonnetService get-sonnet]` syntax when declaring optional dependencies.

The `Service` protocol has two helpers to make it easier to work with optional dependencies:

* `(maybe-get-service [this service-id])` which takes a keyword service ID and returns the service, if included, or nil
* `(service-included? [this service-id])` which takes a keyword service ID and returns true or false based on its inclusion.

These helpers live alongside the other service helpers like `get-service` in `puppetlabs.trapperkeeper.services`.

## Referencing Services

To learn how to refer to services in the rest of your application, head over to the [Referencing Services](Referencing-Services.md) page.