Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make defservice be implemented with metadata-based protocol extension #1

Open
wants to merge 4 commits into
base: 3.0.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/Built-in-Configuration-Service.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Here's the protocol for the configuration service:

```clj
(defprotocol ConfigService
:extend-via-metadata true
(get-config [this] "Returns a map containing all of the configuration values")
(get-in-config [this ks] [this ks default]
"Returns the individual configuration value from the nested
Expand Down
1 change: 1 addition & 0 deletions documentation/Built-in-Shutdown-Service.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ The shutdown service provides two functions that can be injected into other serv

```clj
(defprotocol ShutdownService
:extend-via-metadata true
(request-shutdown [this] "Asynchronously trigger normal shutdown")
(shutdown-on-error [this service-id f] [this service-id f on-error]
"Higher-order function to execute application logic and trigger shutdown in
Expand Down
1 change: 1 addition & 0 deletions documentation/Command-Line-Arguments.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ There is a protocol that represents a Trapperkeeper application:
```clj
(defprotocol TrapperkeeperApp
"Functions available on a Trapperkeeper application instance"
:extend-via-metadata true
(app-context [this] "Returns the application context for this app (an atom containing a map)")
(check-for-errors! [this] (str "Check for any errors which have occurred in "
"the bootstrap process. If any have "
Expand Down
6 changes: 6 additions & 0 deletions documentation/Defining-Services.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The service `Lifecycle` protocol looks like this:

```clj
(defprotocol Lifecycle
:extend-via-metadata true
(init [this context])
(start [this context])
(stop [this context]))
Expand All @@ -39,6 +40,7 @@ Let's look at a concrete example:
;; 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
:extend-via-metadata true
(foo1 [this x])
(foo2 [this])
(foo3 [this x]))
Expand Down Expand Up @@ -99,6 +101,7 @@ Clojure's protocols allow you to define multi-arity functions:

```clj
(defprotocol MultiArityService
:extend-via-metadata true
(foo [this x] [this x y]))
```

Expand All @@ -124,6 +127,7 @@ Trapperkeeper services can use the syntax from clojure's `reify` to implement th
context))

(defprotocol AnotherService
:extend-via-metadata true
(foo [this]))
```

Expand All @@ -137,8 +141,10 @@ To mark a dependency as optional, you use a different form to specify your depen

```clj
(defprotocol HaikuService
:extend-via-metadata true
(get-haiku [this] "return a lovely haiku"))
(defprotocol SonnetService
:extend-via-metadata true
(get-sonnet [this] "return a lovely sonnet"))

;; ... snip the definitions of HaikuService and SonnetService ...
Expand Down
1 change: 1 addition & 0 deletions documentation/Referencing-Services.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ In some cases you may actually prefer to get a reference to an object that satis
(ns bar.service)

(defprotocol BarService
:extend-via-metadata true
(bar-fn [this]))

...
Expand Down
2 changes: 2 additions & 0 deletions documentation/Service-Interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Here's a concrete example of how this might work:
(ns services.foo)

(defprotocol FooService
:extend-via-metadata true
(foo [this]))

