diff --git a/src/metabase/driver/mongo.clj b/src/metabase/driver/mongo.clj
index 5296b2d0db7ecbe4504a4c088a461a802c6258e4..45f504ea3bc5898a6ce370e178e55c05e15f9093 100644
--- a/src/metabase/driver/mongo.clj
+++ b/src/metabase/driver/mongo.clj
@@ -66,14 +66,17 @@
   (cond
     ;; 1. url?
     (and (string? field-value)
-         (u/url? field-value)) :type/URL
+         (u/url? field-value))
+    :type/URL
+
     ;; 2. json?
     (and (string? field-value)
          (or (.startsWith "{" field-value)
-             (.startsWith "[" field-value))) (when-let [j (u/try-apply json/parse-string field-value)]
-                                               (when (or (map? j)
-                                                         (sequential? j))
-                                                 :type/SerializedJSON))))
+             (.startsWith "[" field-value)))
+    (when-let [j (u/ignore-exceptions (json/parse-string field-value))]
+      (when (or (map? j)
+                (sequential? j))
+        :type/SerializedJSON))))
 
 (defn- find-nested-fields [field-value nested-fields]
   (loop [[k & more-keys] (keys field-value)
diff --git a/src/metabase/models/setting.clj b/src/metabase/models/setting.clj
index 8c6029af06b7f52b934ed169da7a23cf64b58b2f..29a595b9e26a68ac848ae7b55fad76045e5fbce5 100644
--- a/src/metabase/models/setting.clj
+++ b/src/metabase/models/setting.clj
@@ -30,13 +30,20 @@
       (setting/all)"
   (:refer-clojure :exclude [get])
   (:require [cheshire.core :as json]
-            [clojure.string :as str]
+            [clojure
+             [core :as core]
+             [string :as str]]
+            [clojure.core.memoize :as memoize]
+            [clojure.java.jdbc :as jdbc]
             [clojure.tools.logging :as log]
             [environ.core :as env]
+            [honeysql.core :as hsql]
             [metabase
+             [db :as mdb]
              [events :as events]
              [util :as u]]
-            [puppetlabs.i18n.core :refer [tru]]
+            [metabase.util.honeysql-extensions :as hx]
+            [puppetlabs.i18n.core :refer [trs tru]]
             [schema.core :as s]
             [toucan
              [db :as db]
@@ -74,7 +81,8 @@
     setting-or-name
     (let [k (keyword setting-or-name)]
       (or (@registered-settings k)
-          (throw (Exception. (str (tru "Setting {0} does not exist.\nFound: {1}" k (sort (keys @registered-settings))))))))))
+          (throw (Exception.
+                  (str (tru "Setting {0} does not exist.\nFound: {1}" k (sort (keys @registered-settings))))))))))
 
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
@@ -84,12 +92,115 @@
 ;; Cache is a 1:1 mapping of what's in the DB
 ;; Cached lookup time is ~60µs, compared to ~1800µs for DB lookup
 
