|
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 |
|
469 | 486 | "Higher-order function to execute application logic and trigger shutdown in |
470 | 487 | the event of an exception")) |
471 | 488 |
|
| 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 | + |
472 | 503 | (schema/defn shutdown-service |
473 | 504 | "Provides various functions for triggering application shutdown programatically. |
474 | 505 | Primarily intended to serve application services, though TrapperKeeper also uses |
|
497 | 528 | (shutdown-on-error [this svc-id f] (shutdown-on-error* shutdown-reason-promise app-context svc-id f)) |
498 | 529 | (shutdown-on-error [this svc-id f on-error] (shutdown-on-error* shutdown-reason-promise app-context svc-id f on-error)))) |
499 | 530 |
|
| 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 | + |
500 | 585 | (schema/defn ^:always-validate shutdown! :- [Throwable] |
501 | 586 | "Perform shutdown calling the `stop` lifecycle function on each service, |
502 | 587 | in reverse order (to account for dependency relationships). |
|
633 | 718 | service-refs (atom {}) |
634 | 719 | services (conj services |
635 | 720 | (config-service config-data-fn) |
| 721 | + (readiness-service) |
636 | 722 | (initialize-shutdown-service! app-context |
637 | 723 | shutdown-reason-promise)) |
638 | 724 | service-map (apply merge (map s/service-map services)) |
|
660 | 746 | (a/check-for-errors! [this] (throw-app-error-if-exists! |
661 | 747 | this)) |
662 | 748 | (a/init [this] |
| 749 | + (reset-readiness-tracking! services-by-id) |
663 | 750 | (run-lifecycle-fns app-context s/init "init" ordered-services) |
664 | 751 | this) |
665 | 752 | (a/start [this] |
666 | 753 | (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) |
668 | 756 | (inc-restart-counter! this) |
669 | 757 | this) |
670 | 758 | (a/stop [this] |
|
682 | 770 | (notice-service-reloading) |
683 | 771 | (run-lifecycle-fns app-context s/stop "stop" (reverse ordered-services)) |
684 | 772 | (doseq [svc-id (keys services-by-id)] (swap! app-context assoc-in [:service-contexts svc-id] {})) |
| 773 | + (reset-readiness-tracking! services-by-id) |
685 | 774 | (run-lifecycle-fns app-context s/init "init" ordered-services) |
686 | 775 | (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) |
688 | 778 | (inc-restart-counter! this) |
689 | 779 | this |
690 | 780 | (catch Throwable t |
|
0 commit comments