diff --git a/src/metabase/driver/sql_jdbc/connection.clj b/src/metabase/driver/sql_jdbc/connection.clj index eb9878e2915051af74c9eadcfc852848fb4f33fb..969726e864c9c05fac0c170666e8a60cddc8f3a9 100644 --- a/src/metabase/driver/sql_jdbc/connection.clj +++ b/src/metabase/driver/sql_jdbc/connection.clj @@ -172,18 +172,23 @@ (let [database-id (u/the-id db-or-id-or-spec) ;; we need the Database instance no matter what (in order to compare details hash with cached value) db (or (and (instance? (type Database) db-or-id-or-spec) db-or-id-or-spec) ; passed in - (db/select-one [Database :id :engine :details] :id database-id) ; look up by ID - (throw (ex-info (tru "Database {0} does not exist." database-id) - {:status-code 404 - :type qp.error-type/invalid-query - :database-id database-id}))) + (db/select-one [Database :id :engine :details] :id database-id) ; look up by ID + (throw (ex-info (tru "Database {0} does not exist." database-id) + {:status-code 404 + :type qp.error-type/invalid-query + :database-id database-id}))) get-fn (fn [db-id log-invalidation?] (when-let [details (get @database-id->connection-pool db-id)] (cond ;; details hash changed from what is cached; invalid (let [curr-hash (get @database-id->db-details-hashes db-id) new-hash (db-details-hash db)] - (and (some? curr-hash) (not= curr-hash new-hash))) + (when (and (some? curr-hash) (not= curr-hash new-hash)) + ;; the hash didn't match, but it's possible that a stale instance of `DatabaseInstance` + ;; was passed in (ex: from a long-running sync operation); fetch the latest one from + ;; our app DB, and see if it STILL doesn't match + (not= curr-hash (-> (db/select-one [Database :id :engine :details] :id database-id) + db-details-hash)))) (if log-invalidation? (log-db-details-hash-change-msg! db-id) nil) diff --git a/test/metabase/driver/sql_jdbc/connection_test.clj b/test/metabase/driver/sql_jdbc/connection_test.clj index 4617cefc3ba5fc8e269bfc6fa5518e9541fd1872..e6f76e71b6fc2a79e6d026780461a9ede983ca4e 100644 --- a/test/metabase/driver/sql_jdbc/connection_test.clj +++ b/test/metabase/driver/sql_jdbc/connection_test.clj @@ -7,10 +7,12 @@ [metabase.driver.sql-jdbc.test-util :as sql-jdbc.tu] [metabase.driver.util :as driver.u] [metabase.models.database :refer [Database]] + [metabase.sync :as sync] [metabase.test :as mt] [metabase.test.data :as data] [metabase.test.fixtures :as fixtures] - [metabase.util :as u])) + [metabase.util :as u] + [toucan.db :as db])) (use-fixtures :once (fixtures/initialize :db)) @@ -76,39 +78,65 @@ (deftest connection-pool-invalidated-on-details-change (mt/test-drivers (sql-jdbc.tu/sql-jdbc-drivers) (testing "db->pooled-connection-spec marks a connection pool invalid if the db details map changes" - (let [db (mt/db) - hash-change-called (atom false) - hash-change-fn (fn [db-id] - (is (= (u/the-id db) db-id)) - (reset! hash-change-called true) - nil) - perturb-db-details (fn [db] - (update db :details (fn [details] - (cond-> details - ;; swap localhost and 127.0.0.1 - (= "localhost" (:host details)) - (assoc :host "127.0.0.1") + (let [db (mt/db) + hash-change-called-times (atom 0) + hash-change-fn (fn [db-id] + (is (= (u/the-id db) db-id)) + (swap! hash-change-called-times inc) + nil) + perturb-db-details (fn [db] + (update db + :details + (fn [details] + (case driver/*driver* + :redshift + (assoc details :additional-options "defaultRowFetchSize=1000") - (= "127.0.0.1" (:host details)) - (assoc :host "localhost") + (cond-> details + ;; swap localhost and 127.0.0.1 + (= "localhost" (:host details)) + (assoc :host "127.0.0.1") - :else - (assoc :new-config "something")))))] - (sql-jdbc.conn/invalidate-pool-for-db! db) - ;; a little bit hacky to redefine the log fn, but it's the most direct way to test - (with-redefs [sql-jdbc.conn/log-db-details-hash-change-msg! hash-change-fn] - (let [pool-spec-1 (sql-jdbc.conn/db->pooled-connection-spec db) - db-hash-1 (get @@#'sql-jdbc.conn/database-id->db-details-hashes (u/the-id db))] - (testing "hash value calculated correctly for new pooled conn" - (is (some? pool-spec-1)) - (is (integer? db-hash-1)) - (is (not= db-hash-1 0))) - (testing "changing DB details results in hash value changing and connection being invalidated" - (let [db-perturbed (perturb-db-details db) - pool-spec-2 (sql-jdbc.conn/db->pooled-connection-spec db-perturbed) - db-hash-2 (get @@#'sql-jdbc.conn/database-id->db-details-hashes (u/the-id db))] - (is (some? pool-spec-2)) - (is (true? @hash-change-called)) - (is (integer? db-hash-2)) - (is (not= db-hash-2 0)) - (is (not= db-hash-1 db-hash-2)))))))))) + (= "127.0.0.1" (:host details)) + (assoc :host "localhost") + + :else + (assoc :new-config "something"))))))] + (try + (sql-jdbc.conn/invalidate-pool-for-db! db) + ;; a little bit hacky to redefine the log fn, but it's the most direct way to test + (with-redefs [sql-jdbc.conn/log-db-details-hash-change-msg! hash-change-fn] + (let [pool-spec-1 (sql-jdbc.conn/db->pooled-connection-spec db) + db-hash-1 (get @@#'sql-jdbc.conn/database-id->db-details-hashes (u/the-id db))] + (testing "hash value calculated correctly for new pooled conn" + (is (some? pool-spec-1)) + (is (integer? db-hash-1)) + (is (not= db-hash-1 0))) + (testing "changing DB details results in hash value changing and connection being invalidated" + (let [db-perturbed (perturb-db-details db)] + (db/update! Database (mt/id) :details (:details db-perturbed)) + (let [;; this call should result in the connection pool becoming invalidated, and the new hash value + ;; being stored based upon these updated details + pool-spec-2 (sql-jdbc.conn/db->pooled-connection-spec db-perturbed) + db-hash-2 (get @@#'sql-jdbc.conn/database-id->db-details-hashes (u/the-id db))] + ;; to throw a wrench into things, kick off a sync of the original db (unperturbed); this + ;; simulates a long running sync that began before the perturbed details were saved to the app DB + ;; the sync steps SHOULD NOT invalidate the connection pool, because doing so could cause a seesaw + ;; effect that continuously invalidates the connection pool on every sync step and query, which + ;; wreaks havoc (#18499) + ;; instead, the connection pool code will simply fetch the newest DatabaseInstance it + ;; can find in the app DB, in the case of a hash mismatch, and check AGAIN to see whether the hash + ;; still doesn't match (in this test case, it should actually match this time, because we updated + ;; the app DB with the perturbed DatabaseInstance above here) + ;; this should still see a hash mismatch in the case that the DB details were updated external to + ;; this process (i.e. by a different instance), since our in-memory hash value still wouldn't match + ;; even after getting the latest `DatabaseInstance` + (sync/sync-database! db {:scan :schema}) + (is (some? pool-spec-2)) + (is (= 1 @hash-change-called-times)) + (is (integer? db-hash-2)) + (is (not= db-hash-2 0)) + (is (not= db-hash-1 db-hash-2))))))) + (finally + ;; restore the original test DB details, no matter what just happened + (db/update! Database (mt/id) :details (:details db))))))))