-(defonce ^:private cache
+(def ^:private cache
+  "Settings cache. Map of Setting key (string) -> Setting value (string)."
   (atom nil))
 
-(defn- restore-cache-if-needed! []
-  (when-not @cache
-    (reset! cache (db/select-field->field :key :value Setting))))
+;; CACHE SYNCHRONIZATION
+;;
+;; When running multiple Metabase instances (horizontal scaling), it is of course possible for one instance to update
+;; a Setting, and, since Settings are cached (to avoid tons of DB calls), for the other instances to then have an
+;; out-of-date cache. Thus we need a way for instances to know when their caches are out of date, so they can update
+;; them accordingly. Here is our solution:
+;;
+;; We will record the last time *any* Setting was updated in a special Setting called `settings-last-updated`.
+;;
+;; Since `settings-last-updated` itself is a Setting, it will get fetched as part of each instance's local cache; we
+;; can then periodically compare the locally cached value of `settings-last-updated` with the value in the DB. If our
+;; locally cached value is older than the one in the DB, we will flush our cache. When the cache is fetched again, it
+;; will have the up-to-date value.
+;;
+;; Because different machines can have out-of-sync clocks, we'll rely entirely on the application DB for caclulating
+;; and comparing values of `settings-last-updated`. Because the Setting table itself only stores text values, we'll
+;; need to cast it between TEXT and TIMESTAMP SQL types as needed.
+
+(def ^:private ^String settings-last-updated-key "settings-last-updated")
+
+(defn- update-settings-last-updated!
+  "Update the value of `settings-last-updated` in the DB; if the row does not exist, insert one."
+  []
+  (log/debug (trs "Updating value of settings-last-updated in DB..."))
+  ;; for MySQL, cast(current_timestamp AS char); for H2 & Postgres, cast(current_timestamp AS text)
+  (let [current-timestamp-as-string-honeysql (hx/cast (if (= (mdb/db-type) :mysql) :char :text)
+                                                      (hsql/raw "current_timestamp"))]
+    ;; attempt to UPDATE the existing row. If no row exists, `update-where!` will return false...
+    (or (db/update-where! Setting {:key settings-last-updated-key} :value current-timestamp-as-string-honeysql)
+        ;; ...at which point we will try to INSERT a new row. Note that it is entirely possible two instances can both
+        ;; try to INSERT it at the same time; one instance would fail because it would violate the PK constraint on
+        ;; `key`, and throw a SQLException. As long as one instance updates the value, we are fine, so we can go ahead
+        ;; and ignore that Exception if one is thrown.
+        (try
+          ;; Use `simple-insert!` because we do *not* want to trigger pre-insert behavior, such as encrypting `:value`
+          (db/simple-insert! Setting :key settings-last-updated-key, :value current-timestamp-as-string-honeysql)
+          (catch java.sql.SQLException e
+            ;; go ahead and log the Exception anyway on the off chance that it *wasn't* just a race condition issue
+            (log/error (tru "Error inserting new Setting:") (with-out-str (jdbc/print-sql-exception-chain e)))))))
+  ;; Now that we updated the value in the DB, go ahead and update our cached value as well, because we know about the
+  ;; changes
+  (swap! cache assoc settings-last-updated-key (db/select-one-field :value Setting :key settings-last-updated-key)))
+
+(defn- cache-out-of-date?
+  "Check whether our Settings cache is out of date. We know the cache is out of date if either of the following
+  conditions is true:
+
+   *  The cache is empty (the `cache` atom is `nil`), which of course means it needs to be updated
+   *  There is a value of `settings-last-updated` in the cache, and it is older than the value of in the DB. (There
+      will be no value until the first time a normal Setting is updated; thus if it is not yet set, we do not yet need
+      to invalidate our cache.)"
+  []
+  (log/debug (trs "Checking whether settings cache is out of date (requires DB call)..."))
+  (boolean
+   (or
+    ;; is the cache empty?
+    (not @cache)
+    ;; if not, get the cached value of `settings-last-updated`, and if it exists...
+    (when-let [last-known-update (core/get @cache settings-last-updated-key)]
+      ;; compare it to the value in the DB. This is done be seeing whether a row exists
+      ;; WHERE value > <local-value>
+      (db/select-one Setting
+        {:where [:and
+                 [:= :key settings-last-updated-key]
+                 [:> :value last-known-update]]})))))
+
+(def ^:private cache-update-check-interval-ms
+  "How often we should check whether the Settings cache is out of date (which requires a DB call)?"
+  ;; once a minute
+  (* 60 1000))
+
+(def ^:private ^{:arglists '([])} should-restore-cache?
+  "TTL-memoized version of `cache-out-of-date?`. Call this function to see whether we need to repopulate the cache with
+  values from the DB."
+  (memoize/ttl cache-out-of-date? :ttl/threshold cache-update-check-interval-ms))
+
+(def ^:private restore-cache-if-needed-lock (Object.))
+
+(defn- restore-cache-if-needed!
+  "Check whether we need to repopulate the cache with fresh values from the DB (because the cache is either empty or
+  known to be out-of-date), and do so if needed. This is intended to be called every time a Setting value is
+  retrieved, so it should be efficient; thus the calculation (`should-restore-cache?`) is itself TTL-memoized."
+  []
+  ;; There's a potential race condition here where two threads both call this at the exact same moment, and both get
+  ;; `true` when they call `should-restore-cache`, and then both simultaneously try to update the cache (or, one
+  ;; updates the cache, but the other calls `should-restore-cache?` and gets `true` before the other calls
+  ;; `memo-swap!` (see below))
+  ;;
+  ;; This is not desirable, since either situation would result in duplicate work. Better to just add a quick lock
+  ;; here so only one of them does it, since at any rate waiting for the other thread to finish the task in progress is
+  ;; certainly quicker than starting the task ourselves from scratch
+  (locking restore-cache-if-needed-lock
+    (when (should-restore-cache?)
+      (log/debug (trs "Refreshing Settings cache..."))
+      (reset! cache (db/select-field->field :key :value Setting))
+      ;; Now the cache is up-to-date. That is all good, but if we call `should-restore-cache?` again in a second it
+      ;; will still return `true`, because its result is memoized, and we would be on the hook to (again) update the
+      ;; cache. So go ahead and clear the memozied results for `should-restore-cache?`. The next time around when
+      ;; someone calls this it will cache the latest value (which should be `false`)
+      ;;
+      ;; NOTE: I tried using `memo-swap!` instead to set the cached response to `false` here, avoiding the extra DB
+      ;; call the next fn call would make, but it didn't seem to work correctly (I think it was still discarding the
+      ;; new value because of the TTL). So we will just stick with `memo-clear!` for now. (One extra DB call whenever
+      ;; the cache gets invalidated shouldn't be a huge deal)
+      (memoize/memo-clear! should-restore-cache?))))
 
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
@@ -100,14 +211,14 @@
   (name (:name (resolve-setting setting-or-name))))
 
 (defn- env-var-name
-  "Get the env var corresponding to SETTING-OR-NAME.
+  "Get the env var corresponding to `setting-or-name`.
    (This is used primarily for documentation purposes)."
   ^String [setting-or-name]
   (let [setting (resolve-setting setting-or-name)]
     (str "MB_" (str/upper-case (str/replace (setting-name setting) "-" "_")))))
 
 (defn env-var-value
-  "Get the value of SETTING-OR-NAME from the corresponding env var, if any.
+  "Get the value of `setting-or-name` from the corresponding env var, if any.
    The name of the Setting is converted to uppercase and dashes to underscores;
    for example, a setting named `default-domain` can be set with the env var `MB_DEFAULT_DOMAIN`."
   ^String [setting-or-name]
@@ -117,14 +228,14 @@
       v)))
 
 (defn- db-value
-  "Get the value, if any, of SETTING-OR-NAME from the DB (using / restoring the cache as needed)."
+  "Get the value, if any, of `setting-or-name` from the DB (using / restoring the cache as needed)."
   ^String [setting-or-name]
   (restore-cache-if-needed!)
   (clojure.core/get @cache (setting-name setting-or-name)))
 
 
 (defn get-string
-  "Get string value of SETTING-OR-NAME. This is the default getter for `String` settings; valuBis fetched as follows:
+  "Get string value of `setting-or-name`. This is the default getter for `String` settings; valuBis fetched as follows:
 
    1.  From the database (i.e., set via the admin panel), if a value is present;
    2.  From corresponding env var, if any;
@@ -144,32 +255,33 @@
     (case (str/lower-case string-value)
       "true"  true
       "false" false
-      (throw (Exception. (str (tru "Invalid value for string: must be either \"true\" or \"false\" (case-insensitive).")))))))
+      (throw (Exception.
+              (str (tru "Invalid value for string: must be either \"true\" or \"false\" (case-insensitive).")))))))
 
 (defn get-boolean
-  "Get boolean value of (presumably `:boolean`) SETTING-OR-NAME. This is the default getter for `:boolean` settings.
+  "Get boolean value of (presumably `:boolean`) `setting-or-name`. This is the default getter for `:boolean` settings.
    Returns one of the following values:
 
-   *  `nil`   if string value of SETTING-OR-NAME is unset (or empty)
-   *  `true`  if *lowercased* string value of SETTING-OR-NAME is `true`
-   *  `false` if *lowercased* string value of SETTING-OR-NAME is `false`."
+   *  `nil`   if string value of `setting-or-name` is unset (or empty)
+   *  `true`  if *lowercased* string value of `setting-or-name` is `true`
+   *  `false` if *lowercased* string value of `setting-or-name` is `false`."
   ^Boolean [setting-or-name]
   (string->boolean (get-string setting-or-name)))
 
 (defn get-integer
-  "Get integer value of (presumably `:integer`) SETTING-OR-NAME. This is the default getter for `:integer` settings."
+  "Get integer value of (presumably `:integer`) `setting-or-name`. This is the default getter for `:integer` settings."
   ^Integer [setting-or-name]
   (when-let [s (get-string setting-or-name)]
     (Integer/parseInt s)))
 
 (defn get-double
-  "Get double value of (presumably `:double`) SETTING-OR-NAME. This is the default getter for `:double` settings."
+  "Get double value of (presumably `:double`) `setting-or-name`. This is the default getter for `:double` settings."
   ^Double [setting-or-name]
   (when-let [s (get-string setting-or-name)]
     (Double/parseDouble s)))
 
 (defn get-json
-  "Get the string value of SETTING-OR-NAME and parse it as JSON."
+  "Get the string value of `setting-or-name` and parse it as JSON."
   [setting-or-name]
   (json/parse-string (get-string setting-or-name) keyword))
 
