Skip to content
Snippets Groups Projects
Unverified Commit bb3194ba authored by Alexander Solovyov's avatar Alexander Solovyov Committed by GitHub
Browse files

move cache business logic to model from api (#41677)

parent 03db4e38
No related branches found
No related tags found
No related merge requests found
......@@ -2,16 +2,11 @@
(:require
[clojure.walk :as walk]
[compojure.core :refer [GET]]
[java-time.api :as t]
[metabase.api.common :as api]
[metabase.api.common.validation :as validation]
[metabase.db.query :as mdb.query]
[metabase.events :as events]
[metabase.models :refer [CacheConfig Card]]
[metabase.models.cache-config :as cache-config]
[metabase.public-settings.premium-features :as premium-features :refer [defenterprise]]
[metabase.util :as u]
[metabase.util.i18n :refer [tru]]
[metabase.util.i18n :refer [tru trun]]
[metabase.util.malli.schema :as ms]
[toucan2.core :as t2]))
......@@ -49,7 +44,6 @@
[]
(drop-internal-fields (CacheStrategy)))
(def ^:private CachingModel [:enum "root" "database" "dashboard" "question"])
(defn- assert-valid-models [model ids premium?]
(cond
......@@ -68,98 +62,40 @@
"question" :model/Card)
:id [:in ids]))))
(defn- audit-caching-change! [id prev new]
(events/publish-event!
:event/cache-config-update
{:user-id api/*current-user-id*
:model :model/CacheConfig
:model-id id
:details {:model (or (:model prev) (:model new))
:model-id (or (:model_id prev) (:model_id new))
:old-value (dissoc prev :model :model_id)
:new-value (dissoc new :model :model_id)}}))
(api/defendpoint GET "/"
"Return cache configuration."
[:as {{:strs [model collection]
:or {model "root"}}
:query-params}]
{model (ms/QueryVectorOf CachingModel)
{model (ms/QueryVectorOf cache-config/CachingModel)
;; note that `nil` in `collection` means all configurations not scoped to any particular collection
collection [:maybe ms/PositiveInt]}
(validation/check-has-application-permission :setting)
(let [items (if (premium-features/enable-cache-granular-controls?)
(t2/select :model/CacheConfig
:model [:in model]
{:left-join [:report_card [:and
[:= :model [:inline "question"]]
[:= :model_id :report_card.id]
[:= :report_card.collection_id collection]]
:report_dashboard [:and
[:= :model [:inline "dashboard"]]
[:= :model_id :report_dashboard.id]
[:= :report_dashboard.collection_id collection]]]
:where [:case
[:= :model [:inline "question"]] [:!= :report_card.id nil]
[:= :model [:inline "dashboard"]] [:!= :report_dashboard.id nil]
:else true]})
(t2/select :model/CacheConfig :model "root"))]
{:data (mapv cache-config/row->config items)}))
(when (and (not (premium-features/enable-cache-granular-controls?))
(not= model ["root"]))
(throw (premium-features/ee-feature-error (tru "Granular Caching"))))
{:data (cache-config/get-list model collection)})
(api/defendpoint PUT "/"
"Store cache configuration."
[:as {{:keys [model model_id strategy] :as config} :body}]
{model CachingModel
{model cache-config/CachingModel
model_id ms/IntGreaterThanOrEqualToZero
strategy (CacheStrategyAPI)}
(validation/check-has-application-permission :setting)
(assert-valid-models model [model_id] (premium-features/enable-cache-granular-controls?))
(t2/with-transaction [_tx]
(let [data (cache-config/config->row config)
current (t2/select-one :model/CacheConfig :model model :model_id model_id {:for :update})]
{:id (u/prog1 (mdb.query/update-or-insert! :model/CacheConfig {:model model :model_id model_id}
(constantly data))
(audit-caching-change! <> current data))})))
{:id (cache-config/store! api/*current-user-id* config)})
(api/defendpoint DELETE "/"
"Delete cache configuration."
"Delete cache configurations."
[:as {{:keys [model model_id]} :body}]
{model CachingModel
{model cache-config/CachingModel
model_id (ms/QueryVectorOf ms/IntGreaterThanOrEqualToZero)}
(validation/check-has-application-permission :setting)
(assert-valid-models model model_id (premium-features/enable-cache-granular-controls?))
(when-let [current (seq (t2/select :model/CacheConfig :model model :model_id [:in model_id]))]
(t2/delete! :model/CacheConfig :model model :model_id [:in model_id])
(doseq [item current]
(audit-caching-change! (:id item)
(select-keys item [:strategy :config :model :model_id])
nil)))
(cache-config/delete! api/*current-user-id* model model_id)
nil)
(defn- invalidate-cache-configs [database dashboard question]
(let [conditions (for [[k vs] [[:database database]
[:dashboard dashboard]
[:question question]]
v vs]
[:and [:= :model (name k)] [:= :model_id v]])]
(if (empty? conditions)
0
;; using JVM date rather than DB time since it's what are used in cache tasks
(t2/query-one {:update (t2/table-name CacheConfig)
:set {:invalidated_at (t/offset-date-time)}
:where (into [:or] conditions)}))))
(defn- invalidate-cards [database dashboard question]
(let [card-ids (concat
question
(when (seq database)
(t2/select-fn-vec :id [:model/Card :id] :database_id [:in database]))
(when (seq dashboard)
(t2/select-fn-vec :card_id [:model/DashboardCard :card_id] :dashboard_id [:in dashboard])))]
(if (empty? card-ids)
0
(t2/update! Card :id [:in card-ids]
{:cache_invalidated_at (t/offset-date-time)}))))
(api/defendpoint POST "/invalidate"
"Invalidate cache entries.
......@@ -178,19 +114,18 @@
(when-not (premium-features/enable-cache-granular-controls?)
(throw (premium-features/ee-feature-error (tru "Granular Caching"))))
(let [cnt (if (= include :overrides)
(invalidate-cards database dashboard question)
(invalidate-cache-configs database dashboard question))]
(case [(= include :overrides) (pos? cnt)]
[true false] {:status 404
:body {:message (tru "Could not find any cached questions for the given database, dashboard, or questions ids.")}}
[true true] {:status 200
:body {:message (tru "Invalidated cache for {0} question(s)." cnt)
:count cnt}}
[false false] {:status 404
:body {:message (tru "Could not find a cache configuration to invalidate.")}}
[false true] {:status 200
:body {:message (tru "Invalidated {0} cache configuration(s)." cnt)
:count cnt}})))
(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 a question for the criteria you've specified.")
[true 0] (tru "No cached results to invalidate.")
[true 1] (trun "Invalidated a cached result." "Invalidated {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))}}))
(api/define-routes)
......@@ -3,10 +3,15 @@
(:require
[java-time.api :as t]
[medley.core :as m]
[metabase.db.query :as mdb.query]
[metabase.events :as events]
[metabase.models.interface :as mi]
[metabase.util :as u]
[methodical.core :as methodical]
[toucan2.core :as t2]))
(def CachingModel "Caching is configurable for those models" [:enum "root" "database" "dashboard" "question"])
(def CacheConfig "Cache configuration" :model/CacheConfig)
(doto :model/CacheConfig
......@@ -20,6 +25,17 @@
:config mi/transform-json
:state mi/transform-json})
(defn- audit-caching-change! [user-id id prev new]
(events/publish-event!
:event/cache-config-update
{:user-id user-id
:model :model/CacheConfig
:model-id id
:details {:model (or (:model prev) (:model new))
:model-id (or (:model_id prev) (:model_id new))
:old-value (dissoc prev :model :model_id)
:new-value (dissoc new :model :model_id)}}))
;;; API
(defn root-strategy
......@@ -50,3 +66,80 @@
:model_id model_id
:strategy (:type strategy)
:config (dissoc strategy :type)})
(defn get-list
"Get a list of cache configurations for given `models` and a `collection`."
[models collection]
(->> (t2/select
:model/CacheConfig
:model [:in models]
{:left-join [:report_card [:and
[:= :model [:inline "question"]]
[:= :model_id :report_card.id]
[:= :report_card.collection_id collection]]
:report_dashboard [:and
[:= :model [:inline "dashboard"]]
[:= :model_id :report_dashboard.id]
[:= :report_dashboard.collection_id collection]]]
:where [:case
[:= :model [:inline "question"]] [:!= :report_card.id nil]
[:= :model [:inline "dashboard"]] [:!= :report_dashboard.id nil]
:else true]})
(mapv row->config)))
(defn store!
"Store cache configuration in DB."
[user-id {:keys [model model_id] :as config}]
(t2/with-transaction [_tx]
(let [data (config->row config)
current (t2/select-one :model/CacheConfig :model model :model_id model_id {:for :update})]
(u/prog1 (mdb.query/update-or-insert! :model/CacheConfig {:model model :model_id model_id}
(constantly data))
(audit-caching-change! user-id <> current data)))))
(defn delete!
"Delete cache configuration (possibly multiple), identified by a `model` and a vector of `model-ids`."
[user-id model model-ids]
(when-let [current (seq (t2/select :model/CacheConfig :model model :model_id [:in model-ids]))]
(t2/delete! :model/CacheConfig :model model :model_id [:in model-ids])
(doseq [item current]
(audit-caching-change! user-id
(:id item)
(select-keys item [:strategy :config :model :model_id])
nil))))
;;; Invalidation
(defn- invalidate-cards [databases dashboards questions]
(let [card-ids (concat
questions
(when (seq databases)
(t2/select-fn-vec :id [:model/Card :id] :database_id [:in databases]))
(when (seq dashboards)
(t2/select-fn-vec :card_id [:model/DashboardCard :card_id] :dashboard_id [:in dashboards])))]
(if (empty? card-ids)
-1
(t2/update! :model/Card :id [:in card-ids]
{:cache_invalidated_at (t/offset-date-time)}))))
(defn- invalidate-cache-configs [databases dashboards questions]
(let [conditions (for [[k vs] [[:database databases]
[:dashboard dashboards]
[:question questions]]
v vs]
[:and [:= :model (name k)] [:= :model_id v]])]
(if (empty? conditions)
-1
;; using JVM date rather than DB time since it's what are used in cache tasks
(t2/query-one {:update (t2/table-name CacheConfig)
:set {:invalidated_at (t/offset-date-time)}
:where (into [:or] conditions)}))))
(defn invalidate!
"Invalidate cache configuration. Accepts lists of ids for different types of models. If `with-overrides?` is passed,
then invalidates cache on each individual card suitable for criteria."
[{:keys [databases dashboards questions with-overrides?]}]
(if with-overrides?
(invalidate-cards databases dashboards questions)
(invalidate-cache-configs databases dashboards questions)))
......@@ -15,7 +15,10 @@
(mt/user-http-request :crowberto :put 402 "cache/"
{:model "question"
:model_id 1
:strategy {:type "nocache"}}))))
:strategy {:type "nocache"}})))
(is (= "Granular Caching is a paid feature not currently available to your instance. Please upgrade to use it. Learn more at metabase.com/upgrade/"
(mt/user-http-request :crowberto :get 402 "cache/"
:model "question"))))
(testing "Can operate on root settings though"
(is (mt/user-http-request :crowberto :put 200 "cache/"
{:model "root"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment