Skip to content

Commit 7ab2994

Browse files
committed
Add readiness coordination for systemd notifications
Signed-off-by: Tim Meusel <tim@bastelfreak.de>
1 parent 534a712 commit 7ab2994

5 files changed

Lines changed: 171 additions & 15 deletions

File tree

project.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
[org.openvoxproject/i18n ~i18n-version]]
9696

9797
:eastwood {:ignored-faults {:reflection {puppetlabs.trapperkeeper.logging [{:line 92}]
98-
puppetlabs.trapperkeeper.internal [{:line 177}]
98+
puppetlabs.trapperkeeper.internal [{:line 194}]
9999
puppetlabs.trapperkeeper.testutils.logging true
100100
puppetlabs.trapperkeeper.testutils.logging-test true
101101
puppetlabs.trapperkeeper.services.nrepl.nrepl-service-test true

src/puppetlabs/trapperkeeper/core.clj

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,19 @@
1313
(:import
1414
(clojure.lang ExceptionInfo)))
1515

16-
(def #^{:macro true
17-
:doc "An alias for the `puppetlabs.trapperkeeper.services/service` macro
18-
so that it is accessible from the core namespace along with the
19-
rest of the API."}
20-
service #'services/service)
21-
22-
(def #^{:macro true
23-
:doc "An alias for the `puppetlabs.trapperkeeper.services/defservice` macro
24-
so that it is accessible from the core namespace along with the
25-
rest of the API."}
26-
defservice #'services/defservice)
16+
(defmacro service
17+
"An alias for the `puppetlabs.trapperkeeper.services/service` macro
18+
so that it is accessible from the core namespace along with the
19+
rest of the API."
20+
[& forms]
21+
`(services/service ~@forms))
22+
23+
(defmacro defservice
24+
"An alias for the `puppetlabs.trapperkeeper.services/defservice` macro
25+
so that it is accessible from the core namespace along with the
26+
rest of the API."
27+
[svc-name & forms]
28+
`(services/defservice ~svc-name ~@forms))
2729

2830
(defn build-app
2931
"Given a list of services and a map of configuration data, build an instance

src/puppetlabs/trapperkeeper/internal.clj

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
(:require [clojure.tools.logging :as log]
1212
[beckon]
13+
[clojure.set :as set]
1314
[plumbing.graph :as graph]
1415
[slingshot.slingshot :refer [throw+]]
1516
[puppetlabs.trapperkeeper.config :refer [config-service get-in-config]]
@@ -77,6 +78,22 @@
7778
(defn notice-service-reloading [] (maybe-notify-systemd "RELOADING=1\n"))
7879
(defn notice-service-stopping [] (maybe-notify-systemd "STOPPING=1\n"))
7980

81+
(defn maybe-send-ready-notice!
82+
[readiness-state]
83+
(let [send-notice? (atom false)]
84+
(swap! readiness-state
85+
(fn [{:keys [notifications-enabled? registered ready notice-sent?] :as state}]
86+
(if (and notifications-enabled?
87+
(not notice-sent?)
88+
(seq registered)
89+
(set/subset? registered ready))
90+
(do
91+
(reset! send-notice? true)
92+
(assoc state :notice-sent? true))
93+
state)))
94+
(when @send-notice?
95+
(notice-service-ready))))
96+
8097
;; This is (eww) a global variable that holds a reference to all of the running
8198
;; Trapperkeeper apps in the process. It can be used when connecting via nrepl
8299
;; to allow you to do useful things, and also may be used for other things
@@ -469,6 +486,20 @@
469486
"Higher-order function to execute application logic and trigger shutdown in
470487
the event of an exception"))
471488

489+
(defprotocol ReadinessService
490+
(register-ready! [this service-id]
491+
"Register a service that will signal readiness explicitly.")
492+
(signal-ready! [this service-id]
493+
"Mark a registered service as ready.")
494+
(readiness-coordinated? [this]
495+
"Returns true if any services have registered explicit readiness coordination.")
496+
(enable-ready-notifications! [this]
497+
"Allow readiness notifications to be emitted once all registered services are ready.")
498+
(reset-readiness! [this]
499+
"Reset readiness tracking for a boot or restart cycle.")
500+
(readiness-state [this]
501+
"Return the current readiness tracking state."))
502+
472503
(schema/defn shutdown-service
473504
"Provides various functions for triggering application shutdown programatically.
474505
Primarily intended to serve application services, though TrapperKeeper also uses
@@ -497,6 +528,60 @@
497528
(shutdown-on-error [this svc-id f] (shutdown-on-error* shutdown-reason-promise app-context svc-id f))
498529
(shutdown-on-error [this svc-id f on-error] (shutdown-on-error* shutdown-reason-promise app-context svc-id f on-error))))
499530

531+
(schema/defn readiness-service :- (schema/protocol s/ServiceDefinition)
532+
"Provides explicit readiness coordination for services that need to delay
533+
systemd READY=1 until after their own startup work is complete. Services that
534+
participate should call `register-ready!` before app startup completes and
535+
`signal-ready!` when they are actually ready to serve traffic."
536+
[]
537+
(let [state (atom {:notifications-enabled? false
538+
:registered #{}
539+
:ready #{}
540+
:notice-sent? false})]
541+
(s/service ReadinessService
542+
[]
543+
(register-ready! [_ service-id]
544+
(schema/validate schema/Keyword service-id)
545+
(swap! state update :registered conj service-id)
546+
(maybe-send-ready-notice! state)
547+
nil)
548+
(signal-ready! [_ service-id]
549+
(schema/validate schema/Keyword service-id)
550+
(swap! state update :ready conj service-id)
551+
(maybe-send-ready-notice! state)
552+
nil)
553+
(readiness-coordinated? [_]
554+
(boolean (seq (:registered @state))))
555+
(enable-ready-notifications! [_]
556+
(swap! state assoc :notifications-enabled? true)
557+
(maybe-send-ready-notice! state)
558+
nil)
559+
(reset-readiness! [_]
560+
(reset! state {:notifications-enabled? false
561+
:registered #{}
562+
:ready #{}
563+
:notice-sent? false})
564+
nil)
565+
(readiness-state [_]
566+
@state))))
567+
568+
(defn reset-readiness-tracking!
569+
[services-by-id]
570+
(when-let [readiness-service (services-by-id :ReadinessService)]
571+
(reset-readiness! readiness-service)))
572+
573+
(defn enable-ready-notifications-for-app!
574+
[services-by-id]
575+
(when-let [readiness-service (services-by-id :ReadinessService)]
576+
(enable-ready-notifications! readiness-service)))
577+
578+
(defn notice-service-ready-if-uncoordinated!
579+
[services-by-id]
580+
(if-let [readiness-service (services-by-id :ReadinessService)]
581+
(when-not (readiness-coordinated? readiness-service)
582+
(notice-service-ready))
583+
(notice-service-ready)))
584+
500585
(schema/defn ^:always-validate shutdown! :- [Throwable]
501586
"Perform shutdown calling the `stop` lifecycle function on each service,
502587
in reverse order (to account for dependency relationships).
@@ -633,6 +718,7 @@
633718
service-refs (atom {})
634719
services (conj services
635720
(config-service config-data-fn)
721+
(readiness-service)
636722
(initialize-shutdown-service! app-context
637723
shutdown-reason-promise))
638724
service-map (apply merge (map s/service-map services))
@@ -660,11 +746,13 @@
660746
(a/check-for-errors! [this] (throw-app-error-if-exists!
661747
this))
662748
(a/init [this]
749+
(reset-readiness-tracking! services-by-id)
663750
(run-lifecycle-fns app-context s/init "init" ordered-services)
664751
this)
665752
(a/start [this]
666753
(run-lifecycle-fns app-context s/start "start" ordered-services)
667-
(notice-service-ready)
754+
(enable-ready-notifications-for-app! services-by-id)
755+
(notice-service-ready-if-uncoordinated! services-by-id)
668756
(inc-restart-counter! this)
669757
this)
670758
(a/stop [this]
@@ -682,9 +770,11 @@
682770
(notice-service-reloading)
683771
(run-lifecycle-fns app-context s/stop "stop" (reverse ordered-services))
684772
(doseq [svc-id (keys services-by-id)] (swap! app-context assoc-in [:service-contexts svc-id] {}))
773+
(reset-readiness-tracking! services-by-id)
685774
(run-lifecycle-fns app-context s/init "init" ordered-services)
686775
(run-lifecycle-fns app-context s/start "start" ordered-services)
687-
(notice-service-ready)
776+
(enable-ready-notifications-for-app! services-by-id)
777+
(notice-service-ready-if-uncoordinated! services-by-id)
688778
(inc-restart-counter! this)
689779
this
690780
(catch Throwable t

src/puppetlabs/trapperkeeper/services.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
(get-services [this#]
104104
(-> ~'@tk-app-context
105105
:services-by-id
106-
(dissoc :ConfigService :ShutdownService)
106+
(dissoc :ConfigService :ShutdownService :ReadinessService)
107107
vals))
108108
(service-symbol [this#] '~service-sym)
109109
(service-included? [this# service-id#]

test/puppetlabs/trapperkeeper/services_test.clj

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@
5555
(defprotocol Service3
5656
(service3-fn [this]))
5757

58+
(defprotocol CoordinatedReadyService
59+
(mark-ready [this])
60+
(current-readiness-state [this]))
61+
5862
(deftest test-services-not-required
5963
(testing "services are not required to define lifecycle functions"
6064
(let [service1 (service Service1
@@ -520,6 +524,66 @@
520524
(is (= #{:EmptyService :HelloService}
521525
(set (map svcs/service-id all-services)))))))))))
522526

527+
(deftest readiness-service-falls-back-to-central-ready-notice
528+
(let [ready-notices (atom 0)
529+
service1 (service Service1
530+
[]
531+
(service1-fn [_] "hi"))]
532+
(with-redefs [internal/notice-service-ready #(swap! ready-notices inc)]
533+
(with-app-with-empty-config app [service1]
534+
(is (= 1 @ready-notices))))))
535+
536+
(deftest readiness-service-defers-ready-notice-until-signaled
537+
(let [ready-notices (atom 0)
538+
coordinated-service
539+
(service CoordinatedReadyService
540+
[[:ReadinessService register-ready! signal-ready! readiness-state]]
541+
(init [_ context]
542+
(register-ready! :CoordinatedReadyService)
543+
context)
544+
(start [_ context]
545+
context)
546+
(mark-ready [_]
547+
(signal-ready! :CoordinatedReadyService))
548+
(current-readiness-state [_]
549+
(readiness-state)))]
550+
(with-redefs [internal/notice-service-ready #(swap! ready-notices inc)]
551+
(with-app-with-empty-config app [coordinated-service]
552+
(let [service (app/get-service app :CoordinatedReadyService)]
553+
(is (zero? @ready-notices))
554+
(is (= #{:CoordinatedReadyService}
555+
(get-in (current-readiness-state service) [:registered])))
556+
(mark-ready service)
557+
(is (= 1 @ready-notices))
558+
(mark-ready service)
559+
(is (= 1 @ready-notices)))))))
560+
561+
(deftest readiness-service-resets-across-restart
562+
(let [ready-notices (atom 0)
563+
coordinated-service
564+
(service CoordinatedReadyService
565+
[[:ReadinessService register-ready! signal-ready! readiness-state]]
566+
(init [_ context]
567+
(register-ready! :CoordinatedReadyService)
568+
context)
569+
(start [_ context]
570+
context)
571+
(mark-ready [_]
572+
(signal-ready! :CoordinatedReadyService))
573+
(current-readiness-state [_]
574+
(readiness-state)))]
575+
(with-redefs [internal/notice-service-ready #(swap! ready-notices inc)]
576+
(with-app-with-empty-config app [coordinated-service]
577+
(let [service (app/get-service app :CoordinatedReadyService)]
578+
(mark-ready service)
579+
(is (= 1 @ready-notices))
580+
(app/restart app)
581+
(is (= 1 @ready-notices))
582+
(is (= #{:CoordinatedReadyService}
583+
(get-in (current-readiness-state service) [:registered])))
584+
(mark-ready service)
585+
(is (= 2 @ready-notices)))))))
586+
523587
(deftest minimal-services-test
524588
(testing "minimal services can be defined without a protocol"
525589
(let [call-seq (atom [])

0 commit comments

Comments
 (0)