(ns metabase.api.cache (:require [metabase.api.common :as api] [metabase.api.macros :as api.macros] [metabase.config :as config] [metabase.models.cache-config :as cache-config] [metabase.premium-features.core :as premium-features] [metabase.util.cron :as u.cron] [metabase.util.i18n :refer [tru trun]] [metabase.util.malli :as mu] [metabase.util.malli.registry :as mr] [metabase.util.malli.schema :as ms] [toucan2.core :as t2])) | |
Data shape | |
(mr/def ::cache-strategy.base [:map [:type [:enum :nocache :ttl :duration :schedule]]]) | |
(mr/def ::cache-strategy.nocache [:map ; not closed due to a way it's used in tests for clarity [:type [:= :nocache]]]) | |
(mr/def ::cache-strategy.ttl [:map {:closed true} [:type [:= :ttl]] [:multiplier ms/PositiveInt] [:min_duration_ms ms/IntGreaterThanOrEqualToZero]]) | |
(mr/def ::cache-strategy.oss "Schema for a caching strategy (OSS)" [:and ::cache-strategy.base [:multi {:dispatch :type} [:nocache ::cache-strategy.nocache] [:ttl ::cache-strategy.ttl]]]) | |
(mr/def ::cache-strategy.ee.duration [:map {:closed true} [:type [:= :duration]] [:duration ms/PositiveInt] [:unit [:enum "hours" "minutes" "seconds" "days"]] [:refresh_automatically {:optional true} [:maybe :boolean]]]) | |
(mr/def ::cache-strategy.ee.schedule [:map {:closed true} [:type [:= :schedule]] [:schedule u.cron/CronScheduleString] [:refresh_automatically {:optional true} [:maybe :boolean]]]) | |
(mr/def ::cache-strategy.ee "Schema for a caching strategy in EE when we have an premium token with `:cache-granular-controls`." [:and ::cache-strategy.base [:multi {:dispatch :type} [:nocache ::cache-strategy.nocache] [:ttl ::cache-strategy.ttl] [:duration ::cache-strategy.ee.duration] [:schedule ::cache-strategy.ee.schedule]]]) | |
(mr/def ::cache-strategy (if config/ee-available? [:multi {:dispatch (fn [_value] (if (premium-features/has-feature? :cache-granular-controls) :ee :oss))} [:ee ::cache-strategy.ee] [:oss ::cache-strategy.oss]] ::cache-strategy.oss)) | |
(defn- assert-valid-models [model ids premium?] (cond (= model "root") (when-not (some zero? ids) (throw (ex-info (tru "Root configuration is only valid with model_id = 0") {:status-code 404 :model_id (first ids)}))) (not premium?) (throw (premium-features/ee-feature-error (tru "Granular Caching"))) :else (api/check-404 (t2/select-one (case model "database" :model/Database "dashboard" :model/Dashboard "question" :model/Card) :id [:in ids])))) | |
(defn- check-cache-access [model id] (if (or (nil? id) ;; sometimes its a sequence and we're going to check for settings access anyway (not (number? id)) (zero? id)) ;; if you're not accessing a concrete entity, you have to be an admin (api/check-superuser) (api/write-check (case model "database" :model/Database "dashboard" :model/Dashboard "question" :model/Card) id))) | |
(api.macros/defendpoint :get "/" "Return cache configuration." [_route-params {:keys [model collection id]} :- [:map [:model {:default ["root"]} (mu/with (ms/QueryVectorOf cache-config/CachingModel) {:description "Type of model"})] [:collection {:optional true} (mu/with [:maybe ms/PositiveInt] {:description "Collection id to filter results. Returns everything if not supplied."})] [:id {:optional true} (mu/with [:maybe ms/PositiveInt] {:description "Model id to get configuration for."})]]] (when (and (not (premium-features/enable-cache-granular-controls?)) (not= model ["root"])) (throw (premium-features/ee-feature-error (tru "Granular Caching")))) (check-cache-access (first model) id) {:data (cache-config/get-list model collection id)}) | |
(api.macros/defendpoint :put "/" "Store cache configuration." [_route-params _query-params {:keys [model model_id] :as config} :- [:map [:model cache-config/CachingModel] [:model_id ms/IntGreaterThanOrEqualToZero] [:strategy ::cache-strategy]]] (assert-valid-models model [model_id] (premium-features/enable-cache-granular-controls?)) (check-cache-access model model_id) {:id (cache-config/store! api/*current-user-id* config)}) | |
(api.macros/defendpoint :delete "/" "Delete cache configurations." [_route-params _query-params {:keys [model model_id]} :- [:map [:model cache-config/CachingModel] [:model_id (ms/QueryVectorOf ms/IntGreaterThanOrEqualToZero)]]] (assert-valid-models model model_id (premium-features/enable-cache-granular-controls?)) (doseq [id model_id] (check-cache-access model id)) (cache-config/delete! api/*current-user-id* model model_id) nil) | |
(api.macros/defendpoint :post "/invalidate" "Invalidate cache entries. Use it like `/api/cache/invalidate?database=1&dashboard=15` (any number of database/dashboard/question can be supplied). `&include=overrides` controls whenever you want to invalidate cache for a specific cache configuration without touching all nested configurations, or you want your invalidation to trickle down to every card." [_route-params {:keys [include database dashboard question]} :- [:map [:include {:optional true} [:maybe {:description "All cache configuration overrides should invalidate cache too"} [:= :overrides]]] [:database {:optional true} [:maybe {:description "A list of database ids"} (ms/QueryVectorOf ms/IntGreaterThanOrEqualToZero)]] [:dashboard {:optional true} [:maybe {:description "A list of dashboard ids"} (ms/QueryVectorOf ms/IntGreaterThanOrEqualToZero)]] [:question {:optional true} [:maybe {:description "A list of question ids"} (ms/QueryVectorOf ms/IntGreaterThanOrEqualToZero)]]]] (when-not (premium-features/enable-cache-granular-controls?) (throw (premium-features/ee-feature-error (tru "Granular Caching")))) (doseq [db-id database] (api/write-check :model/Database db-id)) (doseq [dashboard-id dashboard] (api/write-check :model/Dashboard dashboard-id)) (doseq [question-id question] (api/write-check :model/Card question-id)) (let [cnt (cache-config/invalidate! {:databases database :dashboards dashboard :questions question :with-overrides? (= include :overrides)})] {:status (if (= cnt -1) 404 200) :body {:count cnt :message (case [(= include :overrides) (if (pos? cnt) 1 cnt)] [true -1] (tru "Could not find any questions for the criteria you specified.") [true 0] (tru "No cached results to clear.") [true 1] (trun "Cleared a cached result." "Cleared {0} cached results." cnt) [false -1] (tru "Nothing to invalidate.") [false 0] (tru "No cache configuration to invalidate.") [false 1] (trun "Invalidated cache configuration." "Invalidated {0} cache configurations." cnt))}})) | |