From dc39729d565b95dfb09fc65f3e069288c3b801cb Mon Sep 17 00:00:00 2001 From: Cam Saul <1455846+camsaul@users.noreply.github.com> Date: Thu, 29 Oct 2020 19:48:47 -0700 Subject: [PATCH] Fix collections graph stack overflow (#13618) * Fix collections/graph stack overflow * Test fixes :wrench: [ci postgres] [ci mysql] * Show engine in Task trouble shooting logs (#13608) * Show engine in Task trouble shooting logs in an effort to get better feedback when people raise issues about long running sync processes, include the database engine in the logs. Also wrapped `th` in a `tr` inside the `thead` to remove react warning * Const intead of let on the db_id_to_engine * Swap db id for db name * Move comment out of jsx * fixup! Merge branch 'release-x.37.x' into fix-collections-graph-stack-overflow Co-authored-by: dpsutton <dan@dpsutton.com> --- resources/log4j2.xml | 1 + resources/migrations/000_migrations.yaml | 22 ++++++++++ src/metabase/models/collection/graph.clj | 22 +++++----- src/metabase/util/honeysql_extensions.clj | 4 +- .../metabase/models/collection/graph_test.clj | 42 ++++++++++++++++++- test/metabase/test/util.clj | 2 +- 6 files changed, 76 insertions(+), 17 deletions(-) diff --git a/resources/log4j2.xml b/resources/log4j2.xml index f71bc3a827a..cc2636dd4c9 100644 --- a/resources/log4j2.xml +++ b/resources/log4j2.xml @@ -23,6 +23,7 @@ <Loggers> <Logger name="metabase" level="INFO"/> + <Logger name="metabase-enterprise" level="INFO"/> <Logger name="metabase.plugins" level="DEBUG"/> <Logger name="metabase.middleware" level="DEBUG"/> <Logger name="metabase.query-processor.async" level="DEBUG"/> diff --git a/resources/migrations/000_migrations.yaml b/resources/migrations/000_migrations.yaml index b8c0fae938b..6a9fc3a0c83 100644 --- a/resources/migrations/000_migrations.yaml +++ b/resources/migrations/000_migrations.yaml @@ -7722,3 +7722,25 @@ databaseChangeLog: tableName: core_user columnName: email newDataType: varchar_ignorecase(254) + + - changeSet: + id: 273 + author: camsaul + comment: Added 0.37.1 + changes: + - addDefaultValue: + tableName: core_user + columnName: is_superuser + columnDataType: boolean + defaultValueBoolean: false + + - changeSet: + id: 274 + author: camsaul + comment: Added 0.37.1 + changes: + - addDefaultValue: + tableName: core_user + columnName: is_active + columnDataType: boolean + defaultValueBoolean: true diff --git a/src/metabase/models/collection/graph.clj b/src/metabase/models/collection/graph.clj index 338ce8a5f54..18abbcc90a3 100644 --- a/src/metabase/models/collection/graph.clj +++ b/src/metabase/models/collection/graph.clj @@ -1,6 +1,5 @@ (ns metabase.models.collection.graph (:require [clojure.data :as data] - [honeysql.helpers :as h] [metabase.api.common :as api :refer [*current-user-id*]] [metabase.models [collection :as collection :refer [Collection]] @@ -8,7 +7,9 @@ [permissions :as perms :refer [Permissions]] [permissions-group :refer [PermissionsGroup]]] [metabase.util :as u] - [metabase.util.schema :as su] + [metabase.util + [honeysql-extensions :as hx] + [schema :as su]] [schema.core :as s] [toucan.db :as db])) @@ -57,16 +58,13 @@ Collections (i.e., things that you can set Permissions for, and that should go in the graph.)" [collection-namespace :- (s/maybe su/KeywordOrString)] (let [personal-collection-ids (db/select-ids Collection :personal_owner_id [:not= nil]) - honeysql-form (cond-> {:select [[:id :id]] - :from [Collection] - :where [:= :namespace (u/qualified-name collection-namespace)]} - (seq personal-collection-ids) - (h/merge-where [:not-in :id (set personal-collection-ids)])) - honeysql-form (reduce - (fn [honeysql-form collection-id] - (h/merge-where honeysql-form [:not [:like :location (format "/%d/%%" collection-id)]])) - honeysql-form - personal-collection-ids)] + honeysql-form {:select [[:id :id]] + :from [Collection] + :where (into [:and + [:= :namespace (u/qualified-name collection-namespace)] + [:= :personal_owner_id nil]] + (for [collection-id personal-collection-ids] + [:not [:like :location (hx/literal (format "/%d/%%" collection-id))]]))}] (set (map :id (db/query honeysql-form))))) (s/defn graph :- PermissionsGraph diff --git a/src/metabase/util/honeysql_extensions.clj b/src/metabase/util/honeysql_extensions.clj index 4d2bda5f79b..edc518a8718 100644 --- a/src/metabase/util/honeysql_extensions.clj +++ b/src/metabase/util/honeysql_extensions.clj @@ -186,8 +186,8 @@ (def ^{:arglists '([& exprs])} concat "SQL `concat` function." (partial hsql/call :concat)) ;; Etc (Dev Stuff) -(alter-meta! #'honeysql.core/format assoc :style/indent 1) -(alter-meta! #'honeysql.core/call assoc :style/indent 1) +(alter-meta! #'honeysql.core/format assoc :style/indent :defn) +(alter-meta! #'honeysql.core/call assoc :style/indent :defn) (require 'honeysql.types) (extend-protocol PrettyPrintable diff --git a/test/metabase/models/collection/graph_test.clj b/test/metabase/models/collection/graph_test.clj index 17cb9a66ef3..f9c6cfcc140 100644 --- a/test/metabase/models/collection/graph_test.clj +++ b/test/metabase/models/collection/graph_test.clj @@ -1,7 +1,9 @@ (ns metabase.models.collection.graph-test - (:require [clojure.test :refer :all] + (:require [clojure.java.jdbc :as jdbc] + [clojure.test :refer :all] [medley.core :as m] [metabase + [models :refer [User]] [test :as mt] [util :as u]] [metabase.api.common :refer [*current-user-id*]] @@ -14,7 +16,8 @@ [metabase.test.fixtures :as fixtures] [metabase.util.schema :as su] [schema.core :as s] - [toucan.db :as db])) + [toucan.db :as db] + [toucan.util.test :as tt])) (use-fixtures :once (fixtures/initialize :db :test-users :test-users-personal-collections)) @@ -415,3 +418,38 @@ :after {(keyword (str group-id)) {:root (s/eq "write")}} s/Keyword s/Any} (db/select-one CollectionRevision {:order-by [[:id :desc]]}))))))))))) + +(defn- do-with-n-temp-users-with-personal-collections! [num-users thunk] + (mt/with-model-cleanup [User Collection] + ;; insert all the users + (let [user-ids (jdbc/execute! + (db/connection) + (db/honeysql->sql + {:insert-into User + :values (repeatedly num-users #(assoc (tt/with-temp-defaults User) :date_joined :%now))})) + max-id (:max-id (db/select-one [User [:%max.id :max-id]])) + ;; determine the range of IDs we inserted -- MySQL doesn't support INSERT INTO ... RETURNING like Postgres + ;; so this is the fastest way to do this + user-ids (range (inc (- max-id num-users)) (inc max-id))] + (assert (= (count user-ids) num-users)) + ;; insert the Collections + (jdbc/execute! + (db/connection) + (db/honeysql->sql + {:insert-into Collection + :values (for [user-id user-ids + :let [collection (tt/with-temp-defaults Collection)]] + (assoc collection + :personal_owner_id user-id + :slug "my_collection"))}))) + ;; now run the thunk + (thunk))) + +(defmacro ^:private with-n-temp-users-with-personal-collections [num-users & body] + `(do-with-n-temp-users-with-personal-collections! ~num-users (fn [] ~@body))) + +(deftest mega-graph-test + (testing "A truly insane amount of Personal Collections shouldn't cause a Stack Overflow (#13211)" + (with-n-temp-users-with-personal-collections 2000 + (is (>= (db/count Collection :personal_owner_id [:not= nil]) 2000)) + (is (map? (graph/graph)))))) diff --git a/test/metabase/test/util.clj b/test/metabase/test/util.clj index aba9e2a1c28..741b651af5c 100644 --- a/test/metabase/test/util.clj +++ b/test/metabase/test/util.clj @@ -561,7 +561,7 @@ ;; might not have an old max ID if this is the first time the macro is used in this test run. :let [old-max-id (or (get model->old-max-id model) 0)]] - (db/delete! model :id [:> old-max-id])))))) + (db/simple-delete! model :id [:> old-max-id])))))) (defmacro with-model-cleanup "Execute `body`, then delete any *new* rows created for each model in `models`. Calls `delete!`, so if the model has -- GitLab