From d2113a27e0a12406ffb1b3163c47d1014ae9590e Mon Sep 17 00:00:00 2001 From: Chris Truter <crisptrutski@users.noreply.github.com> Date: Tue, 23 Jan 2024 15:59:33 +0200 Subject: [PATCH] Fix potential deadlock when loading feature flags Fixes https://github.com/metabase/metabase/issues/38009 ### Description This makes the feature flag loading code more robust by making the function re-entrant. To understand how this function could deadlock before, see the description in https://github.com/metabase/metabase/pull/38027. It works by ensuring that the call to initialize the nonce comes from the same thread that initially locked on fetching the tokens. There is a subtle behavior change here - was allow for a 10s timeout on each call individually now. Since it is unlikely that both endpoints will timeout consecutively, and the combined wait time is still fairly short, I think this is OK. ### How to verify You can use the same steps as https://github.com/metabase/metabase/pull/38027, either of these PRs are sufficient to fix the bug. --- .../public_settings/premium_features.clj | 65 ++++++++++--------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/src/metabase/public_settings/premium_features.clj b/src/metabase/public_settings/premium_features.clj index af1067898c2..88ca1e5988a 100644 --- a/src/metabase/public_settings/premium_features.clj +++ b/src/metabase/public_settings/premium_features.clj @@ -96,15 +96,28 @@ [:trial {:optional true} :boolean] [:valid-thru {:optional true} ms/NonBlankString]]) ; ISO 8601 timestamp -(defn- fetch-token-and-parse-body - [token base-url] + +(defn- fetch-token-and-parse-body* + [token base-url site-uuid] (some-> (token-status-url token base-url) (http/get {:query-params {:users (cached-active-users-count) - :site-uuid (setting/get :site-uuid-for-premium-features-token-checks) + :site-uuid site-uuid :mb-version (:tag config/mb-version-info)}}) :body (json/parse-string keyword))) +(defn- fetch-token-and-parse-body + [token base-url site-uuid] + (let [fut (future (fetch-token-and-parse-body* token base-url site-uuid)) + result (deref fut fetch-token-status-timeout-ms ::timed-out)] + (if (not= result ::timed-out) + result + (do + (future-cancel fut) + {:valid false + :status (tru "Unable to validate token") + :error-details (tru "Token validation timed out.")})))) + (mu/defn ^:private fetch-token-status* :- TokenStatus "Fetch info about the validity of `token` from the MetaStore." [token :- :string] @@ -117,32 +130,26 @@ {:valid false :status "invalid" :error-details (trs "Token should be 64 hexadecimal characters.")}) - (let [fut (future - (try (fetch-token-and-parse-body token token-check-url) - (catch Exception e1 - (log/error e1 (trs "Error fetching token status from {0}:" token-check-url)) - ;; Try the fallback URL, which was the default URL prior to 45.2 - (try (fetch-token-and-parse-body token store-url) - ;; if there was an error fetching the token from both the normal and fallback URLs, log the - ;; first error and return a generic message about the token being invalid. This message - ;; will get displayed in the Settings page in the admin panel so we do not want something - ;; complicated - (catch Exception e2 - (log/error e2 (trs "Error fetching token status from {0}:" store-url)) - (let [body (u/ignore-exceptions (some-> (ex-data e1) :body (json/parse-string keyword)))] - (or - body - {:valid false - :status (tru "Unable to validate token") - :error-details (.getMessage e1)}))))))) - result (deref fut fetch-token-status-timeout-ms ::timed-out)] - (if (= result ::timed-out) - (do - (future-cancel fut) - {:valid false - :status (tru "Unable to validate token") - :error-details (tru "Token validation timed out.")}) - result)))) + (let [site-uuid (setting/get :site-uuid-for-premium-features-token-checks)] + (try (fetch-token-and-parse-body token token-check-url site-uuid) + (catch Exception e1 + ;; Unwrap exception from inside the future + (let [e1 (ex-cause e1)] + (log/error e1 (trs "Error fetching token status from {0}:" token-check-url)) + ;; Try the fallback URL, which was the default URL prior to 45.2 + (try (fetch-token-and-parse-body token store-url site-uuid) + ;; if there was an error fetching the token from both the normal and fallback URLs, log the + ;; first error and return a generic message about the token being invalid. This message + ;; will get displayed in the Settings page in the admin panel so we do not want something + ;; complicated + (catch Exception e2 + (log/error (ex-cause e2) (trs "Error fetching token status from {0}:" store-url)) + (let [body (u/ignore-exceptions (some-> (ex-data e1) :body (json/parse-string keyword)))] + (or + body + {:valid false + :status (tru "Unable to validate token") + :error-details (.getMessage e1)})))))))))) (def ^{:arglists '([token])} fetch-token-status "TTL-memoized version of `fetch-token-status*`. Caches API responses for 5 minutes. This is important to avoid making -- GitLab