From 35a487d367112e199d7e25a68923154321188126 Mon Sep 17 00:00:00 2001 From: Ngoc Khuat <qn.khuat@gmail.com> Date: Mon, 13 May 2024 22:17:44 +0700 Subject: [PATCH] Static viz get background color thread safe (#42548) --- src/metabase/pulse/render/color.clj | 18 ++++++++---------- src/metabase/pulse/render/js_engine.clj | 9 +++++++++ test/metabase/db/query_test.clj | 16 +++------------- test/metabase/pulse/render/body_test.clj | 10 ++-------- test/metabase/pulse/render/color_test.clj | 18 +++++++++++++++++- test/metabase/pulse/render/js_engine_test.clj | 9 ++++----- test/metabase/test.clj | 1 + test/metabase/test/util.clj | 14 +++++++++++++- 8 files changed, 57 insertions(+), 38 deletions(-) diff --git a/src/metabase/pulse/render/color.clj b/src/metabase/pulse/render/color.clj index 81456d5a337..8c05ad76c02 100644 --- a/src/metabase/pulse/render/color.clj +++ b/src/metabase/pulse/render/color.clj @@ -16,16 +16,14 @@ (def ^:private js-file-path "frontend_shared/color_selector.js") (def ^:private ^{:arglists '([])} js-engine - ;; The code that loads the JS engine is behind a delay so that we don't incur that cost on startup. The below - ;; assertion till look for the javascript file at startup and fail if it doesn't find it. This is to avoid a big - ;; delay in finding out that the system is broken - (let [file-url (io/resource js-file-path)] - (assert file-url (trs "Can''t find JS color selector at ''{0}''" js-file-path)) - (let [dlay (delay - (doto (js/context) - (js/load-resource js-file-path)))] - (fn [] - @dlay)))) + ;; As of 2024/05/13, a single color selector js engine takes 3.5 MiB of memory + (js/threadlocal-fifo-memoizer + (fn [] + (let [file-url (io/resource js-file-path)] + (assert file-url (trs "Can''t find JS color selector at ''{0}''" js-file-path)) + (doto (js/context) + (js/load-resource js-file-path)))) + 5)) (def ^:private QueryResults "This is a pretty loose schema, more as a safety net as we have a long feedback loop for this being broken as it's diff --git a/src/metabase/pulse/render/js_engine.clj b/src/metabase/pulse/render/js_engine.clj index 916a0cce3aa..6a331924599 100644 --- a/src/metabase/pulse/render/js_engine.clj +++ b/src/metabase/pulse/render/js_engine.clj @@ -7,6 +7,7 @@ Javadocs: https://www.graalvm.org/truffle/javadoc/overview-summary.html" (:require + [clojure.core.memoize :as memoize] [clojure.java.io :as io] [metabase.util.i18n :refer [trs]]) (:import @@ -14,6 +15,14 @@ (set! *warn-on-reflection* true) +(defn threadlocal-fifo-memoizer + "Returns a memoizer that is unique to each thread." + [thunk threshold] + (memoize/fifo + (with-meta thunk {::memoize/args-fn (fn [_] + [(.getId (Thread/currentThread))])}) + :fifo/threshold threshold)) + (defn context "Create a new org.graalvm.polyglot.Context suitable to evaluate javascript" ^Context [] diff --git a/test/metabase/db/query_test.clj b/test/metabase/db/query_test.clj index 385b1403952..3c8f5d01956 100644 --- a/test/metabase/db/query_test.clj +++ b/test/metabase/db/query_test.clj @@ -84,16 +84,6 @@ (verify-same-query q))))) -(defn- repeat-concurrently [n f] - ;; Use a latch to ensure that the functions start as close to simultaneously as possible. - (let [latch (CountDownLatch. n) - futures (atom [])] - (dotimes [_ n] - (swap! futures conj (future (.countDown latch) - (.await latch) - (f)))) - (into #{} (map deref) @futures))) - (deftest select-or-insert!-test ;; We test both a case where the database protects against duplicates, and where it does not. ;; Using Setting is perfect because it has only two required fields - (the primary) key & value (with no constraint). @@ -121,7 +111,7 @@ (.countDown latch) (.await latch) {other-col (str (random-uuid))}))) - results (repeat-concurrently threads thunk) + results (set (mt/repeat-concurrently threads thunk)) n (count results) latest (t2/select-one Setting search-col search-value)] @@ -181,7 +171,7 @@ (.countDown latch) (.await latch) {other-col <>})))) - values-set (repeat-concurrently threads thunk) + values-set (set (mt/repeat-concurrently threads thunk)) latest (get (t2/select-one Setting search-col search-value) other-col)] (testing "each update tried to set a different value" @@ -199,7 +189,7 @@ (testing "After the database is created, it does not create further duplicates" (let [count (t2/count Setting search-col search-value)] (is (pos? count)) - (is (empty? (set/intersection values-set (repeat-concurrently threads thunk)))) + (is (empty? (set/intersection values-set (set (mt/repeat-concurrently threads thunk))))) (is (= count (t2/count Setting search-col search-value)))))) ;; Since we couldn't use with-temp, we need to clean up manually. diff --git a/test/metabase/pulse/render/body_test.clj b/test/metabase/pulse/render/body_test.clj index 47aa3ca8d41..55d758a7233 100644 --- a/test/metabase/pulse/render/body_test.clj +++ b/test/metabase/pulse/render/body_test.clj @@ -757,12 +757,6 @@ [render-type card data] (body/render render-type :attachment (pulse/defaulted-timezone card) card nil data)) -(defn execute-n-times-in-parallel - "Execute f n times in parallel and derefs all the results." - [f n] - (mapv deref (for [_ (range n)] - (future (f))))) - (deftest render-cards-are-thread-safe-test-for-js-visualization (mt/with-temp [:model/Card card {:dataset_query (mt/mbql-query orders {:aggregation [[:count]] @@ -772,10 +766,10 @@ :visualization_settings {:graph.dimensions ["CREATED_AT"] :graph.metrics ["count"]}}] (let [data (:data (qp/process-query (:dataset_query card)))] - (is (every? some? (execute-n-times-in-parallel #(render-card :javascript_visualization card data) 3)))))) + (is (every? some? (mt/repeat-concurrently 3 #(render-card :javascript_visualization card data))))))) (deftest render-cards-are-thread-safe-test-for-table (mt/with-temp [:model/Card card {:dataset_query (mt/mbql-query venues {:limit 1}) :display :table}] (let [data (:data (qp/process-query (:dataset_query card)))] - (is (every? some? (execute-n-times-in-parallel #(render-card :table card data) 3)))))) + (is (every? some? (mt/repeat-concurrently 3 #(render-card :table card data))))))) diff --git a/test/metabase/pulse/render/color_test.clj b/test/metabase/pulse/render/color_test.clj index 433252bfb45..d5a9d617ef2 100644 --- a/test/metabase/pulse/render/color_test.clj +++ b/test/metabase/pulse/render/color_test.clj @@ -2,7 +2,8 @@ (:require [clojure.test :refer :all] [metabase.pulse.render.color :as color] - [metabase.pulse.render.js-engine :as js])) + [metabase.pulse.render.js-engine :as js] + [metabase.test :as mt])) (def ^:private red "#ff0000") (def ^:private green "#00ff00") @@ -50,3 +51,18 @@ (is (= [red green red green] (for [row-index (range 0 4)] (color/get-background-color color-selector "any value" "any column" row-index)))))))) + +(deftest render-color-is-thread-safe-test + (is (every? some? + (mt/repeat-concurrently + 3 + (fn [] + (color/get-background-color (color/make-color-selector {:cols [{:name "test"}] + :rows [[5] [5]]} + {:table.column_formatting[{:columns ["test"], + :type :single, + :operator "=", + :value 5, + :color "#ff0000", + :highlight_row true}]}) + "any value" "test" 1)))))) diff --git a/test/metabase/pulse/render/js_engine_test.clj b/test/metabase/pulse/render/js_engine_test.clj index 9f194ee7536..79eca9b9bed 100644 --- a/test/metabase/pulse/render/js_engine_test.clj +++ b/test/metabase/pulse/render/js_engine_test.clj @@ -1,8 +1,8 @@ (ns metabase.pulse.render.js-engine-test (:require [clojure.test :refer :all] - [metabase.pulse.render.body-test :as body-test] - [metabase.pulse.render.js-engine :as js])) + [metabase.pulse.render.js-engine :as js] + [metabase.test :as mt])) (set! *warn-on-reflection* true) @@ -23,6 +23,5 @@ (let [context (js/context)] (js/load-js-string context "function plus (x, y) { return x + y }" "plus test") (is (= (repeat 10 2) - (body-test/execute-n-times-in-parallel - #(.asLong (js/execute-fn-name context "plus" 1 1)) - 10)))))) + (mt/repeat-concurrently 10 + #(.asLong (js/execute-fn-name context "plus" 1 1)))))))) diff --git a/test/metabase/test.clj b/test/metabase/test.clj index 0d621a0777d..90c0387797e 100644 --- a/test/metabase/test.clj +++ b/test/metabase/test.clj @@ -243,6 +243,7 @@ secret-value-equals? select-keys-sequentially throw-if-called! + repeat-concurrently with-all-users-permission with-column-remappings with-discarded-collections-perms-changes diff --git a/test/metabase/test/util.clj b/test/metabase/test/util.clj index ad8152fd56d..11e6500e79a 100644 --- a/test/metabase/test/util.clj +++ b/test/metabase/test/util.clj @@ -55,7 +55,7 @@ (java.io File FileInputStream) (java.net ServerSocket) (java.util Locale) - (java.util.concurrent TimeoutException) + (java.util.concurrent CountDownLatch TimeoutException) (org.quartz CronTrigger JobDetail JobKey Scheduler Trigger) (org.quartz.impl StdSchedulerFactory))) @@ -1409,3 +1409,15 @@ {:order-by [[:id :desc]] :where [:and (when topic [:= :topic (name topic)]) (when model-id [:= :model_id model-id])]}))) + +(defn repeat-concurrently + "Run `f` `n` times concurrently. Returns a vector of the results of each invocation of `f`." + [n f] + ;; Use a latch to ensure that the functions start as close to simultaneously as possible. + (let [latch (CountDownLatch. n) + futures (atom [])] + (dotimes [_ n] + (swap! futures conj (future (.countDown latch) + (.await latch) + (f)))) + (mapv deref @futures))) -- GitLab