@@ -181,7 +293,7 @@
    :double  get-double})
 
 (defn get
-  "Fetch the value of SETTING-OR-NAME. What this means depends on the Setting's `:getter`; by default, this looks for
+  "Fetch the value of `setting-or-name`. What this means depends on the Setting's `:getter`; by default, this looks for
    first for a corresponding env var, then checks the cache, then returns the default value of the Setting, if any."
   [setting-or-name]
   ((:getter (resolve-setting setting-or-name))))
@@ -191,7 +303,11 @@
 ;;; |                                                      set!                                                      |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
-(defn- update-setting! [setting-name new-value]
+(defn- update-setting!
+  "Update an existing Setting. Used internally by `set-string!` below; do not use directly."
+  [setting-name new-value]
+  (assert (not= setting-name settings-last-updated-key)
+    (tru "You cannot update `settings-last-updated` yourself! This is done automatically."))
   ;; This is indeed a very annoying way of having to do things, but `update-where!` doesn't call `pre-update` (in case
   ;; it updates thousands of objects). So we need to manually trigger `pre-update` behavior by calling `do-pre-update`
   ;; so that `value` can get encrypted if `MB_ENCRYPTION_SECRET_KEY` is in use. Then take that possibly-encrypted
@@ -201,8 +317,7 @@
       :value maybe-encrypted-new-value)))
 
 (defn- set-new-setting!
-  "Insert a new row for a Setting with SETTING-NAME and SETTING-VALUE.
-   Takes care of resetting the cache if the insert fails for some reason."
+  "Insert a new row for a Setting. Used internally by `set-string!` below; do not use directly."
   [setting-name new-value]
   (try (db/insert! Setting
          :key   setting-name
@@ -211,13 +326,15 @@
        ;; and there's actually a row in the DB that's not in the cache for some reason. Go ahead and update the
        ;; existing value and log a warning
        (catch Throwable e
-         (log/warn "Error INSERTing a new Setting:" (.getMessage e)
-                   "\nAssuming Setting already exists in DB and updating existing value.")
+         (log/warn (tru "Error inserting a new Setting:") "\n"
+                   (.getMessage e) "\n"
+                   (tru "Assuming Setting already exists in DB and updating existing value."))
          (update-setting! setting-name new-value))))
 
 (s/defn set-string!
-  "Set string value of SETTING-OR-NAME. A `nil` or empty NEW-VALUE can be passed to unset (i.e., delete)
-   SETTING-OR-NAME."
+  "Set string value of `setting-or-name`. A `nil` or empty `new-value` can be passed to unset (i.e., delete)
+  `setting-or-name`. String-type settings use this function directly; all other types ultimately call this (e.g.
+  `set-boolean!` eventually calls `set-string!`). Returns the `new-value`."
   [setting-or-name, new-value :- (s/maybe s/Str)]
   (let [new-value    (when (seq new-value)
                        new-value)
@@ -235,11 +352,14 @@
     (if new-value
       (swap! cache assoc  setting-name new-value)
       (swap! cache dissoc setting-name))
+    ;; Record the fact that a Setting has been updated so eventaully other instances (if applicable) find out about it
+    (update-settings-last-updated!)
+    ;; Now return the `new-value`.
     new-value))
 
 (defn set-boolean!
-  "Set the value of boolean SETTING-OR-NAME. NEW-VALUE can be nil, a boolean, or a string representation of one, such
-   as `\"true\"` or `\"false\"` (these strings are case-insensitive)."
+  "Set the value of boolean `setting-or-name`. `new-value` can be nil, a boolean, or a string representation of one,
+  such as `\"true\"` or `\"false\"` (these strings are case-insensitive)."
   [setting-or-name new-value]
   (set-string! setting-or-name (if (string? new-value)
                                  (set-boolean! setting-or-name (string->boolean new-value))
@@ -249,7 +369,7 @@
                                    nil   nil))))
 
 (defn set-integer!
-  "Set the value of integer SETTING-OR-NAME."
+  "Set the value of integer `setting-or-name`."
   [setting-or-name new-value]
   (set-string! setting-or-name (when new-value
                                  (assert (or (integer? new-value)
@@ -258,7 +378,7 @@
                                  (str new-value))))
 
 (defn set-double!
-  "Set the value of double SETTING-OR-NAME."
+  "Set the value of double `setting-or-name`."
   [setting-or-name new-value]
   (set-string! setting-or-name (when new-value
                                  (assert (or (float? new-value)
@@ -267,7 +387,7 @@
                                  (str new-value))))
 
 (defn set-json!
-  "Serialize NEW-VALUE for SETTING-OR-NAME as a JSON string and save it."
+  "Serialize `new-value` for `setting-or-name` as a JSON string and save it."
   [setting-or-name new-value]
   (set-string! setting-or-name (when new-value
                                  (json/generate-string new-value))))
@@ -280,7 +400,7 @@
    :double  set-double!})
 
 (defn set!
-  "Set the value of SETTING-OR-NAME. What this means depends on the Setting's `:setter`; by default, this just updates
+  "Set the value of `setting-or-name`. What this means depends on the Setting's `:setter`; by default, this just updates
    the Settings cache and writes its value to the DB.
 
     (set :mandrill-api-key \"xyz123\")
@@ -297,7 +417,7 @@
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
 (defn register-setting!
-  "Register a new `Setting` with a map of `SettingDefinition` attributes.
+  "Register a new Setting with a map of `SettingDefinition` attributes.
    This is used internally be `defsetting`; you shouldn't need to use it yourself."
   [{setting-name :name, setting-type :type, default :default, :as setting}]
   (u/prog1 (let [setting-type (s/validate Type (or setting-type :string))]
@@ -326,21 +446,21 @@
    ;; Turn on auto-complete-mode in Emacs and see for yourself!
    :doc (str/join "\n" [        description
                                 ""
-                        (format "`%s` is a %s `Setting`. You can get its value by calling:" (setting-name setting) (name setting-type))
+                        (format "`%s` is a %s Setting. You can get its value by calling:" (setting-name setting) (name setting-type))
                                 ""
-                        (format "    (%s)"                                                  (setting-name setting))
+                        (format "    (%s)"                                                (setting-name setting))
                                 ""
                                 "and set its value by calling:"
                                 ""
-                        (format "    (%s <new-value>)"                                      (setting-name setting))
+                        (format "    (%s <new-value>)"                                    (setting-name setting))
                                 ""
-                        (format "You can also set its value with the env var `%s`."         (env-var-name setting))
+                        (format "You can also set its value with the env var `%s`."       (env-var-name setting))
                                 ""
                                 "Clear its value by calling:"
                                 ""
-                        (format "    (%s nil)"                                              (setting-name setting))
+                        (format "    (%s nil)"                                            (setting-name setting))
                                 ""
-                        (format "Its default value is `%s`."                                (if (nil? default) "nil" default))])})
+                        (format "Its default value is `%s`."                              (if (nil? default) "nil" default))])})
 
 
 
@@ -357,7 +477,7 @@
      (metabase.models.setting/set! setting new-value))))
 
 (defmacro defsetting
-  "Defines a new `Setting` that will be added to the DB at some point in the future.
+  "Defines a new Setting that will be added to the DB at some point in the future.
    Conveniently can be used as a getter/setter as well:
 
      (defsetting mandrill-api-key \"API key for Mandrill.\")
@@ -373,7 +493,7 @@
    *  `:default`   - The default value of the setting. (default: `nil`)
    *  `:type`      - `:string` (default), `:boolean`, `:integer`, or `:json`. Non-`:string` settings have special
                      default getters and setters that automatically coerce values to the correct types.
-   *  `:internal?` - This `Setting` is for internal use and shouldn't be exposed in the UI (i.e., not returned by the
+   *  `:internal?` - This Setting is for internal use and shouldn't be exposed in the UI (i.e., not returned by the
                       corresponding endpoints). Default: `false`
    *  `:getter`    - A custom getter fn, which takes no arguments. Overrides the default implementation. (This can in
                      turn call functions in this namespace like `get-string` or `get-boolean` to invoke the default
@@ -397,7 +517,7 @@
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
 (defn set-many!
-  "Set the value of several `Settings` at once.
+  "Set the value of several Settings at once.
 
     (set-all {:mandrill-api-key \"xyz123\", :another-setting \"ABC\"})"
   [settings]
diff --git a/src/metabase/util.clj b/src/metabase/util.clj
index dca42ebe128c2bcb31233c135a71ea401c10647b..34e204693edada38097b50be687eaee91bf1e061 100644
--- a/src/metabase/util.clj
+++ b/src/metabase/util.clj
@@ -4,9 +4,7 @@
              [data :as data]
              [pprint :refer [pprint]]
              [string :as s]]
-            [clojure.java
-             [classpath :as classpath]
-             [jdbc :as jdbc]]
+            [clojure.java.classpath :as classpath]
             [clojure.math.numeric-tower :as math]
             [clojure.tools.logging :as log]
             [clojure.tools.namespace.find :as ns-find]
@@ -15,7 +13,6 @@
             [puppetlabs.i18n.core :as i18n :refer [trs]]
             [ring.util.codec :as codec])
   (:import [java.net InetAddress InetSocketAddress Socket]
-           java.sql.SQLException
            [java.text Normalizer Normalizer$Form]))
 
 ;; This is the very first log message that will get printed.  It's here because this is one of the very first
@@ -290,45 +287,6 @@
                                                          :when (re-find #"metabase" s)]
                                                      (s/replace s #"^metabase\." ""))))})
 
-(defn wrap-try-catch
-  "Returns a new function that wraps F in a `try-catch`. When an exception is caught, it is logged
-   with `log/error` and returns `nil`."
-  ([f]
-   (wrap-try-catch f nil))
-  ([f f-name]
-   (let [exception-message (if f-name
-                             (format "Caught exception in %s: " f-name)
-                             "Caught exception: ")]
-     (fn [& args]
-       (try
-         (apply f args)
-         (catch SQLException e
-           (log/error (format-color 'red "%s\n%s\n%s"
-                                    exception-message
-                                    (with-out-str (jdbc/print-sql-exception-chain e))
-                                    (pprint-to-str (filtered-stacktrace e)))))
-         (catch Throwable e
-           (log/error (format-color 'red "%s %s\n%s"
-                                    exception-message
-                                    (or (.getMessage e) e)
-                                    (pprint-to-str (filtered-stacktrace e))))))))))
-
-(defn try-apply
-  "Like `apply`, but wraps F inside a `try-catch` block and logs exceptions caught.
-   (This is actaully more flexible than `apply` -- the last argument doesn't have to be
-   a sequence:
-
-     (try-apply vector :a :b [:c :d]) -> [:a :b :c :d]
-     (apply vector :a :b [:c :d])     -> [:a :b :c :d]
-     (try-apply vector :a :b :c :d)   -> [:a :b :c :d]
-     (apply vector :a :b :c :d)       -> Not ok - :d is not a sequence
-
-   This allows us to use `try-apply` in more situations than we'd otherwise be able to."
-  [^clojure.lang.IFn f & args]
-  (apply (wrap-try-catch f) (concat (butlast args) (if (sequential? (last args))
-                                                     (last args)
-                                                     [(last args)]))))
-
 (defn deref-with-timeout
   "Call `deref` on a FUTURE and throw an exception if it takes more than TIMEOUT-MS."
   [futur timeout-ms]
diff --git a/src/metabase/util/date.clj b/src/metabase/util/date.clj
index 23b2775a420bca234a70dd8be031577966c467dd..2ad043a84d29ea060b1314144319add26eca8644 100644
--- a/src/metabase/util/date.clj
+++ b/src/metabase/util/date.clj
@@ -1,4 +1,5 @@
 (ns metabase.util.date
+  "Utility functions for working with datetimes of different types, and other related tasks."
   (:require [clj-time
              [coerce :as coerce]
              [core :as t]
diff --git a/src/metabase/util/encryption.clj b/src/metabase/util/encryption.clj
index bcd2624ad6dfd2930e7ca4d41573945867ca68a9..7f541c43dc42fa9ab07c5fe94841c0a3818232c9 100644
--- a/src/metabase/util/encryption.clj
+++ b/src/metabase/util/encryption.clj
@@ -87,7 +87,7 @@
            (log/error
             (trs "Cannot decrypt encrypted credentials. Have you changed or forgot to set MB_ENCRYPTION_SECRET_KEY?")
             (.getMessage e)
-            (u/filtered-stacktrace e))
+            (u/pprint-to-str (u/filtered-stacktrace e)))
            ;; otherwise return S without decrypting. It's probably not decrypted in the first place
            s)))
      s)))
diff --git a/test/metabase/models/setting_test.clj b/test/metabase/models/setting_test.clj
index b4a76c5eb2b5a7e05be85d31a77c8153cb8ef4de..c7c55ef06ac6b7eb920b6d8b3b4acf98c50f3def 100644
--- a/test/metabase/models/setting_test.clj
+++ b/test/metabase/models/setting_test.clj
@@ -1,8 +1,12 @@
 (ns metabase.models.setting-test
-  (:require [expectations :refer :all]
+  (:require [clojure.core.memoize :as memoize]
+            [expectations :refer :all]
+            [honeysql.core :as hsql]
+            [metabase
+             [db :as mdb]
+             [util :as u]]
             [metabase.models.setting :as setting :refer [defsetting Setting]]
             [metabase.test.util :refer :all]
-            [metabase.util :as u]
             [metabase.util
              [encryption :as encryption]
              [encryption-test :as encryption-test]]
@@ -283,7 +287,7 @@
     ;; restore the cache
     ((resolve 'metabase.models.setting/restore-cache-if-needed!))
     ;; now set a value for the `toucan-name` setting the wrong way
-    (db/insert! setting/Setting {:key "toucan-name", :value "Rasta"})
+    (db/insert! setting/Setting {:key "toucan-name", :value "Reggae"})
     ;; ok, now try to set the Setting the correct way
     (toucan-name "Banana Beak")
     ;; ok, make sure the setting was set
@@ -317,3 +321,126 @@
   (encryption-test/with-secret-key nil
     (toucan-name "Sad Can")
     (actual-value-in-db :toucan-name)))
+
+
+;;; --------------------------------------------- Cache Synchronization ----------------------------------------------
+
+(def ^:private settings-last-updated-key @(resolve 'metabase.models.setting/settings-last-updated-key))
+
+(defn- clear-settings-last-updated-value-in-db! []
+  (db/simple-delete! Setting {:key settings-last-updated-key}))
+
+(defn- settings-last-updated-value-in-db []
+  (db/select-one-field :value Setting :key settings-last-updated-key))
+
+(defn- clear-cache! []
+  (reset! @(resolve 'metabase.models.setting/cache) nil))
+
+(defn- settings-last-updated-value-in-cache []
+  (get @@(resolve 'metabase.models.setting/cache) settings-last-updated-key))
+
+(defn- update-settings-last-updated-value-in-db!
+  "Simulate a different instance updating the value of `settings-last-updated` in the DB by updating its value without
+  updating our locally cached value.."
+  []
+  (db/update-where! Setting {:key settings-last-updated-key}
+    :value (hsql/raw (case (mdb/db-type)
+                       ;; make it one second in the future so we don't end up getting an exact match when we try to test
+                       ;; to see if things update below
+                       :h2       "cast(dateadd('second', 1, current_timestamp) AS text)"
+                       :mysql    "cast((current_timestamp + interval 1 second) AS char)"
+                       :postgres "cast((current_timestamp + interval '1 second') AS text)"))))
+
+(defn- simulate-another-instance-updating-setting! [setting-name new-value]
+  (db/update-where! Setting {:key (name setting-name)} :value new-value)
+  (update-settings-last-updated-value-in-db!))
+
+(defn- flush-memoized-results-for-should-restore-cache!
+  "Remove any memoized results for `should-restore-cache?`, so we can test `restore-cache-if-needed!` works the way we'd
+  expect."
+  []
+  (memoize/memo-clear! @(resolve 'metabase.models.setting/should-restore-cache?)))
+
+;; When I update a Setting, does it set/update `settings-last-updated`?
+(expect
+  (do
+    (clear-settings-last-updated-value-in-db!)
+    (toucan-name "Bird Can")
+    (string? (settings-last-updated-value-in-db))))
+
+;; ...and is the value updated in the cache as well?
+(expect
+  (do
+    (clear-cache!)
+    (toucan-name "Bird Can")
+    (string? (settings-last-updated-value-in-cache))))
+
+;; ...and if I update it again, will the value be updated?
+(expect
+  (do
+    (clear-settings-last-updated-value-in-db!)
+    (toucan-name "Bird Can")
+    (let [first-value (settings-last-updated-value-in-db)]
+      ;; MySQL only has the resolution of one second on the timestamps here so we should wait that long to make sure
+      ;; the second-value actually ends up being greater than the first
+      (Thread/sleep 1200)
+      (toucan-name "Bird Can")
+      (let [second-value (settings-last-updated-value-in-db)]
+        ;; first & second values should be different, and first value should be "less than" the second value
+        (and (not= first-value second-value)
+             (neg? (compare first-value second-value)))))))
+
+;; If there is no cache, it should be considered out of date!`
+(expect
+  (do
+    (clear-cache!)
+    (#'setting/cache-out-of-date?)))
+
+;; But if I set a setting, it should cause the cache to be populated, and be up-to-date
+(expect
+  false
+  (do
+    (clear-cache!)
+    (toucan-name "Reggae Toucan")
+    (#'setting/cache-out-of-date?)))
+
+;; If another instance updates a Setting, `cache-out-of-date?` should return `true` based on DB comparisons...
+;; be true!
+(expect
+  (do
+    (clear-cache!)
+    (toucan-name "Reggae Toucan")
+    (simulate-another-instance-updating-setting! :toucan-name "Bird Can")
+    (#'setting/cache-out-of-date?)))
+
+;; of course, `restore-cache-if-needed!` should use TTL memoization, and the cache should not get updated right away
+;; even if another instance updates a value...
+(expect
+  "Sam"
+  (do
+    (flush-memoized-results-for-should-restore-cache!)
+    (clear-cache!)
+    (toucan-name "Sam")                 ; should restore cache, and put in {"toucan-name" "Sam"}
+    ;; since we cleared the memoized value of `should-restore-cache?` call it again to make sure it gets set to
+    ;; `false` as it would IRL if we were calling it again from the same instance
+    (#'setting/should-restore-cache?)
+    ;; now have another instance change the value
+    (simulate-another-instance-updating-setting! :toucan-name "Bird Can")
+    ;; our cache should not be updated yet because it's on a TTL
+    (toucan-name)))
+
+;; ...and when it comes time to check our cache for updating (when calling `restore-cache-if-needed!`, it should get
+;; the updated value. (we're not actually going to wait a minute for the memoized values of `should-restore-cache?` to
+;; be invalidated, so we will manually flush the memoization cache to simulate it happening)
+(expect
+  "Bird Can"
+  (do
+    (clear-cache!)
+    (toucan-name "Reggae Toucan")
+    (simulate-another-instance-updating-setting! :toucan-name "Bird Can")
+    (flush-memoized-results-for-should-restore-cache!)
+    ;; calling `toucan-name` will call `restore-cache-if-needed!`, which will in turn call `should-restore-cache?`.
+    ;; Since memoized value is no longer present, this should call `cache-out-of-date?`, which checks the DB; it will
+    ;; detect a cache out-of-date situation and flush the cache as appropriate, giving us the updated value when we
+    ;; call! :wow:
+    (toucan-name)))