The background worker which performs the analysis of queries, and updates the database in accordance. Restricts the CPU and database load corresponding to this analysis via a crude rate limiting algorithm that puts the worker to sleep such that it is active at most [[max-cpu-usage-fraction]] of the time.

(ns metabase.task.analyze-queries
  (:require
   [clojurewerkz.quartzite.jobs :as jobs]
   [clojurewerkz.quartzite.schedule.simple :as simple]
   [clojurewerkz.quartzite.triggers :as triggers]
   [metabase.public-settings :as public-settings]
   [metabase.query-analysis :as query-analysis]
   [metabase.query-analysis.failure-map :as failure-map]
   [metabase.task :as task]
   [metabase.util :as u]
   [metabase.util.log :as log])
  (:import
   (org.quartz DisallowConcurrentExecution)))
(set! *warn-on-reflection* true)
(def ^:private max-cpu-usage-fraction 0.2)
(def ^:private wait-ratio
  (/ (- 1 max-cpu-usage-fraction) max-cpu-usage-fraction))
(def ^:private fail-wait-ms (* 2 1000))
(def ^:private min-wait-ms 5)
(def ^:private max-wait-ms (* 10 1000))
(defn- wait-proportional ^long [time-taken-ms]
  (->> time-taken-ms
       (* wait-ratio)
       (max min-wait-ms)
       (min max-wait-ms)))
(defn- wait-fail ^long [time-taken-ms]
  (max fail-wait-ms (wait-proportional time-taken-ms)))
(defn- analyzer-loop* [stop-after next-card-id-fn]
  (try
    (loop [remaining stop-after]
      (let [card-or-id (next-card-id-fn)
            card-id    (u/the-id card-or-id)
            timer      (u/start-timer)
            card       (query-analysis/->analyzable card-or-id)]
        (when (public-settings/query-analysis-enabled)
          (if (failure-map/non-retryable? card)
            (log/warnf "Skipping analysis of Card %s as its query has caused failures in the past." card-id)
            (try
              (if (:error (query-analysis/analyze!* card))
                (failure-map/track-failure! card)
                (failure-map/track-success! card))
              (let [taken-ms (Math/ceil (u/since-ms timer))
                    sleep-ms (wait-proportional taken-ms)]
                (log/debugf "Query analysis for Card %s took %sms (incl. persisting)" card-id taken-ms)
                (log/debugf "Waiting %sms before analysing further cards" sleep-ms)
                (Thread/sleep sleep-ms))
              (catch Exception e
                (log/errorf e "Error analysing and updating query for Card %s" card-id)
                (failure-map/track-failure! card)
                (Thread/sleep (wait-fail (u/since-ms timer))))))
          (cond
            (nil? remaining) (recur nil)
            (> remaining 1)  (recur (dec remaining))))))
    (catch Exception e
      (log/error e "Unhandled error when attempting to analyse the next card in the queue"))))
(defn- analyzer-loop!
  ([]
   (analyzer-loop! nil))
  ([stop-after]
   (analyzer-loop* stop-after query-analysis/next-card-or-id!))
  ([stop-after queue]
   (analyzer-loop! stop-after queue Long/MAX_VALUE))
  ([stop-after queue timeout]
   (analyzer-loop* stop-after (partial query-analysis/next-card-or-id! queue timeout))))

Analyze

(jobs/defjob ^{DisallowConcurrentExecution true
               :doc                        }
  QueryAnalyzer [_ctx]
  (analyzer-loop!))
(defmethod task/init! ::BackfillQueryField [_]
  (let [job     (jobs/build
                 (jobs/of-type QueryAnalyzer)
                 (jobs/with-identity (jobs/key "metabase.task.analyze-queries.job")))
        trigger (triggers/build
                 (triggers/with-identity (triggers/key "metabase.task.analyze-queries.trigger"))
                 (triggers/with-schedule
                  (simple/schedule (simple/with-interval-in-minutes 1)))
                 (triggers/start-now))]
    (task/schedule-task! job trigger)))