-
Notifications
You must be signed in to change notification settings - Fork 5
Add readiness coordination for systemd notifications #87
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
|
|
||
| (:require [clojure.tools.logging :as log] | ||
| [beckon] | ||
| [clojure.set :as set] | ||
| [plumbing.graph :as graph] | ||
| [slingshot.slingshot :refer [throw+]] | ||
| [puppetlabs.trapperkeeper.config :refer [config-service get-in-config]] | ||
|
|
@@ -77,6 +78,22 @@ | |
| (defn notice-service-reloading [] (maybe-notify-systemd "RELOADING=1\n")) | ||
| (defn notice-service-stopping [] (maybe-notify-systemd "STOPPING=1\n")) | ||
|
|
||
| (defn maybe-send-ready-notice! | ||
| [readiness-state] | ||
| (let [send-notice? (atom false)] | ||
| (swap! readiness-state | ||
| (fn [{:keys [notifications-enabled? registered ready notice-sent?] :as state}] | ||
| (if (and notifications-enabled? | ||
| (not notice-sent?) | ||
| (seq registered) | ||
| (set/subset? registered ready)) | ||
| (do | ||
| (reset! send-notice? true) | ||
| (assoc state :notice-sent? true)) | ||
| state))) | ||
| (when @send-notice? | ||
| (notice-service-ready)))) | ||
|
|
||
| ;; This is (eww) a global variable that holds a reference to all of the running | ||
| ;; Trapperkeeper apps in the process. It can be used when connecting via nrepl | ||
| ;; to allow you to do useful things, and also may be used for other things | ||
|
|
@@ -85,6 +102,42 @@ | |
|
|
||
| (def max-pending-lifecycle-events 5) | ||
|
|
||
| ;; Debounce at the signal handler boundary to avoid back-to-back | ||
| ;; restart cycles that can interrupt in-flight agent requests. | ||
| (def min-sighup-restart-interval-ms 500) | ||
| (def last-sighup-restart-ms (atom nil)) | ||
|
|
||
| (defn now-ms [] | ||
| (System/currentTimeMillis)) | ||
|
|
||
| (defn should-handle-sighup-restart? | ||
| [] | ||
| (let [current-ms (now-ms) | ||
| accepted? (atom false) | ||
| previous-ms (atom nil)] | ||
| (swap! last-sighup-restart-ms | ||
| (fn [last-ms] | ||
| (reset! previous-ms last-ms) | ||
| (if (and (some? last-ms) | ||
| (< (- current-ms last-ms) min-sighup-restart-interval-ms)) | ||
| last-ms | ||
| (do | ||
| (reset! accepted? true) | ||
| current-ms)))) | ||
| (if @accepted? | ||
| (log/debug (i18n/trs "Accepting SIGHUP restart at {0}; previous accepted restart was {1}" | ||
| current-ms | ||
| @previous-ms)) | ||
| (log/warn (i18n/trs "Ignoring duplicate SIGHUP restart at {0}; previous accepted restart was {1}; minimum interval is {2} ms" | ||
| current-ms | ||
| @previous-ms | ||
| min-sighup-restart-interval-ms))) | ||
| @accepted?)) | ||
|
bastelfreak marked this conversation as resolved.
|
||
|
|
||
| (defn app-log-id | ||
| [app] | ||
| (str "app=0x" (Integer/toHexString (System/identityHashCode app)))) | ||
|
Comment on lines
+137
to
+139
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was intended for debugging and not that helpful. or do the apps have humand readable names somewhere? |
||
|
|
||
| (defn service-graph? | ||
| "Predicate that tests whether or not the argument is a valid trapperkeeper | ||
| service graph." | ||
|
|
@@ -351,6 +404,7 @@ | |
| [apps] | ||
| (log/info (i18n/trs "SIGHUP handler restarting TK apps.")) | ||
| (doseq [app apps] | ||
| (log/info (i18n/trs "Queueing SIGHUP restart for TK {0}" (app-log-id app))) | ||
| (let [{:keys [lifecycle-channel]} @(a/app-context app) | ||
| restart-fn #(a/restart app)] | ||
| (when-not (async/offer! lifecycle-channel | ||
|
|
@@ -359,6 +413,14 @@ | |
| (log/warn (i18n/trs "Ignoring new SIGHUP restart requests; too many requests queued ({0})" | ||
| max-pending-lifecycle-events)))))) | ||
|
|
||
| (defn maybe-restart-tk-apps | ||
| "Restart all TK apps unless this SIGHUP request is a rapid duplicate." | ||
| [apps] | ||
| (if (should-handle-sighup-restart?) | ||
| (restart-tk-apps apps) | ||
| (log/warn (i18n/trs "Ignoring duplicate SIGHUP restart request received within {0} ms" | ||
| min-sighup-restart-interval-ms)))) | ||
|
Comment on lines
+416
to
+422
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this would be enough as a minimal workaround and we don't need the readinessservice (although I think it makes sense to have it). However, I don't know why we even receive a second SIGHUP. |
||
|
|
||
| (defn register-sighup-handler | ||
| "Register a handler for SIGHUP that restarts all trapperkeeper apps. The | ||
| default handler terminates the process, so we always overwrite that. This | ||
|
|
@@ -367,7 +429,7 @@ | |
| (register-sighup-handler @tk-apps)) | ||
| ([apps] | ||
| (log/debug (i18n/trs "Registering SIGHUP handler for restarting TK apps")) | ||
| (reset! (beckon/signal-atom "HUP") #{(partial restart-tk-apps apps)}))) | ||
| (reset! (beckon/signal-atom "HUP") #{(partial maybe-restart-tk-apps apps)}))) | ||
|
|
||
| ;;;; Application Shutdown Support | ||
| ;;;; | ||
|
|
@@ -469,6 +531,20 @@ | |
| "Higher-order function to execute application logic and trigger shutdown in | ||
| the event of an exception")) | ||
|
|
||
| (defprotocol ReadinessService | ||
| (register-ready! [this service-id] | ||
| "Register a service that will signal readiness explicitly.") | ||
| (signal-ready! [this service-id] | ||
| "Mark a registered service as ready.") | ||
| (readiness-coordinated? [this] | ||
| "Returns true if any services have registered explicit readiness coordination.") | ||
| (enable-ready-notifications! [this] | ||
| "Allow readiness notifications to be emitted once all registered services are ready.") | ||
| (reset-readiness! [this] | ||
| "Reset readiness tracking for a boot or restart cycle.") | ||
| (readiness-state [this] | ||
| "Return the current readiness tracking state.")) | ||
|
|
||
| (schema/defn shutdown-service | ||
| "Provides various functions for triggering application shutdown programatically. | ||
| Primarily intended to serve application services, though TrapperKeeper also uses | ||
|
|
@@ -497,6 +573,60 @@ | |
| (shutdown-on-error [this svc-id f] (shutdown-on-error* shutdown-reason-promise app-context svc-id f)) | ||
| (shutdown-on-error [this svc-id f on-error] (shutdown-on-error* shutdown-reason-promise app-context svc-id f on-error)))) | ||
|
|
||
| (schema/defn readiness-service :- (schema/protocol s/ServiceDefinition) | ||
| "Provides explicit readiness coordination for services that need to delay | ||
| systemd READY=1 until after their own startup work is complete. Services that | ||
| participate should call `register-ready!` before app startup completes and | ||
| `signal-ready!` when they are actually ready to serve traffic." | ||
| [] | ||
| (let [state (atom {:notifications-enabled? false | ||
| :registered #{} | ||
| :ready #{} | ||
| :notice-sent? false})] | ||
| (s/service ReadinessService | ||
| [] | ||
| (register-ready! [_ service-id] | ||
| (schema/validate schema/Keyword service-id) | ||
| (swap! state update :registered conj service-id) | ||
| (maybe-send-ready-notice! state) | ||
| nil) | ||
| (signal-ready! [_ service-id] | ||
| (schema/validate schema/Keyword service-id) | ||
| (swap! state update :ready conj service-id) | ||
| (maybe-send-ready-notice! state) | ||
| nil) | ||
| (readiness-coordinated? [_] | ||
| (boolean (seq (:registered @state)))) | ||
| (enable-ready-notifications! [_] | ||
| (swap! state assoc :notifications-enabled? true) | ||
| (maybe-send-ready-notice! state) | ||
| nil) | ||
| (reset-readiness! [_] | ||
| (reset! state {:notifications-enabled? false | ||
| :registered #{} | ||
| :ready #{} | ||
| :notice-sent? false}) | ||
| nil) | ||
| (readiness-state [_] | ||
| @state)))) | ||
|
|
||
| (defn reset-readiness-tracking! | ||
| [services-by-id] | ||
| (when-let [readiness-service (services-by-id :ReadinessService)] | ||
| (reset-readiness! readiness-service))) | ||
|
|
||
| (defn enable-ready-notifications-for-app! | ||
| [services-by-id] | ||
| (when-let [readiness-service (services-by-id :ReadinessService)] | ||
| (enable-ready-notifications! readiness-service))) | ||
|
|
||
| (defn notice-service-ready-if-uncoordinated! | ||
| [services-by-id] | ||
| (if-let [readiness-service (services-by-id :ReadinessService)] | ||
| (when-not (readiness-coordinated? readiness-service) | ||
| (notice-service-ready)) | ||
| (notice-service-ready))) | ||
|
|
||
| (schema/defn ^:always-validate shutdown! :- [Throwable] | ||
| "Perform shutdown calling the `stop` lifecycle function on each service, | ||
| in reverse order (to account for dependency relationships). | ||
|
|
@@ -633,6 +763,7 @@ | |
| service-refs (atom {}) | ||
| services (conj services | ||
| (config-service config-data-fn) | ||
| (readiness-service) | ||
| (initialize-shutdown-service! app-context | ||
| shutdown-reason-promise)) | ||
| service-map (apply merge (map s/service-map services)) | ||
|
|
@@ -660,11 +791,13 @@ | |
| (a/check-for-errors! [this] (throw-app-error-if-exists! | ||
| this)) | ||
| (a/init [this] | ||
| (reset-readiness-tracking! services-by-id) | ||
| (run-lifecycle-fns app-context s/init "init" ordered-services) | ||
| this) | ||
| (a/start [this] | ||
| (run-lifecycle-fns app-context s/start "start" ordered-services) | ||
| (notice-service-ready) | ||
| (enable-ready-notifications-for-app! services-by-id) | ||
| (notice-service-ready-if-uncoordinated! services-by-id) | ||
| (inc-restart-counter! this) | ||
| this) | ||
| (a/stop [this] | ||
|
|
@@ -679,12 +812,16 @@ | |
| this))) | ||
| (a/restart [this] | ||
| (try | ||
| (log/info (i18n/trs "Starting restart for TK {0}" (app-log-id this))) | ||
| (notice-service-reloading) | ||
| (run-lifecycle-fns app-context s/stop "stop" (reverse ordered-services)) | ||
| (doseq [svc-id (keys services-by-id)] (swap! app-context assoc-in [:service-contexts svc-id] {})) | ||
| (reset-readiness-tracking! services-by-id) | ||
| (run-lifecycle-fns app-context s/init "init" ordered-services) | ||
| (run-lifecycle-fns app-context s/start "start" ordered-services) | ||
| (notice-service-ready) | ||
| (enable-ready-notifications-for-app! services-by-id) | ||
| (notice-service-ready-if-uncoordinated! services-by-id) | ||
| (log/info (i18n/trs "Finished restart for TK {0}" (app-log-id this))) | ||
| (inc-restart-counter! this) | ||
| this | ||
| (catch Throwable t | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My one concern about this is that a reload cycle for a production service can take much longer than 500ms to complete.
The atom should probably track if we are
:running, or:reloading, only allow one invocation ofSIGHUPto swap over to:reloading, and short-circuit all other invocations until the atom is set back to:running.