|
10 | 10 |
|
11 | 11 | (:require [clojure.tools.logging :as log] |
12 | 12 | [beckon] |
| 13 | + [clojure.set :as set] |
13 | 14 | [plumbing.graph :as graph] |
14 | 15 | [slingshot.slingshot :refer [throw+]] |
15 | 16 | [puppetlabs.trapperkeeper.config :refer [config-service get-in-config]] |
|
77 | 78 | (defn notice-service-reloading [] (maybe-notify-systemd "RELOADING=1\n")) |
78 | 79 | (defn notice-service-stopping [] (maybe-notify-systemd "STOPPING=1\n")) |
79 | 80 |
|
| 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 | + |
80 | 97 | ;; This is (eww) a global variable that holds a reference to all of the running |
81 | 98 | ;; Trapperkeeper apps in the process. It can be used when connecting via nrepl |
82 | 99 | ;; to allow you to do useful things, and also may be used for other things |
|
85 | 102 |
|
86 | 103 | (def max-pending-lifecycle-events 5) |
87 | 104 |
|
| 105 | +;; Debounce at the signal handler boundary to avoid back-to-back |
| 106 | +;; restart cycles that can interrupt in-flight agent requests. |
| 107 | +(def min-sighup-restart-interval-ms 500) |
| 108 | +(def last-sighup-restart-ms (atom nil)) |
| 109 | + |
| 110 | +(defn now-ms [] |
| 111 | + (System/currentTimeMillis)) |
| 112 | + |
| 113 | +(defn should-handle-sighup-restart? |
| 114 | + [] |
| 115 | + (let [current-ms (now-ms) |
| 116 | + accepted? (atom false) |
| 117 | + previous-ms (atom nil)] |
| 118 | + (swap! last-sighup-restart-ms |
| 119 | + (fn [last-ms] |
| 120 | + (reset! previous-ms last-ms) |
| 121 | + (if (and (some? last-ms) |
| 122 | + (< (- current-ms last-ms) min-sighup-restart-interval-ms)) |
| 123 | + last-ms |
| 124 | + (do |
| 125 | + (reset! accepted? true) |
| 126 | + current-ms)))) |
| 127 | + (if @accepted? |
| 128 | + (log/debug (i18n/trs "Accepting SIGHUP restart at {0}; previous accepted restart was {1}" |
| 129 | + current-ms |
| 130 | + @previous-ms)) |
| 131 | + (log/warn (i18n/trs "Ignoring duplicate SIGHUP restart at {0}; previous accepted restart was {1}; minimum interval is {2} ms" |
| 132 | + current-ms |
| 133 | + @previous-ms |
| 134 | + min-sighup-restart-interval-ms))) |
| 135 | + @accepted?)) |
| 136 | + |
| 137 | +(defn app-log-id |
| 138 | + [app] |
| 139 | + (str "app=0x" (Integer/toHexString (System/identityHashCode app)))) |
| 140 | + |
88 | 141 | (defn service-graph? |
89 | 142 | "Predicate that tests whether or not the argument is a valid trapperkeeper |
90 | 143 | service graph." |
|
351 | 404 | [apps] |
352 | 405 | (log/info (i18n/trs "SIGHUP handler restarting TK apps.")) |
353 | 406 | (doseq [app apps] |
| 407 | + (log/info (i18n/trs "Queueing SIGHUP restart for TK {0}" (app-log-id app))) |
354 | 408 | (let [{:keys [lifecycle-channel]} @(a/app-context app) |
355 | 409 | restart-fn #(a/restart app)] |
356 | 410 | (when-not (async/offer! lifecycle-channel |
|
359 | 413 | (log/warn (i18n/trs "Ignoring new SIGHUP restart requests; too many requests queued ({0})" |
360 | 414 | max-pending-lifecycle-events)))))) |
361 | 415 |
|
| 416 | +(defn maybe-restart-tk-apps |
| 417 | + "Restart all TK apps unless this SIGHUP request is a rapid duplicate." |
| 418 | + [apps] |
| 419 | + (if (should-handle-sighup-restart?) |
| 420 | + (restart-tk-apps apps) |
| 421 | + (log/warn (i18n/trs "Ignoring duplicate SIGHUP restart request received within {0} ms" |
| 422 | + min-sighup-restart-interval-ms)))) |
| 423 | + |
362 | 424 | (defn register-sighup-handler |
363 | 425 | "Register a handler for SIGHUP that restarts all trapperkeeper apps. The |
364 | 426 | default handler terminates the process, so we always overwrite that. This |
|
367 | 429 | (register-sighup-handler @tk-apps)) |
368 | 430 | ([apps] |
369 | 431 | (log/debug (i18n/trs "Registering SIGHUP handler for restarting TK apps")) |
370 | | - (reset! (beckon/signal-atom "HUP") #{(partial restart-tk-apps apps)}))) |
| 432 | + (reset! (beckon/signal-atom "HUP") #{(partial maybe-restart-tk-apps apps)}))) |
371 | 433 |
|
372 | 434 | ;;;; Application Shutdown Support |
373 | 435 | ;;;; |
|
469 | 531 | "Higher-order function to execute application logic and trigger shutdown in |
470 | 532 | the event of an exception")) |
471 | 533 |
|
| 534 | +(defprotocol ReadinessService |
| 535 | + (register-ready! [this service-id] |
| 536 | + "Register a service that will signal readiness explicitly.") |
| 537 | + (signal-ready! [this service-id] |
| 538 | + "Mark a registered service as ready.") |
| 539 | + (readiness-coordinated? [this] |
| 540 | + "Returns true if any services have registered explicit readiness coordination.") |
| 541 | + (enable-ready-notifications! [this] |
| 542 | + "Allow readiness notifications to be emitted once all registered services are ready.") |
| 543 | + (reset-readiness! [this] |
| 544 | + "Reset readiness tracking for a boot or restart cycle.") |
| 545 | + (readiness-state [this] |
| 546 | + "Return the current readiness tracking state.")) |
| 547 | + |
472 | 548 | (schema/defn shutdown-service |
473 | 549 | "Provides various functions for triggering application shutdown programatically. |
474 | 550 | Primarily intended to serve application services, though TrapperKeeper also uses |
|
497 | 573 | (shutdown-on-error [this svc-id f] (shutdown-on-error* shutdown-reason-promise app-context svc-id f)) |
498 | 574 | (shutdown-on-error [this svc-id f on-error] (shutdown-on-error* shutdown-reason-promise app-context svc-id f on-error)))) |
499 | 575 |
|
| 576 | +(schema/defn readiness-service :- (schema/protocol s/ServiceDefinition) |
| 577 | + "Provides explicit readiness coordination for services that need to delay |
| 578 | + systemd READY=1 until after their own startup work is complete. Services that |
| 579 | + participate should call `register-ready!` before app startup completes and |
| 580 | + `signal-ready!` when they are actually ready to serve traffic." |
| 581 | + [] |
| 582 | + (let [state (atom {:notifications-enabled? false |
| 583 | + :registered #{} |
| 584 | + :ready #{} |
| 585 | + :notice-sent? false})] |
| 586 | + (s/service ReadinessService |
| 587 | + [] |
| 588 | + (register-ready! [_ service-id] |
| 589 | + (schema/validate schema/Keyword service-id) |
| 590 | + (swap! state update :registered conj service-id) |
| 591 | + (maybe-send-ready-notice! state) |
| 592 | + nil) |
| 593 | + (signal-ready! [_ service-id] |
| 594 | + (schema/validate schema/Keyword service-id) |
| 595 | + (swap! state update :ready conj service-id) |
| 596 | + (maybe-send-ready-notice! state) |
| 597 | + nil) |
| 598 | + (readiness-coordinated? [_] |
| 599 | + (boolean (seq (:registered @state)))) |
| 600 | + (enable-ready-notifications! [_] |
| 601 | + (swap! state assoc :notifications-enabled? true) |
| 602 | + (maybe-send-ready-notice! state) |
| 603 | + nil) |
| 604 | + (reset-readiness! [_] |
| 605 | + (reset! state {:notifications-enabled? false |
| 606 | + :registered #{} |
| 607 | + :ready #{} |
| 608 | + :notice-sent? false}) |
| 609 | + nil) |
| 610 | + (readiness-state [_] |
| 611 | + @state)))) |
| 612 | + |
| 613 | +(defn reset-readiness-tracking! |
| 614 | + [services-by-id] |
| 615 | + (when-let [readiness-service (services-by-id :ReadinessService)] |
| 616 | + (reset-readiness! readiness-service))) |
| 617 | + |
| 618 | +(defn enable-ready-notifications-for-app! |
| 619 | + [services-by-id] |
| 620 | + (when-let [readiness-service (services-by-id :ReadinessService)] |
| 621 | + (enable-ready-notifications! readiness-service))) |
| 622 | + |
| 623 | +(defn notice-service-ready-if-uncoordinated! |
| 624 | + [services-by-id] |
| 625 | + (if-let [readiness-service (services-by-id :ReadinessService)] |
| 626 | + (when-not (readiness-coordinated? readiness-service) |
| 627 | + (notice-service-ready)) |
| 628 | + (notice-service-ready))) |
| 629 | + |
500 | 630 | (schema/defn ^:always-validate shutdown! :- [Throwable] |
501 | 631 | "Perform shutdown calling the `stop` lifecycle function on each service, |
502 | 632 | in reverse order (to account for dependency relationships). |
|
633 | 763 | service-refs (atom {}) |
634 | 764 | services (conj services |
635 | 765 | (config-service config-data-fn) |
| 766 | + (readiness-service) |
636 | 767 | (initialize-shutdown-service! app-context |
637 | 768 | shutdown-reason-promise)) |
638 | 769 | service-map (apply merge (map s/service-map services)) |
|
660 | 791 | (a/check-for-errors! [this] (throw-app-error-if-exists! |
661 | 792 | this)) |
662 | 793 | (a/init [this] |
| 794 | + (reset-readiness-tracking! services-by-id) |
663 | 795 | (run-lifecycle-fns app-context s/init "init" ordered-services) |
664 | 796 | this) |
665 | 797 | (a/start [this] |
666 | 798 | (run-lifecycle-fns app-context s/start "start" ordered-services) |
667 | | - (notice-service-ready) |
| 799 | + (enable-ready-notifications-for-app! services-by-id) |
| 800 | + (notice-service-ready-if-uncoordinated! services-by-id) |
668 | 801 | (inc-restart-counter! this) |
669 | 802 | this) |
670 | 803 | (a/stop [this] |
|
679 | 812 | this))) |
680 | 813 | (a/restart [this] |
681 | 814 | (try |
| 815 | + (log/info (i18n/trs "Starting restart for TK {0}" (app-log-id this))) |
682 | 816 | (notice-service-reloading) |
683 | 817 | (run-lifecycle-fns app-context s/stop "stop" (reverse ordered-services)) |
684 | 818 | (doseq [svc-id (keys services-by-id)] (swap! app-context assoc-in [:service-contexts svc-id] {})) |
| 819 | + (reset-readiness-tracking! services-by-id) |
685 | 820 | (run-lifecycle-fns app-context s/init "init" ordered-services) |
686 | 821 | (run-lifecycle-fns app-context s/start "start" ordered-services) |
687 | | - (notice-service-ready) |
| 822 | + (enable-ready-notifications-for-app! services-by-id) |
| 823 | + (notice-service-ready-if-uncoordinated! services-by-id) |
| 824 | + (log/info (i18n/trs "Finished restart for TK {0}" (app-log-id this))) |
688 | 825 | (inc-restart-counter! this) |
689 | 826 | this |
690 | 827 | (catch Throwable t |
|
0 commit comments