diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index 8bda9e1d0..8beaa78ec 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -361,12 +361,20 @@ [data] (org.postgresql.util.PGInterval. ^String data)) +(defn connection? + [conn] + (instance? Connection conn)) + (defn savepoint ([^Connection conn] (.setSavepoint conn)) ([^Connection conn label] (.setSavepoint conn (name label)))) +(defn release! + [^Connection conn ^Savepoint sp ] + (.releaseSavepoint conn sp)) + (defn rollback! ([^Connection conn] (.rollback conn)) diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index d98c91c9b..6e0973349 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -23,8 +23,8 @@ [app.loggers.webhooks :as-alias webhooks] [app.main :as-alias main] [app.rpc :as-alias rpc] + [app.rpc.retry :as rtry] [app.tokens :as tokens] - [app.util.retry :as rtry] [app.util.services :as-alias sv] [app.util.time :as dt] [app.worker :as wrk] @@ -203,7 +203,8 @@ ;; this case we just retry the operation. (rtry/with-retry {::rtry/when rtry/conflict-exception? ::rtry/max-retries 6 - ::rtry/label "persist-audit-log"} + ::rtry/label "persist-audit-log" + ::db/conn (dm/check db/connection? conn-or-pool)} (let [now (dt/now)] (db/insert! conn-or-pool :audit-log (-> params diff --git a/backend/src/app/rpc/commands/comments.clj b/backend/src/app/rpc/commands/comments.clj index c7c9b1606..e91529a3b 100644 --- a/backend/src/app/rpc/commands/comments.clj +++ b/backend/src/app/rpc/commands/comments.clj @@ -19,8 +19,8 @@ [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.quotes :as quotes] + [app.rpc.retry :as rtry] [app.util.pointer-map :as pmap] - [app.util.retry :as rtry] [app.util.services :as sv] [app.util.time :as dt] [clojure.spec.alpha :as s])) @@ -309,7 +309,8 @@ (rtry/with-retry {::rtry/when rtry/conflict-exception? ::rtry/max-retries 3 - ::rtry/label "create-comment-thread"} + ::rtry/label "create-comment-thread" + ::db/conn conn} (create-comment-thread conn {:created-at request-at :profile-id profile-id diff --git a/backend/src/app/rpc/retry.clj b/backend/src/app/rpc/retry.clj index f0dab0f67..9cb048ea9 100644 --- a/backend/src/app/rpc/retry.clj +++ b/backend/src/app/rpc/retry.clj @@ -5,19 +5,20 @@ ;; Copyright (c) KALEIDOS INC (ns app.rpc.retry - "A fault tolerance RPC middleware. Allow retry some operations that we - know we can retry." (:require [app.common.logging :as l] - [app.util.retry :refer [conflict-exception?]] - [app.util.services :as sv])) + [app.db :as db] + [app.util.services :as sv]) + (:import + org.postgresql.util.PSQLException)) -(defn conflict-db-insert? +(defn conflict-exception? "Check if exception matches a insertion conflict on postgresql." [e] - (conflict-exception? e)) + (and (instance? PSQLException e) + (= "23505" (.getSQLState ^PSQLException e)))) -(def always-false (constantly false)) +(def ^:private always-false (constantly false)) (defn wrap-retry [_ f {:keys [::matches ::sv/name] :or {matches always-false} :as mdata}] @@ -40,3 +41,23 @@ (throw cause))))) 1)) f)) +(defmacro with-retry + [{:keys [::when ::max-retries ::label ::db/conn] :or {max-retries 3}} & body] + `(let [conn# ~conn] + (assert (or (nil? conn#) (db/connection? conn#)) "invalid database connection") + (loop [tnum# 1] + (let [result# (let [sp# (some-> conn# db/savepoint)] + (try + (let [result# (do ~@body)] + (some->> sp# (db/release! conn#)) + result#) + (catch Throwable cause# + (some->> sp# (db/rollback! conn#)) + (if (and (~when cause#) (<= tnum# ~max-retries)) + ::retry + (throw cause#)))))] + (if (= ::retry result#) + (do + (l/warn :hint "retrying operation" :label ~label :retry tnum#) + (recur (inc tnum#))) + result#))))) diff --git a/backend/src/app/util/retry.clj b/backend/src/app/util/retry.clj deleted file mode 100644 index cd3ae6d91..000000000 --- a/backend/src/app/util/retry.clj +++ /dev/null @@ -1,34 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.util.retry - "A fault tolerance helpers. Allow retry some operations that we know - we can retry." - (:require - [app.common.logging :as l]) - (:import - org.postgresql.util.PSQLException)) - -(defn conflict-exception? - "Check if exception matches a insertion conflict on postgresql." - [e] - (and (instance? PSQLException e) - (= "23505" (.getSQLState ^PSQLException e)))) - -(defmacro with-retry - [{:keys [::when ::max-retries ::label] :or {max-retries 3}} & body] - `(loop [tnum# 1] - (let [result# (try - ~@body - (catch Throwable cause# - (if (and (~when cause#) (<= tnum# ~max-retries)) - ::retry - (throw cause#))))] - (if (= ::retry result#) - (do - (l/warn :hint "retrying operation" :label ~label :retry tnum#) - (recur (inc tnum#))) - result#)))) diff --git a/common/src/app/common/data/macros.cljc b/common/src/app/common/data/macros.cljc index 76a168459..c8a8e5db9 100644 --- a/common/src/app/common/data/macros.cljc +++ b/common/src/app/common/data/macros.cljc @@ -108,6 +108,14 @@ `(do ~@body) (reverse (partition 2 bindings)))) +(defmacro check + "Applies a predicate to the value, if result is true, return the + value if not, returns nil." + [pred-fn value] + `(if (~pred-fn ~value) + ~value + nil)) + (defmacro get-prop "A macro based, optimized variant of `get` that access the property directly on CLJS, on CLJ works as get."