diff --git a/resources/migrations/000_migrations.yaml b/resources/migrations/000_migrations.yaml index 1927dce96d75ea0166612ce6beb37fbcd8df1d99..cc4aadbceafab5c518d433881b462c68c5fe8eca 100644 --- a/resources/migrations/000_migrations.yaml +++ b/resources/migrations/000_migrations.yaml @@ -3649,6 +3649,7 @@ databaseChangeLog: - changeSet: id: 56 author: wwwiiilll + comment: 'Added 0.25.0' changes: - addColumn: tableName: core_user @@ -3662,6 +3663,7 @@ databaseChangeLog: - changeSet: id: 57 author: camsaul + comment: 'Added 0.25.0' changes: - addColumn: tableName: report_card @@ -3673,6 +3675,7 @@ databaseChangeLog: - changeSet: id: 58 author: senior + comment: 'Added 0.25.0' changes: - createTable: tableName: dimension @@ -3744,6 +3747,7 @@ databaseChangeLog: - changeSet: id: 59 author: camsaul + comment: 'Added 0.26.0' changes: - addColumn: tableName: metabase_field @@ -3752,3 +3756,25 @@ databaseChangeLog: name: fingerprint type: text remarks: 'Serialized JSON containing non-identifying information about this Field, such as min, max, and percent JSON. Used for classification.' + - changeSet: + id: 60 + author: camsaul + comment: 'Added 0.26.0' + changes: + - addColumn: + tableName: metabase_database + columns: + - column: + name: metadata_sync_schedule + type: varchar(254) + remarks: 'The cron schedule string for when this database should undergo the metadata sync process (and analysis for new fields).' + defaultValue: '0 50 * * * ? *' # run at the end of every hour + constraints: + nullable: false + - column: + name: cache_field_values_schedule + type: varchar(254) + remarks: 'The cron schedule string for when FieldValues for eligible Fields should be updated.' + defaultValue: '0 50 0 * * ? *' # run at 12:50 AM + constraints: + nullable: false diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj index 06d5200b0328efa654eb21f5f401cf90c50b54a8..5988c55b6178efe4a2f6af3c3a594b945d69b498 100644 --- a/src/metabase/driver.clj +++ b/src/metabase/driver.clj @@ -238,7 +238,7 @@ (defn- init-driver-in-namespace! [ns-symb] (require ns-symb) - (if-let [register-driver-fn (ns-resolve ns-symb (symbol "-init-driver"))] + (if-let [register-driver-fn (ns-resolve ns-symb '-init-driver)] (register-driver-fn) (log/warn (format "No -init-driver function found for '%s'" (name ns-symb))))) @@ -326,8 +326,7 @@ This loads the corresponding driver if needed." (let [db-id->engine (memoize (fn [db-id] (db/select-one-field :engine Database, :id db-id)))] (fn [db-id] - {:pre [db-id]} - (when-let [engine (db-id->engine db-id)] + (when-let [engine (db-id->engine (u/get-id db-id))] (engine->driver engine))))) (defn ->driver diff --git a/src/metabase/events/sync_database.clj b/src/metabase/events/sync_database.clj index a9490440b9474e83f6c29652e65c906b1af7eb33..b0c71feaf6c6e868be563b0dfa95ba8f900913d3 100644 --- a/src/metabase/events/sync_database.clj +++ b/src/metabase/events/sync_database.clj @@ -4,7 +4,8 @@ [metabase [events :as events] [sync :as sync]] - [metabase.models.database :refer [Database]])) + [metabase.models.database :refer [Database]] + [metabase.sync.sync-metadata :as sync-metadata])) (def ^:const sync-database-topics "The `Set` of event topics which are subscribed to for use in database syncing." @@ -28,7 +29,10 @@ (when-let [database (Database (events/object->model-id topic object))] ;; just kick off a sync on another thread (future (try - (sync/sync-database! database) + ;; only do the 'full' sync if this is a "full sync" database. Otherwise just do metadata sync only + (if (:is_full_sync database) + (sync/sync-database! database) + (sync-metadata/sync-db-metadata! database)) (catch Throwable t (log/error (format "Error syncing Database: %d" (:id database)) t)))))) (catch Throwable e diff --git a/src/metabase/models/database.clj b/src/metabase/models/database.clj index aa448ae66ede2aa96ebace5883564d7cb3b0506e..a16dcd17be92b00d825c9846c9a2463398343e44 100644 --- a/src/metabase/models/database.clj +++ b/src/metabase/models/database.clj @@ -1,5 +1,6 @@ (ns metabase.models.database (:require [cheshire.generate :refer [add-encoder encode-map]] + [clojure.tools.logging :as log] [metabase [db :as mdb] [util :as u]] @@ -12,7 +13,6 @@ [db :as db] [models :as models]])) - ;;; ------------------------------------------------------------ Constants ------------------------------------------------------------ ;; TODO - should this be renamed `saved-cards-virtual-id`? @@ -34,23 +34,68 @@ (models/defmodel Database :metabase_database) + +(defn- schedule-tasks! + "(Re)schedule sync operation tasks for DATABASE. (Existing scheduled tasks will be deleted first.)" + [database] + (try + ;; this is done this way to avoid circular dependencies + (require 'metabase.task.sync-databases) + ((resolve 'metabase.task.sync-databases/schedule-tasks-for-db!) database) + (catch Throwable e + (log/error "Error scheduling tasks for DB:" (.getMessage e) "\n" + (u/pprint-to-str (u/filtered-stacktrace e)))))) + +(defn- unschedule-tasks! + "Unschedule any currently pending sync operation tasks for DATABASE." + [database] + (try + (require 'metabase.task.sync-databases) + ((resolve 'metabase.task.sync-databases/unschedule-tasks-for-db!) database) + (catch Throwable e + (log/error "Error unscheduling tasks for DB:" (.getMessage e) "\n" + (u/pprint-to-str (u/filtered-stacktrace e)))))) + (defn- post-insert [{database-id :id, :as database}] (u/prog1 database ;; add this database to the all users and metabot permissions groups (doseq [{group-id :id} [(perm-group/all-users) (perm-group/metabot)]] - (perms/grant-full-db-permissions! group-id database-id)))) + (perms/grant-full-db-permissions! group-id database-id)) + ;; schedule the Database sync tasks + (schedule-tasks! database))) (defn- post-select [{:keys [engine] :as database}] (if-not engine database (assoc database :features (set (when-let [driver ((resolve 'metabase.driver/engine->driver) engine)] ((resolve 'metabase.driver/features) driver)))))) -(defn- pre-delete [{:keys [id]}] +(defn- pre-delete [{id :id, :as database}] + (unschedule-tasks! database) (db/delete! 'Card :database_id id) (db/delete! 'Permissions :object [:like (str (perms/object-path id) "%")]) (db/delete! 'Table :db_id id)) +;; TODO - this logic would make more sense in post-update if such a method existed +(defn- pre-update [{new-metadata-schedule :metadata_sync_schedule, new-fieldvalues-schedule :cache_field_values_schedule, :as database}] + (u/prog1 database + ;; if the sync operation schedules have changed, we need to reschedule this DB + (when (or new-metadata-schedule new-fieldvalues-schedule) + (let [{old-metadata-schedule :metadata_sync_schedule + old-fieldvalues-schedule :cache_field_values_schedule} (db/select-one [Database :metadata_sync_schedule :cache_field_values_schedule] :id (u/get-id database)) + ;; if one of the schedules wasn't passed continue using the old one + new-metadata-schedule (or new-metadata-schedule old-metadata-schedule) + new-fieldvalues-schedule (or new-fieldvalues-schedule old-fieldvalues-schedule)] + (when (or (not= new-metadata-schedule old-metadata-schedule) + (not= new-fieldvalues-schedule old-fieldvalues-schedule)) + (log/info "DB's schedules have changed!\n" + (format "Sync metadata was: '%s', is now: '%s'\n" old-metadata-schedule new-metadata-schedule) + (format "Cache FieldValues was: '%s', is now: '%s'\n" old-fieldvalues-schedule new-fieldvalues-schedule)) + ;; reschedule the database. Make sure we're passing back the old schedule if one of the two wasn't supplied + (schedule-tasks! (assoc database + :metadata_sync_schedule new-metadata-schedule + :cache_field_values_schedule new-fieldvalues-schedule))))))) + (defn- perms-objects-set [database _] #{(perms/object-path (u/get-id database))}) @@ -60,16 +105,20 @@ models/IModel (merge models/IModelDefaults {:hydration-keys (constantly [:database :db]) - :types (constantly {:details :encrypted-json, :engine :keyword}) + :types (constantly {:details :encrypted-json + :engine :keyword + :metadata_sync_schedule :cron-string + :cache_field_values_schedule :cron-string}) :properties (constantly {:timestamped? true}) :post-insert post-insert :post-select post-select + :pre-update pre-update :pre-delete pre-delete}) i/IObjectPermissions (merge i/IObjectPermissionsDefaults - {:perms-objects-set perms-objects-set - :can-read? (partial i/current-user-has-partial-permissions? :read) - :can-write? i/superuser?})) + {:perms-objects-set perms-objects-set + :can-read? (partial i/current-user-has-partial-permissions? :read) + :can-write? i/superuser?})) ;;; ------------------------------------------------------------ Hydration / Util Fns ------------------------------------------------------------ @@ -106,10 +155,12 @@ "The string to replace passwords with when serializing Databases." "**MetabasePass**") -(add-encoder DatabaseInstance (fn [db json-generator] - (encode-map (cond - (not (:is_superuser @*current-user*)) (dissoc db :details) - (get-in db [:details :password]) (assoc-in db [:details :password] protected-password) - (get-in db [:details :pass]) (assoc-in db [:details :pass] protected-password) ; MongoDB uses "pass" instead of password - :else db) - json-generator))) +(add-encoder + DatabaseInstance + (fn [db json-generator] + (encode-map (cond + (not (:is_superuser @*current-user*)) (dissoc db :details) + (get-in db [:details :password]) (assoc-in db [:details :password] protected-password) + (get-in db [:details :pass]) (assoc-in db [:details :pass] protected-password) ; MongoDB uses "pass" instead of password + :else db) + json-generator))) diff --git a/src/metabase/models/interface.clj b/src/metabase/models/interface.clj index c7c2ace99bdf05c96530b78bfa3dd98537870b7b..3e956aa0f1e8cbcdf055dd8ff111c7ce7eadb84c 100644 --- a/src/metabase/models/interface.clj +++ b/src/metabase/models/interface.clj @@ -2,7 +2,10 @@ (:require [cheshire.core :as json] [clojure.core.memoize :as memoize] [metabase.util :as u] - [metabase.util.encryption :as encryption] + [metabase.util + [encryption :as encryption] + [schema :as su]] + [schema.core :as s] [taoensso.nippy :as nippy] [toucan.models :as models]) (:import java.sql.Blob)) @@ -59,6 +62,13 @@ :in compress :out decompress) +(defn- validate-cron-string [s] + (s/validate (s/maybe su/CronScheduleString) s)) + +(models/add-type! :cron-string + :in validate-cron-string + :out identity) + ;;; properties diff --git a/src/metabase/sync.clj b/src/metabase/sync.clj index 4dde3530da20d8a6752f30d8730489919be2435b..da9999f3356448a483d6ec135130126e17e4b4c4 100644 --- a/src/metabase/sync.clj +++ b/src/metabase/sync.clj @@ -16,25 +16,20 @@ [schema.core :as s] [metabase.sync.util :as sync-util])) -(def ^:private SyncDatabaseOptions - {(s/optional-key :full-sync?) s/Bool}) - (s/defn ^:always-validate sync-database! "Perform all the different sync operations synchronously for DATABASE. - You may optionally supply OPTIONS, which can be used to disable so-called 'full-sync', - meaning just metadata will be synced, but no 'analysis' (special type determination and - FieldValues syncing) will be done." - ([database] - (sync-database! database {:full-sync? true})) - ([database :- i/DatabaseInstance, options :- SyncDatabaseOptions] - (sync-util/sync-operation :sync database (format "Sync %s with options: %s" (sync-util/name-for-logging database) options) - ;; First make sure Tables, Fields, and FK information is up-to-date - (sync-metadata/sync-db-metadata! database) - (when (:full-sync? options) - ;; Next, run the 'analysis' step where we do things like scan values of fields and update special types accordingly - (analyze/analyze-db! database) - ;; Finally, update FieldValues - (field-values/update-field-values! database))))) + This is considered a 'full sync' in that all the different sync operations are performed at the same time. + Please note that this function is *not* what is called by the scheduled tasks. Those call different steps + independently." + {:style/indent 1} + [database :- i/DatabaseInstance] + (sync-util/sync-operation :sync database (format "Sync %s" (sync-util/name-for-logging database)) + ;; First make sure Tables, Fields, and FK information is up-to-date + (sync-metadata/sync-db-metadata! database) + ;; Next, run the 'analysis' step where we do things like scan values of fields and update special types accordingly + (analyze/analyze-db! database) + ;; Finally, update cached FieldValues + (field-values/update-field-values! database))) (s/defn ^:always-validate sync-table! diff --git a/src/metabase/sync/interface.clj b/src/metabase/sync/interface.clj index 6ba02b7b9e4fa469644df39aed82ac7a3ec91b32..7087972b7970e2c340baaa1259cbdd3bb20e75cb 100644 --- a/src/metabase/sync/interface.clj +++ b/src/metabase/sync/interface.clj @@ -97,6 +97,7 @@ (def Fingerprint "Schema for a Field 'fingerprint' generated as part of the analysis stage. Used to power the 'classification' sub-stage of analysis. Stored as the `fingerprint` column of Field." - {(s/optional-key :global) GlobalFingerprint + {(s/optional-key :version) su/IntGreaterThanZero ; Fingerprints with no version key are assumed to have version of 1 + (s/optional-key :global) GlobalFingerprint (s/optional-key :type) TypeSpecificFingerprint (s/optional-key :experimental) {s/Keyword s/Any}}) diff --git a/src/metabase/task.clj b/src/metabase/task.clj index 5a75c79423c9a51964d1aced2f7167ddcf593b0c..3cbe37d1e50d64129e45a272004da5be1f44c2e1 100644 --- a/src/metabase/task.clj +++ b/src/metabase/task.clj @@ -1,20 +1,36 @@ (ns metabase.task - "Background task scheduling via Quartzite. Individual tasks are defined in `metabase.task.*`. + "Background task scheduling via Quartzite. Individual tasks are defined in `metabase.task.*`. ## Regarding Task Initialization: The most appropriate way to initialize tasks in any `metabase.task.*` namespace is to implement the - `task-init` function which accepts zero arguments. This function is dynamically resolved and called - exactly once when the application goes through normal startup procedures. Inside this function you + `task-init` function which accepts zero arguments. This function is dynamically resolved and called + exactly once when the application goes through normal startup procedures. Inside this function you can do any work needed and add your task to the scheduler as usual via `schedule-task!`." (:require [clojure.tools.logging :as log] [clojurewerkz.quartzite.scheduler :as qs] - [metabase.util :as u])) + [metabase.util :as u] + [schema.core :as s]) + (:import [org.quartz JobDetail JobKey Scheduler Trigger TriggerKey])) +;;; +------------------------------------------------------------------------------------------------------------------------+ +;;; | SCHEDULER INSTANCE | +;;; +------------------------------------------------------------------------------------------------------------------------+ (defonce ^:private quartz-scheduler (atom nil)) +(defn- scheduler + "Fetch the instance of our Quartz scheduler. Call this function rather than dereffing the atom directly + because there are a few places (e.g., in tests) where we swap the instance out." + ^Scheduler [] + @quartz-scheduler) + + +;;; +------------------------------------------------------------------------------------------------------------------------+ +;;; | FINDING & LOADING TASKS | +;;; +------------------------------------------------------------------------------------------------------------------------+ + (defn- find-and-load-tasks! "Search Classpath for namespaces that start with `metabase.tasks.`, then `require` them so initialization can happen." [] @@ -26,6 +42,11 @@ (when-let [init-fn (ns-resolve ns-symb 'task-init)] (init-fn)))) + +;;; +------------------------------------------------------------------------------------------------------------------------+ +;;; | STARTING/STOPPING SCHEDULER | +;;; +------------------------------------------------------------------------------------------------------------------------+ + (defn start-scheduler! "Start our Quartzite scheduler which allows jobs to be submitted and triggers to begin executing." [] @@ -41,21 +62,25 @@ [] (log/debug "Stopping Quartz Scheduler") ;; tell quartz to stop everything - (when @quartz-scheduler - (qs/shutdown @quartz-scheduler)) + (when-let [scheduler (scheduler)] + (qs/shutdown scheduler)) ;; reset our scheduler reference (reset! quartz-scheduler nil)) -(defn schedule-task! +;;; +------------------------------------------------------------------------------------------------------------------------+ +;;; | SCHEDULING/DELETING TASKS | +;;; +------------------------------------------------------------------------------------------------------------------------+ + +(s/defn ^:always-validate schedule-task! "Add a given job and trigger to our scheduler." - [job trigger] - (when @quartz-scheduler - (qs/schedule @quartz-scheduler job trigger))) + [job :- JobDetail, trigger :- Trigger] + (when-let [scheduler (scheduler)] + (qs/schedule scheduler job trigger))) -(defn delete-task! +(s/defn ^:always-validate delete-task! "delete a task from the scheduler" - [job-key trigger-key] - (when @quartz-scheduler - (qs/delete-trigger @quartz-scheduler trigger-key) - (qs/delete-job @quartz-scheduler job-key))) + [job-key :- JobKey, trigger-key :- TriggerKey] + (when-let [scheduler (scheduler)] + (qs/delete-trigger scheduler trigger-key) + (qs/delete-job scheduler job-key))) diff --git a/src/metabase/task/sync_databases.clj b/src/metabase/task/sync_databases.clj index 0589d11391169fa77284d77b1c30b1eb1b13d920..a2dd9ac0174e3cf624a39fec1d2d548ac0510d54 100644 --- a/src/metabase/task/sync_databases.clj +++ b/src/metabase/task/sync_databases.clj @@ -1,49 +1,169 @@ (ns metabase.task.sync-databases - (:require [clj-time.core :as t] - [clojure.tools.logging :as log] + "Scheduled tasks for syncing metadata/analyzing and caching FieldValues for connected Databases." + (:require [clojure.tools.logging :as log] [clojurewerkz.quartzite + [conversion :as qc] [jobs :as jobs] [triggers :as triggers]] [clojurewerkz.quartzite.schedule.cron :as cron] [metabase - [driver :as driver] - [sync :as sync] - [task :as task]] + [task :as task] + [util :as u]] [metabase.models.database :refer [Database]] - [toucan.db :as db])) - -(def ^:private ^:const sync-databases-job-key "metabase.task.sync-databases.job") -(def ^:private ^:const sync-databases-trigger-key "metabase.task.sync-databases.trigger") - -(defonce ^:private sync-databases-job (atom nil)) -(defonce ^:private sync-databases-trigger (atom nil)) - -;; simple job which looks up all databases and runs a sync on them -(jobs/defjob SyncDatabases [_] - (doseq [database (db/select Database, :is_sample false)] ; skip Sample Dataset DB - (try - ;; NOTE: this happens synchronously for now to avoid excessive load if there are lots of databases - ;; most of the time we do a quick sync and avoid the lengthy analysis process - ;; at midnight we run the full sync - (let [full-sync? (not (and (zero? (t/hour (t/now))) - (driver/driver-supports? (driver/engine->driver (:engine database)) :dynamic-schema)))] - (sync/sync-database! database {:full-sync? full-sync?})) - (catch Throwable e - (log/error (format "Error syncing database %d: " (:id database)) e))))) + [metabase.sync + [analyze :as analyze] + [field-values :as field-values] + [sync-metadata :as sync-metadata]] + [metabase.util.schema :as su] + [schema.core :as s] + [toucan.db :as db]) + (:import metabase.models.database.DatabaseInstance + [org.quartz CronTrigger JobDetail JobKey TriggerKey])) + +;;; +------------------------------------------------------------------------------------------------------------------------+ +;;; | JOB LOGIC | +;;; +------------------------------------------------------------------------------------------------------------------------+ + +(s/defn ^:private ^:always-validate job-context->database :- DatabaseInstance + "Get the Database referred to in JOB-CONTEXT. Guaranteed to return a valid Database." + [job-context] + (Database (u/get-id (get (qc/from-job-data job-context) "db-id")))) + + +(jobs/defjob SyncAndAnalyzeDatabase [job-context] + (let [database (job-context->database job-context)] + (sync-metadata/sync-db-metadata! database) + ;; only run analysis if this is a "full sync" database + (when (:is_full_sync database) + (analyze/analyze-db! database)))) + + +(jobs/defjob UpdateFieldValues [job-context] + (field-values/update-field-values! (job-context->database job-context))) + + +;;; +------------------------------------------------------------------------------------------------------------------------+ +;;; | TASK INFO AND GETTER FUNCTIONS | +;;; +------------------------------------------------------------------------------------------------------------------------+ + +(def ^:private TaskInfo + "One-off schema for information about the various sync tasks we run for a DB." + {:key s/Keyword + :db-schedule-column s/Keyword + :job-class Class}) + + +(def ^:private task-infos + "Maps containing info about the different independent sync tasks we schedule for each DB." + [{:key :sync-and-analyze + :db-schedule-column :metadata_sync_schedule + :job-class SyncAndAnalyzeDatabase} + {:key :update-field-values + :db-schedule-column :cache_field_values_schedule + :job-class UpdateFieldValues}]) + + +;; These getter functions are not strictly neccesary but are provided primarily so we can get some extra validation by using them + +(s/defn ^:private ^:always-validate job-key :- JobKey + "Return an appropriate string key for the job described by TASK-INFO for DATABASE-OR-ID." + [database :- DatabaseInstance, task-info :- TaskInfo] + (jobs/key (format "metabase.task.%s.job.%d" (name (:key task-info)) (u/get-id database)))) + +(s/defn ^:private ^:always-validate trigger-key :- TriggerKey + "Return an appropriate string key for the trigger for TASK-INFO and DATABASE-OR-ID." + [database :- DatabaseInstance, task-info :- TaskInfo] + (triggers/key (format "metabase.task.%s.trigger.%d" (name (:key task-info)) (u/get-id database)))) + +(s/defn ^:private ^:always-validate cron-schedule :- su/CronScheduleString + "Fetch the appropriate cron schedule string for DATABASE and TASK-INFO." + [database :- DatabaseInstance, task-info :- TaskInfo] + (get database (:db-schedule-column task-info))) + +(s/defn ^:private ^:always-validate job-class :- Class + "Get the Job class for TASK-INFO." + [task-info :- TaskInfo] + (:job-class task-info)) + +(s/defn ^:private ^:always-validate description :- s/Str + "Return an appropriate description string for a job/trigger for Database described by TASK-INFO." + [database :- DatabaseInstance, task-info :- TaskInfo] + (format "%s Database %d" (name (:key task-info)) (u/get-id database))) + + +;;; +------------------------------------------------------------------------------------------------------------------------+ +;;; | DELETING TASKS FOR A DB | +;;; +------------------------------------------------------------------------------------------------------------------------+ + +(s/defn ^:private ^:always-validate delete-task! + "Cancel a single sync job for DATABASE-OR-ID and TASK-INFO." + [database :- DatabaseInstance, task-info :- TaskInfo] + (let [job-key (job-key database task-info) + trigger-key (trigger-key database task-info)] + (log/debug (u/format-color 'red "Unscheduling task for Database %d: job: %s; trigger: %s" (u/get-id database) (.getName job-key) (.getName trigger-key))) + (task/delete-task! job-key trigger-key))) + +(s/defn ^:always-validate unschedule-tasks-for-db! + "Cancel *all* scheduled sync and FieldValues caching tassks for DATABASE-OR-ID." + [database :- DatabaseInstance] + (doseq [task-info task-infos] + (delete-task! database task-info))) + + +;;; +------------------------------------------------------------------------------------------------------------------------+ +;;; | (RE)SCHEDULING TASKS FOR A DB | +;;; +------------------------------------------------------------------------------------------------------------------------+ + +(s/defn ^:private ^:always-validate job :- JobDetail + "Build a Quartz Job for DATABASE and TASK-INFO." + [database :- DatabaseInstance, task-info :- TaskInfo] + (jobs/build + (jobs/with-description (description database task-info)) + (jobs/of-type (job-class task-info)) + (jobs/using-job-data {"db-id" (u/get-id database)}) + (jobs/with-identity (job-key database task-info)))) + +(s/defn ^:private ^:always-validate trigger :- CronTrigger + "Build a Quartz Trigger for DATABASE and TASK-INFO." + [database :- DatabaseInstance, task-info :- TaskInfo] + (triggers/build + (triggers/with-description (description database task-info)) + (triggers/with-identity (trigger-key database task-info)) + (triggers/start-now) + (triggers/with-schedule + (cron/schedule + (cron/cron-schedule (cron-schedule database task-info)) + ;; drop tasks if they start to back up + (cron/with-misfire-handling-instruction-do-nothing))))) + + +(s/defn ^:private ^:always-validate schedule-task-for-db! + "Schedule a new Quartz job for DATABASE and TASK-INFO." + [database :- DatabaseInstance, task-info :- TaskInfo] + (let [job (job database task-info) + trigger (trigger database task-info)] + (log/debug (u/format-color 'green "Scheduling task for Database %d: job: %s; trigger: %s" (u/get-id database) (.getName (.getKey job)) (.getName (.getKey trigger)))) + (task/schedule-task! job trigger))) + + +(s/defn ^:always-validate schedule-tasks-for-db! + "Schedule all the different sync jobs we have for DATABASE. + Unschedules any existing jobs." + [database :- DatabaseInstance] + ;; unschedule any tasks that might already be scheduled + (unschedule-tasks-for-db! database) + ;; now (re)schedule all the tasks + (doseq [task-info task-infos] + (schedule-task-for-db! database task-info))) + + +;;; +------------------------------------------------------------------------------------------------------------------------+ +;;; | TASK INITIALIZATION | +;;; +------------------------------------------------------------------------------------------------------------------------+ (defn task-init - "Automatically called during startup; start the job for syncing databases." + "Automatically called during startup; start the jobs for syncing/analyzing and updating FieldValues for all Dtabases besides + the sample dataset." [] - ;; build our job - (reset! sync-databases-job (jobs/build - (jobs/of-type SyncDatabases) - (jobs/with-identity (jobs/key sync-databases-job-key)))) - ;; build our trigger - (reset! sync-databases-trigger (triggers/build - (triggers/with-identity (triggers/key sync-databases-trigger-key)) - (triggers/start-now) - (triggers/with-schedule - ;; run at the end of every hour - (cron/cron-schedule "0 50 * * * ? *")))) - ;; submit ourselves to the scheduler - (task/schedule-task! @sync-databases-job @sync-databases-trigger)) + (doseq [database (db/select Database, :is_sample false)] + (schedule-tasks-for-db! database))) diff --git a/src/metabase/util/schema.clj b/src/metabase/util/schema.clj index 01600347032c10226b6e2fd1ae632d03c171d3d0..552a49d6c6d469efed8ef5e034f1ac4bc4c00086 100644 --- a/src/metabase/util/schema.clj +++ b/src/metabase/util/schema.clj @@ -5,7 +5,8 @@ [medley.core :as m] [metabase.util :as u] [metabase.util.password :as password] - [schema.core :as s])) + [schema.core :as s]) + (:import org.quartz.CronExpression)) (defn with-api-error-message "Return SCHEMA with an additional API-ERROR-MESSAGE that will be used to explain the error if a parameter fails validation." @@ -137,3 +138,16 @@ "Schema for a valid map of embedding params." (with-api-error-message (s/maybe {s/Keyword (s/enum "disabled" "enabled" "locked")}) "value must be a valid embedding params map.")) + + +(def CronScheduleString + "Schema for a valid cron schedule string." + ;; See `http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger#format` for details on Cron string format + (s/constrained + NonBlankString + (fn [^String s] + (try (CronExpression/validateExpression s) + true + (catch Throwable _ + false))) + "Invalid cron schedule string.")) diff --git a/test/metabase/api/database_test.clj b/test/metabase/api/database_test.clj index 52086a920eda4aa379cd31a604608502563bdf84..b4f46eef4c70c9d22784cefd814ed75bcfe68c49 100644 --- a/test/metabase/api/database_test.clj +++ b/test/metabase/api/database_test.clj @@ -53,13 +53,15 @@ (second @~result))))) (def ^:private default-db-details - {:engine "h2" - :name "test-data" - :is_sample false - :is_full_sync true - :description nil - :caveats nil - :points_of_interest nil}) + {:engine "h2" + :name "test-data" + :is_sample false + :is_full_sync true + :description nil + :caveats nil + :points_of_interest nil + :cache_field_values_schedule "0 50 0 * * ? *" + :metadata_sync_schedule "0 50 * * * ? *"}) (defn- db-details diff --git a/test/metabase/api/field_test.clj b/test/metabase/api/field_test.clj index 6a2b2b7c19d104a3bc4a170de4b3e7ab65aac24e..c0fe291f301155052b4da9d454dc9a981084c5b9 100644 --- a/test/metabase/api/field_test.clj +++ b/test/metabase/api/field_test.clj @@ -22,17 +22,19 @@ (defn- db-details [] (tu/match-$ (db) - {:created_at $ - :engine "h2" - :caveats nil - :points_of_interest nil - :id $ - :updated_at $ - :name "test-data" - :is_sample false - :is_full_sync true - :description nil - :features (mapv name (driver/features (driver/engine->driver :h2)))})) + {:created_at $ + :engine "h2" + :caveats nil + :points_of_interest nil + :id $ + :updated_at $ + :name "test-data" + :is_sample false + :is_full_sync true + :description nil + :features (mapv name (driver/features (driver/engine->driver :h2))) + :cache_field_values_schedule "0 50 0 * * ? *" + :metadata_sync_schedule "0 50 * * * ? *"})) ;; ## GET /api/field/:id diff --git a/test/metabase/api/table_test.clj b/test/metabase/api/table_test.clj index 62c60bf1444a9b02f40b42825bb32db5933dd978..7312bb511408aa7bfce7b5f6293e47ec0f057504 100644 --- a/test/metabase/api/table_test.clj +++ b/test/metabase/api/table_test.clj @@ -9,16 +9,17 @@ [middleware :as middleware] [sync :as sync] [util :as u]] + [metabase.api.table :as table-api] [metabase.models [card :refer [Card]] [database :as database :refer [Database]] [field :refer [Field]] [permissions :as perms] [permissions-group :as perms-group] - [table :refer [Table]]] + [table :as table :refer [Table]]] [metabase.test [data :as data] - [util :as tu :refer [match-$ resolve-private-vars]]] + [util :as tu :refer [match-$]]] [metabase.test.data [dataset-definitions :as defs] [users :refer [user->client]]] @@ -27,12 +28,6 @@ [hydrate :as hydrate]] [toucan.util.test :as tt])) -(resolve-private-vars metabase.models.table pk-field-id) -(resolve-private-vars metabase.api.table - dimension-options-for-response datetime-dimension-indexes numeric-dimension-indexes - numeric-default-index date-default-index coordinate-default-index) - - ;; ## /api/org/* AUTHENTICATION Tests ;; We assume that all endpoints for a given context are enforced by the same middleware, so we don't run the same ;; authentication test on every single individual endpoint @@ -45,17 +40,19 @@ (defn- db-details [] (match-$ (data/db) - {:created_at $ - :engine "h2" - :id $ - :updated_at $ - :name "test-data" - :is_sample false - :is_full_sync true - :description nil - :caveats nil - :points_of_interest nil - :features (mapv name (driver/features (driver/engine->driver :h2)))})) + {:created_at $ + :engine "h2" + :id $ + :updated_at $ + :name "test-data" + :is_sample false + :is_full_sync true + :description nil + :caveats nil + :points_of_interest nil + :features (mapv name (driver/features (driver/engine->driver :h2))) + :cache_field_values_schedule "0 50 0 * * ? *" + :metadata_sync_schedule "0 50 * * * ? *"})) (defn- table-defaults [] {:description nil @@ -140,7 +137,7 @@ :display_name "Venues" :rows 100 :updated_at $ - :pk_field (pk-field-id $$) + :pk_field (#'table/pk-field-id $$) :id (data/id :venues) :db_id (data/id) :raw_table_id $ @@ -156,7 +153,7 @@ ((user->client :rasta) :get 403 (str "table/" table-id)))) (defn- query-metadata-defaults [] - (->> dimension-options-for-response + (->> #'table-api/dimension-options-for-response var-get walk/keywordize-keys (assoc (table-defaults) :dimension_options))) @@ -229,8 +226,8 @@ :display_name "Last Login" :base_type "type/DateTime" :visibility_type "normal" - :dimension_options (var-get datetime-dimension-indexes) - :default_dimension_option (var-get date-default-index) + :dimension_options (var-get #'table-api/datetime-dimension-indexes) + :default_dimension_option (var-get #'table-api/date-default-index) ) (assoc (field-details (Field (data/id :users :name))) :special_type "type/Name" @@ -275,8 +272,8 @@ :name "LAST_LOGIN" :display_name "Last Login" :base_type "type/DateTime" - :dimension_options (var-get datetime-dimension-indexes) - :default_dimension_option (var-get date-default-index)) + :dimension_options (var-get #'table-api/datetime-dimension-indexes) + :default_dimension_option (var-get #'table-api/date-default-index)) (assoc (field-details (Field (data/id :users :name))) :table_id (data/id :users) :special_type "type/Name" @@ -338,7 +335,7 @@ :name $ :rows 15 :display_name "Userz" - :pk_field (pk-field-id $$) + :pk_field (#'table/pk-field-id $$) :id $ :raw_table_id $ :created_at $})) @@ -535,12 +532,12 @@ ;; Ensure dimensions options are sorted numerically, but returned as strings (expect - (map str (sort (map #(Long/parseLong %) (var-get datetime-dimension-indexes)))) - (var-get datetime-dimension-indexes)) + (map str (sort (map #(Long/parseLong %) (var-get #'table-api/datetime-dimension-indexes)))) + (var-get #'table-api/datetime-dimension-indexes)) (expect - (map str (sort (map #(Long/parseLong %) (var-get numeric-dimension-indexes)))) - (var-get numeric-dimension-indexes)) + (map str (sort (map #(Long/parseLong %) (var-get #'table-api/numeric-dimension-indexes)))) + (var-get #'table-api/numeric-dimension-indexes)) ;; Numeric fields without min/max values should not have binning strategies (expect diff --git a/test/metabase/driver/postgres_test.clj b/test/metabase/driver/postgres_test.clj index 7a9c434707cd1823661d17a94aacd0e9e88e7d1e..5441ca57baca534920995586a7de5f5063a50ac8 100644 --- a/test/metabase/driver/postgres_test.clj +++ b/test/metabase/driver/postgres_test.clj @@ -221,7 +221,7 @@ (drop-if-exists-and-create-db! "dropped_views_test") ;; create the DB object (tt/with-temp Database [database {:engine :postgres, :details (assoc details :dbname "dropped_views_test")}] - (let [sync! #(sync/sync-database! database {:full-sync? true})] + (let [sync! #(sync/sync-database! database)] ;; populate the DB and create a view (exec! ["CREATE table birds (name VARCHAR UNIQUE NOT NULL);" "INSERT INTO birds (name) VALUES ('Rasta'), ('Lucky'), ('Kanye Nest');" diff --git a/test/metabase/sync_database_test.clj b/test/metabase/sync_database_test.clj index badf18be90d20769468ae6a5105ce4e3b0b14629..7633c53604621c16468f850c76c4a9d0f3d5cdeb 100644 --- a/test/metabase/sync_database_test.clj +++ b/test/metabase/sync_database_test.clj @@ -351,7 +351,7 @@ ;; create the `blueberries_consumed` table and insert a 100 values (exec! ["CREATE TABLE blueberries_consumed (num INTEGER NOT NULL);" (insert-range-sql (range 100))]) - (sync-database! db {:full-sync? true}) + (sync-database! db) (let [table-id (db/select-one-id Table :db_id (u/get-id db)) field-id (db/select-one-id Field :table_id table-id)] ;; field values should exist... diff --git a/test/metabase/task/sync_databases_test.clj b/test/metabase/task/sync_databases_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..8b8e5838b7ccd60385db92a34c06ac303f269de0 --- /dev/null +++ b/test/metabase/task/sync_databases_test.clj @@ -0,0 +1,209 @@ +(ns metabase.task.sync-databases-test + "Tests for the logic behind scheduling the various sync operations of Databases. Most of the actual logic we're testing is part of `metabase.models.database`, + so there's an argument to be made that these sorts of tests could just as easily belong to a `database-test` namespace." + (:require [clojure.string :as str] + [expectations :refer :all] + [metabase.models.database :refer [Database]] + metabase.task.sync-databases + [metabase.test.util :as tu] + [metabase.util :as u] + [toucan.db :as db] + [toucan.util.test :as tt]) + (:import [metabase.task.sync_databases SyncAndAnalyzeDatabase UpdateFieldValues])) + +(defn- replace-trailing-id-with-<id> [s] + (str/replace s #"\d+$" "<id>")) + +(defn- replace-ids-with-<id> [current-tasks] + (vec (for [task current-tasks] + (-> task + (update :description replace-trailing-id-with-<id>) + (update :key replace-trailing-id-with-<id>) + (update-in [:data "db-id"] class) + (update :triggers (fn [triggers] + (vec (for [trigger triggers] + (update trigger :key replace-trailing-id-with-<id>))))))))) + +(defn- current-tasks [] + (replace-ids-with-<id> (tu/scheduler-current-tasks))) + +;; Check that a newly created database automatically gets scheduled +(expect + [{:description "sync-and-analyze Database <id>" + :class SyncAndAnalyzeDatabase + :key "metabase.task.sync-and-analyze.job.<id>" + :data {"db-id" Integer} + :triggers [{:key "metabase.task.sync-and-analyze.trigger.<id>" + :cron-schedule "0 50 * * * ? *"}]} + {:description "update-field-values Database <id>" + :class UpdateFieldValues + :key "metabase.task.update-field-values.job.<id>" + :data {"db-id" Integer} + :triggers [{:key "metabase.task.update-field-values.trigger.<id>" + :cron-schedule "0 50 0 * * ? *"}]}] + (tu/with-temp-scheduler + (tt/with-temp Database [database {:engine :postgres}] + (current-tasks)))) + + +;; Check that a custom schedule is respected when creating a new Database +(expect + [{:description "sync-and-analyze Database <id>" + :class SyncAndAnalyzeDatabase + :key "metabase.task.sync-and-analyze.job.<id>" + :data {"db-id" Integer} + :triggers [{:key "metabase.task.sync-and-analyze.trigger.<id>" + :cron-schedule "0 30 4,16 * * ? *"}]} + {:description "update-field-values Database <id>" + :class UpdateFieldValues + :key "metabase.task.update-field-values.job.<id>" + :data {"db-id" Integer} + :triggers [{:key "metabase.task.update-field-values.trigger.<id>" + :cron-schedule "0 15 10 ? * 6#3"}]}] + (tu/with-temp-scheduler + (tt/with-temp Database [database {:engine :postgres + :metadata_sync_schedule "0 30 4,16 * * ? *" ; 4:30 AM and PM daily + :cache_field_values_schedule "0 15 10 ? * 6#3"}] ; 10:15 on the 3rd Friday of the Month + (current-tasks)))) + + +;; Check that a deleted database gets unscheduled +(expect + [] + (tu/with-temp-scheduler + (tt/with-temp Database [database {:engine :postgres}] + (db/delete! Database :id (u/get-id database)) + (current-tasks)))) + +;; Check that changing the schedule column(s) for a DB properly updates the scheduled tasks +(expect + [{:description "sync-and-analyze Database <id>" + :class SyncAndAnalyzeDatabase + :key "metabase.task.sync-and-analyze.job.<id>" + :data {"db-id" Integer} + :triggers [{:key "metabase.task.sync-and-analyze.trigger.<id>" + :cron-schedule "0 15 10 ? * MON-FRI"}]} + {:description "update-field-values Database <id>" + :class UpdateFieldValues + :key "metabase.task.update-field-values.job.<id>" + :data {"db-id" Integer} + :triggers [{:key "metabase.task.update-field-values.trigger.<id>" + :cron-schedule "0 11 11 11 11 ?"}]}] + (tu/with-temp-scheduler + (tt/with-temp Database [database {:engine :postgres}] + (db/update! Database (u/get-id database) + :metadata_sync_schedule "0 15 10 ? * MON-FRI" ; 10:15 AM every weekday + :cache_field_values_schedule "0 11 11 11 11 ?") ; Every November 11th at 11:11 AM + (current-tasks)))) + +;; Check that changing one schedule doesn't affect the other +(expect + [{:description "sync-and-analyze Database <id>" + :class SyncAndAnalyzeDatabase + :key "metabase.task.sync-and-analyze.job.<id>" + :data {"db-id" Integer} + :triggers [{:key "metabase.task.sync-and-analyze.trigger.<id>" + :cron-schedule "0 50 * * * ? *"}]} + {:description "update-field-values Database <id>" + :class UpdateFieldValues + :key "metabase.task.update-field-values.job.<id>" + :data {"db-id" Integer} + :triggers [{:key "metabase.task.update-field-values.trigger.<id>" + :cron-schedule "0 15 10 ? * MON-FRI"}]}] + (tu/with-temp-scheduler + (tt/with-temp Database [database {:engine :postgres}] + (db/update! Database (u/get-id database) + :cache_field_values_schedule "0 15 10 ? * MON-FRI") + (current-tasks)))) + +(expect + [{:description "sync-and-analyze Database <id>" + :class SyncAndAnalyzeDatabase + :key "metabase.task.sync-and-analyze.job.<id>" + :data {"db-id" Integer} + :triggers [{:key "metabase.task.sync-and-analyze.trigger.<id>" + :cron-schedule "0 15 10 ? * MON-FRI"}]} + {:description "update-field-values Database <id>" + :class UpdateFieldValues + :key "metabase.task.update-field-values.job.<id>" + :data {"db-id" Integer} + :triggers [{:key "metabase.task.update-field-values.trigger.<id>" + :cron-schedule "0 50 0 * * ? *"}]}] + (tu/with-temp-scheduler + (tt/with-temp Database [database {:engine :postgres}] + (db/update! Database (u/get-id database) + :metadata_sync_schedule "0 15 10 ? * MON-FRI") + (current-tasks)))) + +;; Check that you can't INSERT a DB with an invalid schedule +(expect + Exception + (db/insert! Database {:engine :postgres + :metadata_sync_schedule "0 * ABCD"})) + +(expect + Exception + (db/insert! Database {:engine :postgres + :cache_field_values_schedule "0 * ABCD"})) + +;; Check that you can't UPDATE a DB's schedule to something invalid +(expect + Exception + (tt/with-temp Database [database {:engine :postgres}] + (db/update! Database (u/get-id database) + :metadata_sync_schedule "2 CANS PER DAY"))) + +(expect + Exception + (tt/with-temp Database [database {:engine :postgres}] + (db/update! Database (u/get-id database) + :cache_field_values_schedule "2 CANS PER DAY"))) + + +;;; +------------------------------------------------------------------------------------------------------------------------+ +;;; | CHECKING THAT SYNC TASKS RUN CORRECT FNS | +;;; +------------------------------------------------------------------------------------------------------------------------+ + +(defn- check-if-sync-processes-ran-for-db {:style/indent 0} [db-info] + (let [sync-db-metadata-counter (atom 0) + analyze-db-counter (atom 0) + update-field-values-counter (atom 0)] + (with-redefs [metabase.sync.sync-metadata/sync-db-metadata! (fn [& _] (swap! sync-db-metadata-counter inc)) + metabase.sync.analyze/analyze-db! (fn [& _] (swap! analyze-db-counter inc)) + metabase.sync.field-values/update-field-values! (fn [& _] (swap! update-field-values-counter inc))] + (tu/with-temp-scheduler + (tt/with-temp Database [database db-info] + ;; give tasks some time to run + (Thread/sleep 2000) + {:ran-sync? (not (zero? @sync-db-metadata-counter)) + :ran-analyze? (not (zero? @analyze-db-counter)) + :ran-update-field-values? (not (zero? @update-field-values-counter))}))))) + +(defn- cron-schedule-for-next-year [] + (format "0 15 10 * * ? %d" (inc (u/date-extract :year)))) + +;; Make sure that a database that *is* marked full sync *will* get analyzed +(expect + {:ran-sync? true, :ran-analyze? true, :ran-update-field-values? false} + (check-if-sync-processes-ran-for-db + {:engine :postgres + :metadata_sync_schedule "* * * * * ? *" + :cache_field_values_schedule (cron-schedule-for-next-year)})) + +;; Make sure that a database that *isn't* marked full sync won't get analyzed +(expect + {:ran-sync? true, :ran-analyze? false, :ran-update-field-values? false} + (check-if-sync-processes-ran-for-db + {:engine :postgres + :is_full_sync false + :metadata_sync_schedule "* * * * * ? *" + :cache_field_values_schedule (cron-schedule-for-next-year)})) + +;; Make sure the update field values task calls `update-field-values!` +(expect + {:ran-sync? false, :ran-analyze? false, :ran-update-field-values? true} + (check-if-sync-processes-ran-for-db + {:engine :postgres + :is_full_sync false + :metadata_sync_schedule (cron-schedule-for-next-year) + :cache_field_values_schedule "* * * * * ? *"})) diff --git a/test/metabase/test/util.clj b/test/metabase/test/util.clj index a6a23f89f37d45637da37d764f28154e58193be0..2d5512a5c3d46ee75096dd45959070833db38a11 100644 --- a/test/metabase/test/util.clj +++ b/test/metabase/test/util.clj @@ -1,9 +1,15 @@ (ns metabase.test.util "Helper functions and macros for writing unit tests." (:require [cheshire.core :as json] + [clojure + [string :as str] + [walk :as walk]] [clojure.tools.logging :as log] - [clojure.walk :as walk] + [clojurewerkz.quartzite.scheduler :as qs] [expectations :refer :all] + [metabase + [task :as task] + [util :as u]] [metabase.models [card :refer [Card]] [collection :refer [Collection]] @@ -21,8 +27,8 @@ [table :refer [Table]] [user :refer [User]]] [metabase.test.data :as data] - [metabase.util :as u] - [toucan.util.test :as test])) + [toucan.util.test :as test]) + (:import [org.quartz CronTrigger JobDetail JobKey Scheduler Trigger])) ;; ## match-$ @@ -136,7 +142,7 @@ (u/strict-extend (class Database) test/WithTempDefaults {:with-temp-defaults (fn [_] {:details {} - :engine :yeehaw + :engine :yeehaw ; wtf? :is_sample false :name (random-name)})}) @@ -338,8 +344,64 @@ (update-in-if-present [:fingerprint :type :type/Number] round-fingerprint-fields [:min :max :avg]) (update-in-if-present [:fingerprint :type :type/Text] round-fingerprint-fields [:percent-json :percent-url :percent-email :average-length]))) -(defn round-fingerprint-cols [query-results] +(defn round-fingerprint-cols [query-results] (let [maybe-data-cols (if (contains? query-results :data) [:data :cols] [:cols])] (update-in query-results maybe-data-cols #(map round-fingerprint %)))) + +;;; +------------------------------------------------------------------------------------------------------------------------+ +;;; | SCHEDULER | +;;; +------------------------------------------------------------------------------------------------------------------------+ + +;; Various functions for letting us check that things get scheduled properly. Use these to put a temporary scheduler in place +;; and then check the tasks that get scheduled + +(defn do-with-scheduler [scheduler f] + (with-redefs [metabase.task/scheduler (constantly scheduler)] + (f))) + +(defmacro with-scheduler + "Temporarily bind the Metabase Quartzite scheduler to SCHEULDER and run BODY." + {:style/indent 1} + [scheduler & body] + `(do-with-scheduler ~scheduler (fn [] ~@body))) + +(defn do-with-temp-scheduler [f] + (let [temp-scheduler (qs/start (qs/initialize))] + (with-scheduler temp-scheduler + (try (f) + (finally + (qs/shutdown temp-scheduler)))))) + +(defmacro with-temp-scheduler + "Execute BODY with a temporary scheduler in place. + + (with-temp-scheduler + (do-something-to-schedule-tasks) + ;; verify that the right thing happened + (scheduler-current-tasks))" + {:style/indent 0} + [& body] + `(do-with-temp-scheduler (fn [] ~@body))) + +(defn scheduler-current-tasks + "Return information about the currently scheduled tasks (jobs+triggers) for the current scheduler. + Intended so we can test that things were scheduled correctly." + [] + (when-let [^Scheduler scheduler (#'task/scheduler)] + (vec + (sort-by + :key + (for [^JobKey job-key (.getJobKeys scheduler nil)] + (let [^JobDetail job-detail (.getJobDetail scheduler job-key) + triggers (.getTriggersOfJob scheduler job-key)] + {:description (.getDescription job-detail) + :class (.getJobClass job-detail) + :key (.getName job-key) + :data (into {} (.getJobDataMap job-detail)) + :triggers (vec (for [^Trigger trigger triggers] + (merge + {:key (.getName (.getKey trigger))} + (when (instance? CronTrigger trigger) + {:cron-schedule (.getCronExpression ^CronTrigger trigger)}))))})))))) diff --git a/test_resources/log4j.properties b/test_resources/log4j.properties index a085ebb0927e78d16ea47dfe4d800017a298cd5e..75116bf562a111292e65f08dfd7e38bb8b0414e5 100644 --- a/test_resources/log4j.properties +++ b/test_resources/log4j.properties @@ -19,8 +19,8 @@ log4j.logger.com.mchange=ERROR log4j.logger.org.eclipse.jetty.server.HttpChannel=ERROR log4j.logger.metabase=ERROR log4j.logger.metabase.test-setup=INFO -log4j.logger.metabase.test.data.datasets=INFO -log4j.logger.metabase.util.encryption=INFO -# NOCOMMIT log4j.logger.metabase.sync=DEBUG +log4j.logger.metabase.task.sync-databases=INFO +log4j.logger.metabase.test.data.datasets=INFO log4j.logger.metabase.test.data=DEBUG +log4j.logger.metabase.util.encryption=INFO