(ns services.foo.lowercase-foo
Expand All @@ -33,6 +34,7 @@ Here's a concrete example of how this might work:
(ns services.foo-consumer)

(defprotocol FooConsumer
:extend-via-metadata true
(bar [this]))

(defservice foo-consumer
Expand Down
1 change: 1 addition & 0 deletions documentation/Test-Utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ This macro allows you to specify the services you want to launch directly and to
(ns services.test-service-1)

(defprotocol TestService1
:extend-via-metadata true
(test-fn [this]))

(defservice test-service1
Expand Down
2 changes: 2 additions & 0 deletions documentation/Trapperkeeper-Best-Practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ In general, it's a good idea to keep the code that implements your business logi

```clj
(defprotocol CalculatorService
:extend-via-metadata true
(add [this x y]))

(defservice calculator-service
Expand All @@ -40,6 +41,7 @@ This is better:
(:require calculator.core :as core))

(defprotocol CalculatorService
:extend-via-metadata true
(add [this x y]))

(defservice calculator-service
Expand Down
1 change: 1 addition & 0 deletions documentation/Trapperkeeper-Quick-Start.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ First, you need to define one or more services:

;; A protocol that defines what functions our service will provide
(defprotocol HelloService
:extend-via-metadata true
(hello [this])

(defservice hello-service
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[clojure.tools.logging :as log]))

(defprotocol JavaService
:extend-via-metadata true
(msg-fn [this])
(meaning-of-life-fn [this]))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
(:require [puppetlabs.trapperkeeper.core :refer [defservice]]))

(defprotocol PluginTestService
:extend-via-metadata true
(moo [this]))

(defservice plugin-test-service
Expand Down
7 changes: 4 additions & 3 deletions project.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(defproject puppetlabs/trapperkeeper "3.0.0"
(defproject threatgrid/trapperkeeper "3.2.0"
:description "A framework for configuring, composing, and running Clojure services."

:license {:name "Apache License, Version 2.0"
Expand All @@ -12,12 +12,13 @@
;; Abort when version ranges or version conflicts are detected in
;; dependencies. Also supports :warn to simply emit warnings.
;; requires lein 2.2.0+.
:pedantic? :abort
:dependencies [[org.clojure/clojure]
:dependencies [[org.clojure/clojure "1.10.1"]
[org.clojure/tools.logging]
[org.clojure/tools.macro]
[org.clojure/core.async]

[com.nedap.staffing-solutions/speced.def "2.0.0"]

[org.slf4j/log4j-over-slf4j]
[ch.qos.logback/logback-classic]
;; even though we don't strictly have a dependency on the following two
Expand Down
20 changes: 12 additions & 8 deletions src/puppetlabs/trapperkeeper/app.clj
Original file line number Diff line number Diff line change
@@ -1,33 +1,37 @@
(ns puppetlabs.trapperkeeper.app
(:require [schema.core :as schema]
(:require
[clojure.core.async.impl.protocols :as async-prot]
[puppetlabs.trapperkeeper.services :as s]
[clojure.core.async.impl.protocols :as async-prot])
(:import (clojure.lang IDeref)))
[puppetlabs.trapperkeeper.util :refer [protocol]]
[schema.core :as schema])
(:import
(clojure.lang IDeref)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Schema

(def TrapperkeeperAppOrderedServices
[[(schema/one schema/Keyword "service-id")
(schema/one (schema/protocol s/Service) "Service")]])
(schema/one (protocol s/Service) "Service")]])

(def TrapperkeeperAppContext
"Schema for a Trapperkeeper application's internal context. NOTE: this schema
is intended for internal use by TK and may be subject to minor changes in future
releases."
{:service-contexts {schema/Keyword {schema/Any schema/Any}}
:ordered-services TrapperkeeperAppOrderedServices
:services-by-id {schema/Keyword (schema/protocol s/Service)}
:lifecycle-channel (schema/protocol async-prot/Channel)
:shutdown-channel (schema/protocol async-prot/Channel)
:lifecycle-worker (schema/protocol async-prot/Channel)
:services-by-id {schema/Keyword (protocol s/Service)}
:lifecycle-channel (protocol async-prot/Channel)
:shutdown-channel (protocol async-prot/Channel)
:lifecycle-worker (protocol async-prot/Channel)
:shutdown-reason-promise IDeref})

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; App Protocol

(defprotocol TrapperkeeperApp
"Functions available on a trapperkeeper application instance"
:extend-via-metadata true
(get-service [this service-id] "Returns the service with the given service id")
(service-graph [this] "Returns the prismatic graph of service fns for this app")
(app-context [this] "Returns the application context for this app (an atom containing a map)")
Expand Down
33 changes: 18 additions & 15 deletions src/puppetlabs/trapperkeeper/bootstrap.clj
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
(ns puppetlabs.trapperkeeper.bootstrap
(:import (java.io FileNotFoundException)
(java.net URI URISyntaxException))
(:require [clojure.string :as string]
(:require
[clojure.java.io :as io]
[clojure.string :as string]
[clojure.tools.logging :as log]
[me.raynes.fs :as fs]
[puppetlabs.i18n.core :as i18n]
[puppetlabs.trapperkeeper.util :refer [protocol]]
[puppetlabs.trapperkeeper.common :as common]
[puppetlabs.trapperkeeper.internal :as internal]
[puppetlabs.trapperkeeper.services :as services]
[puppetlabs.trapperkeeper.common :as common]
[schema.core :as schema]
[me.raynes.fs :as fs]
[slingshot.slingshot :refer [try+ throw+]]
[puppetlabs.i18n.core :as i18n]))
[slingshot.slingshot :refer [throw+ try+]])
(:import
(java.io FileNotFoundException)
(java.net URI URISyntaxException)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Schemas
Expand Down Expand Up @@ -175,8 +178,8 @@
"Returns an IllegalArgumentException describing what services implement
the same protocol, including the line number and file the bootstrap entries
were found"
[duplicate-services :- {schema/Keyword [(schema/protocol services/ServiceDefinition)]}
service->entry-map :- {(schema/protocol services/ServiceDefinition) AnnotatedBootstrapEntry}]
[duplicate-services :- {schema/Keyword [(protocol services/ServiceDefinition)]}
service->entry-map :- {(protocol services/ServiceDefinition) AnnotatedBootstrapEntry}]
(let [make-error-message (fn [service]
(let [entry (get service->entry-map service)]
(i18n/trs "{0}:{1}\n{2}" (:bootstrap-file entry) (:line-number entry) (:entry entry))))]
Expand All @@ -188,7 +191,7 @@

(schema/defn check-duplicate-service-implementations!
"Throws an exception if two services implement the same service protocol"
[services :- [(schema/protocol services/ServiceDefinition)]
[services :- [(protocol services/ServiceDefinition)]
bootstrap-entries :- [AnnotatedBootstrapEntry]]

; Zip up the services and bootstrap entries and construct a map out of them
Expand All @@ -200,7 +203,7 @@
(when (not (empty? duplicates))
(throw (duplicate-protocol-error duplicates service->entry-map))))))

(schema/defn ^:private resolve-service! :- (schema/protocol services/ServiceDefinition)
(schema/defn ^:private resolve-service! :- (protocol services/ServiceDefinition)
"Given the namespace and name of a service, loads the namespace,
calls the function, validates that the result is a valid service definition, and
returns the service definition. Throws an `IllegalArgumentException` if the
Expand Down Expand Up @@ -229,7 +232,7 @@
(i18n/trs "Problem loading service ''{0}'' from {1}:{2}:\n{3}"
entry bootstrap-file line-number original-message)))

(schema/defn resolve-and-handle-errors! :- (schema/maybe (schema/protocol services/ServiceDefinition))
(schema/defn resolve-and-handle-errors! :- (schema/maybe (protocol services/ServiceDefinition))
"Attempts to resolve a bootstrap entry into a ServiceDefinition.
If the bootstrap entry can't be resolved, logs a warning and returns nil.

Expand All @@ -247,7 +250,7 @@
(catch [:type ::bootstrap-parse-error] {:keys [message]}
(throw (bootstrap-error entry bootstrap-file line-number message)))))

(schema/defn resolve-services! :- [(schema/protocol services/ServiceDefinition)]
(schema/defn resolve-services! :- [(protocol services/ServiceDefinition)]
"Resolves each bootstrap entry into an instance of a trapperkeeper
ServiceDefinition.

Expand Down Expand Up @@ -287,7 +290,7 @@
;; Public
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(schema/defn parse-bootstrap-configs! :- [(schema/protocol services/ServiceDefinition)]
(schema/defn parse-bootstrap-configs! :- [(protocol services/ServiceDefinition)]
"Parse multiple trapperkeeper bootstrap configuration files and return the
service graph that is the result of merging the graphs of all of the
services specified in the configuration files."
Expand All @@ -306,7 +309,7 @@
(check-duplicate-service-implementations! resolved-services bootstrap-entries)
resolved-services)))

(schema/defn parse-bootstrap-config! :- [(schema/protocol services/ServiceDefinition)]
(schema/defn parse-bootstrap-config! :- [(protocol services/ServiceDefinition)]
"Parse a single bootstrap configuration file and return the service graph
that is the result of merging the graphs of all the services specified in the
configuration file"
Expand Down
1 change: 1 addition & 0 deletions src/puppetlabs/trapperkeeper/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
;;; Service protocol

(defprotocol ConfigService
:extend-via-metadata true
(get-config [this] "Returns a map containing all of the configuration values")
(get-in-config [this ks] [this ks default]
"Returns the individual configuration value from the nested
Expand Down
35 changes: 19 additions & 16 deletions src/puppetlabs/trapperkeeper/core.clj
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
(ns puppetlabs.trapperkeeper.core
(:require [clojure.tools.logging :as log]
[slingshot.slingshot :refer [try+ throw+]]
(:require
[clojure.tools.logging :as log]
[nedap.speced.def :as speced]
[puppetlabs.i18n.core :as i18n]
[puppetlabs.kitchensink.core :refer [without-ns]]
[puppetlabs.trapperkeeper.services :as services]
[puppetlabs.trapperkeeper.app :as app]
[puppetlabs.trapperkeeper.bootstrap :as bootstrap]
[puppetlabs.trapperkeeper.internal :as internal]
[puppetlabs.trapperkeeper.common :as common]
[puppetlabs.trapperkeeper.config :as config]
[puppetlabs.trapperkeeper.util :refer [protocol]]
[puppetlabs.trapperkeeper.internal :as internal]
[puppetlabs.trapperkeeper.plugins :as plugins]
[puppetlabs.trapperkeeper.services :as services]
[schema.core :as schema]
[puppetlabs.trapperkeeper.common :as common]
[puppetlabs.i18n.core :as i18n]))
[slingshot.slingshot :refer [throw+ try+]]))

(def #^{:macro true
:doc "An alias for the `puppetlabs.trapperkeeper.services/service` macro
Expand All @@ -37,14 +40,14 @@
`start`, and then `run-app`."
[services config-data]
{:pre [(sequential? services)
(every? #(satisfies? services/ServiceDefinition %) services)
(every? #(speced/satisfies? services/ServiceDefinition %) services)
(ifn? config-data)]
:post [(satisfies? app/TrapperkeeperApp %)]}
:post [(speced/satisfies? app/TrapperkeeperApp %)]}
(let [config-data-fn (if (map? config-data) (constantly config-data) config-data)]
(config/initialize-logging! (config-data-fn))
(internal/build-app* services config-data-fn)))

(schema/defn boot-services-with-cli-data :- (schema/protocol app/TrapperkeeperApp)
(schema/defn boot-services-with-cli-data :- (protocol app/TrapperkeeperApp)
"Given a list of ServiceDefinitions and a map containing parsed cli data, create
and boot a trapperkeeper app. This function can be used if you prefer to
do your own CLI parsing and loading ServiceDefinitions; it circumvents
Expand All @@ -53,7 +56,7 @@

Returns a TrapperkeeperApp instance. Call `run-app` on it if you'd like to
block the main thread to wait for a shutdown event."
[services :- [(schema/protocol services/ServiceDefinition)]
[services :- [(protocol services/ServiceDefinition)]
cli-data :- common/CLIData]
(let [config-data-fn #(config/parse-config-data cli-data)]
(config/initialize-logging! (config-data-fn))
Expand All @@ -71,9 +74,9 @@
block the main thread to wait for a shutdown event."
[services config-data-fn]
{:pre [(sequential? services)
(every? #(satisfies? services/ServiceDefinition %) services)
(every? #(speced/satisfies? services/ServiceDefinition %) services)
(ifn? config-data-fn)]
:post [(satisfies? app/TrapperkeeperApp %)]}
:post [(speced/satisfies? app/TrapperkeeperApp %)]}
(config/initialize-logging! (config-data-fn))
(internal/boot-services* services config-data-fn))

Expand All @@ -88,12 +91,12 @@
block the main thread to wait for a shutdown event."
[services config-data]
{:pre [(sequential? services)
(every? #(satisfies? services/ServiceDefinition %) services)
(every? #(speced/satisfies? services/ServiceDefinition %) services)
(map? config-data)]
:post [(satisfies? app/TrapperkeeperApp %)]}
:post [(speced/satisfies? app/TrapperkeeperApp %)]}
(boot-services-with-config-fn services (constantly config-data)))

(schema/defn boot-with-cli-data :- (schema/protocol app/TrapperkeeperApp)
(schema/defn boot-with-cli-data :- (protocol app/TrapperkeeperApp)
"Create and boot a trapperkeeper application. This is accomplished by reading a
bootstrap configuration file containing a list of (namespace-qualified)
service functions. These functions will be called to generate a service
Expand Down Expand Up @@ -135,7 +138,7 @@
which may be triggered by one of several different ways. In all cases, services
will be shut down and any exceptions they might throw will be caught and logged."
[app]
{:pre [(satisfies? app/TrapperkeeperApp app)]}
{:pre [(speced/satisfies? app/TrapperkeeperApp app)]}
(let [shutdown-reason (internal/wait-for-app-shutdown app)]
(when (internal/initiated-internally? shutdown-reason)
(internal/call-error-handler! shutdown-reason)
Expand Down
Loading