Commits

Shantanu Kumar  committed 8b5a0a0

add spring transaction (programmatic and declarative) support

  • Participants
  • Parent commits 5882021

Comments (0)

Files changed (3)

File src/main/clj/org/bituf/fountain/transaction.clj

+(ns org.bituf.fountain.transaction
+  "Deal with Spring-JDBC transactions
+  See also:
+    Reference: http://static.springsource.org/spring/docs/3.0.x/reference/transaction.html
+    API docs: http://static.springsource.org/spring/docs/3.0.x/javadoc-api/"
+  (:import
+    (java.util List Map)
+    (javax.sql DataSource)
+    (org.bituf.fountain.jdbc                     CustomTransactionTemplate)
+    (org.springframework.jdbc.datasource         DataSourceTransactionManager)
+    (org.springframework.transaction             PlatformTransactionManager
+                                                 TransactionDefinition
+                                                 TransactionStatus)
+    (org.springframework.transaction.support     DefaultTransactionDefinition
+                                                 TransactionCallback
+                                                 ;TransactionTemplate
+                                                 )
+    (org.springframework.transaction.interceptor DefaultTransactionAttribute
+                                                 NoRollbackRuleAttribute
+                                                 RollbackRuleAttribute
+                                                 RuleBasedTransactionAttribute
+                                                 TransactionAttribute))
+  (:require
+    [org.bituf.clj-miscutil :as mu]
+    [org.bituf.clj-dbspec   :as sp]))
+
+
+(def ^{:doc "Isolation Level keys"}
+      isolation-keys [:default :read-committed :read-uncommitted
+                      :repeatable-read :serializable])
+
+
+(def ^{:doc "Isolation Levels"}
+      isolation-levels (zipmap isolation-keys
+                         [TransactionDefinition/ISOLATION_DEFAULT
+                          TransactionDefinition/ISOLATION_READ_COMMITTED
+                          TransactionDefinition/ISOLATION_READ_UNCOMMITTED
+                          TransactionDefinition/ISOLATION_REPEATABLE_READ
+                          TransactionDefinition/ISOLATION_SERIALIZABLE]))
+
+
+(def ^{:doc "Propagation Behavior keys"}
+      propagation-keys [:mandatory :nested :never :not-supported :required
+                        :requires-new :supports])
+
+
+(def ^{:doc "Propagation Behaviors"}
+      propagation-behaviors (zipmap propagation-keys
+                              [TransactionDefinition/PROPAGATION_MANDATORY
+                               TransactionDefinition/PROPAGATION_NESTED
+                               TransactionDefinition/PROPAGATION_NEVER
+                               TransactionDefinition/PROPAGATION_NOT_SUPPORTED
+                               TransactionDefinition/PROPAGATION_REQUIRED
+                               TransactionDefinition/PROPAGATION_REQUIRES_NEW
+                               TransactionDefinition/PROPAGATION_SUPPORTS]))
+
+
+(def ^{:doc "PlatformTransaction manager key in DB-Spec"}
+      TXNM-KEY :fountain.transaction.txnm)
+
+
+(def ^{:doc "TransactionTemplate key in DB-Spec"}
+      TXNT-KEY :fountain.transaction.txnt)
+
+
+(def ^{:doc "TransactionStatus object for the current transaction."
+       :tag TransactionStatus
+       :dynamic true}
+      *txn-status* nil)
+
+
+(defn ^DefaultTransactionAttribute make-txndef
+  "Return constraints based TransactionDefinition implementation.
+  Optional args:
+    :isolation        Transaction isolation level (default is :default), which
+                      can be either of the following:
+                       :default, :read-committed, :read-uncommitted,
+                       :repeatable-read, :serializable
+
+    :propagation      Transaction propagation behavior (default is :required),
+                      which can be either of the following:
+                       :mandatory, :nested, :never, :not-supported,
+                       :required, :requires-new, :supports
+
+    :read-only        Boolean (default false) - optimization parameter for the
+                      underlying transaction sub-system, which may or may not
+                      respond to this parameter. However, this flag will cause
+                      :read-only to be turned on in the current DB-Spec.
+                      See: org.springframework.transaction.TransactionDefinition/isReadOnly
+
+    :txn-name         Transaction name (String) that shows up on the monitoring
+                      console of the application server (e.g. on WebLogic)
+
+    :rollback-on      A predicate function that accepts the thrown exception
+                      (java.lang.Throwable) as the only argument and returns
+                      true if the transaction should roll back, false otherwise.
+                      Note: This argument is not allowed with `:rollback-for` or
+                      `:no-rollback-for`.
+
+    :rollback-for     List containing exception Class objects/names as string
+                      on which transaction should be rolled back.
+                      Note: This argument is not allowed with `:rollback-on`.
+
+    :no-rollback-for  List containing exception Class objects/names as string
+                      on which transaction should NOT be rolled back, but rather
+                      should be committed as it is. However, the exception will
+                      still be thrown after the transaction is committed.
+                      Note: This argument is not allowed with `:rollback-on`.
+
+    :timeout-sec      Timeout (in seconds) for the transaction (only if the
+                      underlying transaction sub-system supports it.)
+                      See: org.springframework.transaction.TransactionDefinition/getTimeout
+  See also:
+    http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/transaction/interceptor/RollbackRuleAttribute.html
+    Short URL: http://j.mp/ftIHsM"
+  [& {:keys [isolation propagation read-only ^String txn-name rollback-on
+             rollback-for no-rollback-for ^Integer timeout-sec]
+      :or {isolation       nil ; :default
+           propagation     nil ; :required
+           read-only       nil ; false
+           txn-name        nil ; nil (no name)
+           rollback-on     nil ; e.g. #(instance? RuntimeException %)
+           rollback-for    nil ; e.g. [RuntimeException Error]
+           no-rollback-for nil ; e.g. [com.foo.business.NoInstrumentException]
+           timeout-sec     nil ; TransactionDefinition/TIMEOUT_DEFAULT
+           }
+      :as opt}]
+  {:post [(instance? DefaultTransactionAttribute %)]
+   :pre  [(mu/verify-opt #{:isolation :propagation :read-only :txn-name
+                           :rollback-on :rollback-for :no-rollback-for
+                           :timeout-sec} opt)
+          (mu/verify-arg (or (nil? isolation  ) (mu/contains-val? isolation-keys   isolation)))
+          (mu/verify-arg (or (nil? propagation) (mu/contains-val? propagation-keys propagation)))
+          (mu/verify-arg (or (nil? read-only  ) (mu/boolean? read-only)))
+          (mu/verify-arg (or (nil? txn-name   ) (string? txn-name)))
+          (mu/verify-arg (or (nil? rollback-on) (fn? rollback-on)))
+          (mu/verify-arg (or (nil? rollback-for)
+                           (and (coll? rollback-for)
+                             (every? #(or (class? %) (string? %)) rollback-for))))
+          (mu/verify-arg (or (nil? no-rollback-for)
+                           (and (coll? no-rollback-for)
+                             (every? #(or (class? %) (string? %)) no-rollback-for))))
+          (mu/verify-arg (not (and rollback-on (or rollback-for no-rollback-for))))
+          (mu/verify-arg (or (nil? timeout-sec) (mu/posnum? timeout-sec)
+                           (= timeout-sec TransactionDefinition/TIMEOUT_DEFAULT)))]}
+  (let [^DefaultTransactionAttribute
+        td (cond
+             ;; user has specified a predicate to handle exception
+             (fn? rollback-on)  (proxy [DefaultTransactionAttribute] []
+                                  (rollbackOn [^Throwable ex]
+                                    (rollback-on ex)))
+             ;; user specified list of exceptions to rollback on/not to rollback on
+             (or rollback-for
+               no-rollback-for) (let [^RuleBasedTransactionAttribute
+                                      r (RuleBasedTransactionAttribute.)
+                                      y (map #(if (class? %)
+                                                (RollbackRuleAttribute. ^Class %)
+                                                (RollbackRuleAttribute. ^String %))
+                                          rollback-for)
+                                      n (map #(if (class? %)
+                                                (NoRollbackRuleAttribute. ^Class %)
+                                                (NoRollbackRuleAttribute. ^String %))
+                                          no-rollback-for)]
+                                  (.setRollbackRules r (into y n))
+                                  r)
+             ;; user did not specify what to do on exception, so apply default
+             ;; Note: DefaultTransactionAttribute rolls back only on unchecked
+             ;;       exceptions (RuntimeException, Error). Since it is not
+             ;;       mandatory to handle checked exceptions in Clojure we must
+             ;;       rollback on all exceptions without discrimination.
+             :else                (proxy [DefaultTransactionAttribute] []
+                                    (rollbackOn [^Throwable ex]
+                                      true)))]
+    (if isolation   (.setIsolationLevel      td ^Integer (get isolation-levels      isolation)))
+    (if propagation (.setPropagationBehavior td ^Integer (get propagation-behaviors propagation)))
+    (if read-only   (.setReadOnly td true) (.setReadOnly td false))
+    (if txn-name    (.setName     td txn-name))
+    (if timeout-sec (.setTimeout  td timeout-sec))
+    td))
+
+
+(defn ^PlatformTransactionManager
+       get-txnm
+  "Get PlatformTransactionManager instance from DB-Spec if available"
+  ([spec] {:pre [(map? spec)]}
+    (TXNM-KEY spec))
+  ([] (get-txnm sp/*dbspec*)))
+
+
+(defn ^CustomTransactionTemplate ; ^TransactionTemplate
+       get-txnt
+  "Get TransactionTemplate instance from DB-Spec if available"
+  ([spec] {:pre [(map? spec)]}
+    (TXNT-KEY spec))
+  ([] (get-txnt sp/*dbspec*)))
+
+
+(defn get-txn
+  "Get current transaction object (create if necessary) that can be used for
+  commit or rollback.
+  See also:
+    org.springframework.transaction.PlatformTransactionManager/getTransaction"
+  ([^PlatformTransactionManager txnm ^TransactionDefinition txndef]
+    {:pre [(instance? PlatformTransactionManager txnm)
+           (instance? TransactionDefinition      txndef)]}
+    (.getTransaction txnm txndef))
+  ([^TransactionDefinition txndef]
+    {:pre [(instance? TransactionDefinition      txndef)]}
+    (get-txn (get-txnm) txndef))
+  ([]
+    (get-txn (get-txnm) (get-txnt))))
+
+
+(defn commit
+  "Commit given/current transaction."
+  ([^PlatformTransactionManager txnm ^TransactionStatus status]
+    {:pre [(instance? PlatformTransactionManager txnm)
+           (instance? TransactionStatus          status)]}
+    (.commit txnm status))
+  ([^TransactionStatus status]
+    {:pre [(instance? TransactionStatus          status)]}
+    (commit (get-txnm) status))
+  ([]
+    (commit (get-txnm) *txn-status*)))
+
+
+(defn rollback
+  "Rollback given/current transaction."
+  ([^PlatformTransactionManager txnm ^TransactionStatus status]
+    {:pre [(instance? PlatformTransactionManager txnm)
+           (instance? TransactionStatus          status)]}
+    (.rollback txnm status))
+  ([^TransactionStatus status]
+    {:pre [(instance? TransactionStatus status)]}
+    (rollback (get-txnm) status))
+  ([]
+    (rollback (get-txnm) *txn-status*)))
+
+
+(defn make-txntspec
+  "Return a map with the following key associated to its respective value:
+    :fountain.transaction.txnm - PlatformTransactionManager instance
+    :fountain.transaction.txnt - TransactionTemplate instance
+    :read-only                 - Only from a specified transaction definition
+  See also:
+    with-context
+    clj-dbspec/*dbspec*"
+  [& {:keys [^DataSource datasource ^TransactionAttribute ; ^TransactionDefinition
+                                     txndef]
+      :or   {datasource  nil
+             txndef      nil}
+      :as opt}]
+  {:post [(mu/verify-cond (map? %))
+          (mu/verify-cond (contains? % TXNT-KEY))]
+   :pre  [(mu/verify-opt #{:datasource :txndef} opt)
+          (mu/verify-arg (or (nil? datasource) (instance? DataSource datasource)))
+          (mu/verify-arg (or (nil? txndef) (instance? TransactionAttribute ; TransactionDefinition
+                                             txndef)))]}
+  (let [ds   (or datasource (:datasource sp/*dbspec*)
+               (mu/illegal-arg "No valid DataSource found/supplied"))
+        txnm (DataSourceTransactionManager. ^DataSource ds)
+        txro (if txndef (.isReadOnly txndef) nil)
+        txnd (or txndef (make-txndef))
+        txnt (CustomTransactionTemplate. ; TransactionTemplate.
+                 ^PlatformTransactionManager txnm txnd)]
+     (if (nil? txro) {TXNM-KEY txnm
+                      TXNT-KEY txnt}
+       {:read-only txro
+        TXNM-KEY   txnm
+        TXNT-KEY   txnt})))
+
+
+(defn assoc-txnt
+  "Associate TransactionTemplate instance with DB-Spec. `txntspec` is either a
+  spec (map) or is a no-arg function that returns txn-template spec.
+  See also:
+    make-txntspec"
+  ([spec txntspec] {:post [(mu/verify-cond (map? %))]
+                    :pre  [(mu/verify-arg (map? spec))
+                           (mu/verify-arg (or (map? txntspec)
+                                            (fn? txntspec)))]}
+    (let [tspec (sp/with-dbspec spec
+                  (sp/value txntspec))]
+      (mu/verify-cond (map? tspec))
+      (mu/verify-cond (contains? tspec TXNT-KEY))
+      (sp/assoc-kvmap spec tspec)))
+  ([spec] {:post [(mu/verify-cond (map? %))]
+           :pre  [(mu/verify-arg (map? spec))
+                  (mu/verify-arg (contains? spec :datasource))
+                  (mu/verify-arg (instance? DataSource (:datasource spec)))]}
+    (assoc-txnt spec make-txntspec)))
+
+
+(defn wrap-txncallback
+  "Create TransactionTemplate instance and putting into clj-dbspec/*dbspec* as a
+  key execute f."
+  ([txntspec f] {:post [(fn? %)]
+                 :pre  [(and (map? txntspec) (contains? txntspec
+                                               TXNT-KEY))
+                        (fn? f)]}
+    (fn [& args]
+      (let [ts txntspec
+            tt ^CustomTransactionTemplate ; ^TransactionTemplate
+                (TXNT-KEY ts)
+            wf (sp/wrap-dbspec ts
+                 (fn [& args]
+                   (.execute tt (reify TransactionCallback
+                                  (doInTransaction [this ^TransactionStatus status]
+                                    (binding [*txn-status* status]
+                                      (apply f args)))))))]
+        (apply wf args))))
+  ([f] {:post [(fn? %)]
+        :pre  [(fn? f)]}
+    (if (get-txnt) f
+      (wrap-txncallback (make-txntspec) f))))

File src/main/java/org/bituf/fountain/jdbc/CustomTransactionTemplate.java

+package org.bituf.fountain.jdbc;
+
+import java.lang.reflect.UndeclaredThrowableException;
+
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionException;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.TransactionSystemException;
+import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
+import org.springframework.transaction.interceptor.TransactionAttribute;
+import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager;
+import org.springframework.transaction.support.TransactionCallback;
+import org.springframework.transaction.support.TransactionTemplate;
+import org.springframework.util.Assert;
+
+
+@SuppressWarnings("serial")
+public class CustomTransactionTemplate extends TransactionTemplate {
+    
+    protected final PlatformTransactionManager transactionManager;
+    protected final TransactionAttribute transactionAttribute;
+    
+    public CustomTransactionTemplate(PlatformTransactionManager transactionManager) {
+        super(transactionManager);
+        Assert.notNull(transactionManager, "`transactionManager` must not be null");
+        this.transactionManager = transactionManager;
+        this.transactionAttribute = new DefaultTransactionAttribute();
+    }
+    
+    public CustomTransactionTemplate(PlatformTransactionManager transactionManager,
+            TransactionAttribute transactionAttribute) {
+        super(transactionManager, transactionAttribute);
+        Assert.notNull(transactionManager, "`transactionManager` must not be null");
+        Assert.notNull(transactionAttribute, "`transactionAttribute` must not be null");
+        this.transactionManager = transactionManager;
+        this.transactionAttribute = transactionAttribute;
+    }
+    
+    public TransactionAttribute getTransactionAttribute() {
+        return transactionAttribute;
+    }
+    
+    @Override
+    public <T> T execute(TransactionCallback<T> action)
+            throws TransactionException {
+        if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
+            return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
+        }
+        else {
+            TransactionStatus status = this.transactionManager.getTransaction(this);
+            T result;
+            try {
+                result = action.doInTransaction(status);
+            }
+            catch (RuntimeException ex) {
+                if (this.transactionAttribute.rollbackOn(ex)) {
+                    // Transactional code threw application exception -> rollback
+                    rollbackOnException(status, ex);
+                } else {
+                    this.transactionManager.commit(status);
+                }
+                throw ex;
+            }
+            catch (Error err) {
+                if (this.transactionAttribute.rollbackOn(err)) {
+                    // Transactional code threw error -> rollback
+                    rollbackOnException(status, err);
+                } else {
+                    this.transactionManager.commit(status);
+                }
+                throw err;
+            }
+            catch (Exception ex) {
+                if (this.transactionAttribute.rollbackOn(ex)) {
+                    // Transactional code threw unexpected exception -> rollback
+                    rollbackOnException(status, ex);
+                } else {
+                    this.transactionManager.commit(status);
+                }
+                throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
+            }
+            this.transactionManager.commit(status);
+            return result;
+        }
+    }
+    
+    /**
+     * Perform a rollback, handling rollback exceptions properly.
+     * @param status object representing the transaction
+     * @param ex the thrown application exception or error
+     * @throws TransactionException in case of a rollback error
+     */
+    private void rollbackOnException(TransactionStatus status, Throwable ex) throws TransactionException {
+        logger.debug("Initiating transaction rollback on application exception", ex);
+        try {
+            this.transactionManager.rollback(status);
+        }
+        catch (TransactionSystemException ex2) {
+            logger.error("Application exception overridden by rollback exception", ex);
+            ex2.initApplicationException(ex);
+            throw ex2;
+        }
+        catch (RuntimeException ex2) {
+            logger.error("Application exception overridden by rollback exception", ex);
+            throw ex2;
+        }
+        catch (Error err) {
+            logger.error("Application exception overridden by rollback error", ex);
+            throw err;
+        }
+    }
+
+}

File src/test/clj/org/bituf/fountain/test_transaction.clj

+(ns org.bituf.fountain.test-transaction
+  (:import
+    (java.lang.reflect               UndeclaredThrowableException)
+    (java.sql                        Connection Statement)
+    (org.springframework.transaction TransactionDefinition)
+    (org.bituf.clj_dbspec            WriteNotAllowedException))
+  (:require
+    [clojure.java.io         :as io]
+    [clojure.pprint          :as pp]
+    [org.bituf.fountain.jdbc :as jd]
+    [org.bituf.fountain.transaction :as tx]
+    [org.bituf.clj-dbcp      :as cp]
+    [org.bituf.clj-miscutil  :as mu]
+    [org.bituf.clj-dbspec    :as sp]
+    [org.bituf.fountain.test-util :as tu])
+  (:use clojure.test))
+
+
+(def ds (cp/h2-memory-datasource))
+
+
+(def dbspec (-> (sp/make-dbspec ds)
+              jd/assoc-sjt
+              tx/assoc-txnt))
+
+
+(defn custom-dbspec
+  "Create custom DB-Spec using a transaction definition"
+  [txndef]
+  (-> (sp/make-dbspec ds)
+    jd/assoc-sjt
+    (tx/assoc-txnt #(tx/make-txntspec :txndef txndef))))
+
+
+(defmacro do-in-txn
+  [txntspec & body] {:pre [`(map? ~txntspec)]}
+  `(let [g# (tx/wrap-txncallback ~txntspec
+              (fn [] ~@body))]
+     (g#)))
+
+
+(defn commit-test
+  []
+  (tu/sample-setup)
+  (is (= 1 (tu/row-count :sample)))
+  (let [sji (jd/make-sji :sample)]
+    (do-in-txn dbspec
+      (jd/insert sji {:name "Hello" :age 20})
+      (jd/insert sji {:name "Hola"  :age 40})))
+  (is (= 3 (tu/row-count :sample))))
+
+
+(deftest test-commit
+  (testing "Basic transaction commit"
+    (sp/with-dbspec dbspec
+      (commit-test))))
+
+
+(defn rollback-test
+  [^Class ec ^Throwable ex]
+  (tu/sample-setup)
+  (is (= 1 (tu/row-count :sample)))
+  (tu/is-thrown? ec
+    (let [sji (jd/make-sji :sample)]
+      (do-in-txn dbspec
+        (jd/insert sji {:name "Rollback 1" :age 20})
+        (throw ex)
+        (jd/insert sji {:name "Rollback 2" :age 40}))))
+  (is (= 1 (tu/row-count :sample))))
+
+
+(deftest test-rollback
+  (testing "Basic transaction rollback with unchecked exception"
+    (sp/with-dbspec dbspec
+      (rollback-test NullPointerException (NullPointerException.))))
+  (testing "Basic transaction rollback with checked exception"
+    (sp/with-dbspec dbspec
+      (rollback-test UndeclaredThrowableException (java.io.IOException.))))
+  (testing "Basic transaction rollback with Error"
+    (sp/with-dbspec dbspec
+      (rollback-test AssertionError (AssertionError.)))))
+
+
+(defn txndef?
+  [td]
+  (instance? TransactionDefinition td))
+
+
+(deftest test-custom-txndef
+  (testing "Default transaction definition"
+    (is (txndef? (tx/make-txndef)) "Default txn definition"))
+  (testing "Custom transaction definition (positive tests)"
+    (is (txndef? (tx/make-txndef :isolation   :repeatable-read)) "With isolation level")
+    (is (txndef? (tx/make-txndef :propagation :requires-new))    "With propagation behavior")
+    (is (txndef? (tx/make-txndef :read-only   true))             "Read-only txn def")
+    (is (txndef? (tx/make-txndef :rollback-on (fn [ex] true)))   "With predicate based rollback")
+    (is (txndef? (tx/make-txndef :rollback-for    ["Exception" NullPointerException])) "With rule based rollback")
+    (is (txndef? (tx/make-txndef :no-rollback-for [Exception "NullPointerException"])) "With rule based rollback")
+    (is (txndef? (tx/make-txndef :timeout-sec 4))                "With timeout period")
+    (is (txndef? (tx/make-txndef :txn-name    "Some name"))      "With timeout period"))
+  (testing "Transaction definition (negative tests)"
+    (is (thrown? Exception (tx/make-txndef :bad-arg         :bad-value)) "Bad argument")
+    (is (thrown? Exception (tx/make-txndef :isolation       :bad-value)) "With isolation level")
+    (is (thrown? Exception (tx/make-txndef :propagation     :bad-value)) "With propagation behavior")
+    (is (thrown? Exception (tx/make-txndef :read-only       :bad-value)) "Read-only txn def")
+    (is (thrown? Exception (tx/make-txndef :rollback-on     :bad-value)) "With predicate based rollback")
+    (is (thrown? Exception (tx/make-txndef :rollback-for    :bad-value)) "With rule based rollback")
+    (is (thrown? Exception (tx/make-txndef :no-rollback-for :bad-value)) "With rule based rollback")
+    (is (thrown? Exception (tx/make-txndef :timeout-sec     :bad-value)) "With timeout period")
+    (is (thrown? Exception (tx/make-txndef :txn-name        :bad-value)) "With timeout period")))
+
+
+(defn regular
+  [spec sji]
+  (tu/sample-setup)
+  (is (= 1 (tu/sample-count)))
+  (do-in-txn spec
+    (jd/insert sji {:name "Rollback 1" :age 20})
+    (jd/insert sji {:name "Rollback 2" :age 40})))
+
+
+(defn throwex
+  [spec sji ex]
+  (tu/sample-setup)
+  (is (= 1 (tu/sample-count)))
+  (do-in-txn spec
+    (jd/insert sji {:name "Rollback 1" :age 20})
+    (throw ex)
+    (jd/insert sji {:name "Rollback 2" :age 40})))
+
+
+(deftest test-predicate-based-rollback
+  (testing "Predicate based rollback"
+    (let [ct (tx/make-txndef :rollback-on
+               (fn [^Throwable t]
+                 (not (instance? UnsupportedOperationException t))))
+          cd (custom-dbspec ct)]
+      (sp/with-dbspec cd
+        (is (not (.rollbackOn (.getTransactionAttribute (tx/get-txnt))
+                   (UnsupportedOperationException.))))
+        (let [sji (jd/make-sji :sample)]
+          (regular cd sji)
+          (is (= 3 (tu/sample-count)))
+          (is (thrown? UnsupportedOperationException
+                (throwex cd sji (UnsupportedOperationException.))))
+          (is (= 2 (tu/sample-count))))))))
+
+
+(deftest test-rule-based-rollback
+  (testing "Rule based rollback"
+    (let [ct (tx/make-txndef
+               :rollback-for    [NullPointerException]
+               :no-rollback-for [UnsupportedOperationException])
+          cd (custom-dbspec ct)]
+      (sp/with-dbspec cd
+        ;; no-rollback-for
+        (is (not (.rollbackOn (.getTransactionAttribute (tx/get-txnt))
+                   (UnsupportedOperationException.))))
+        (let [sji (jd/make-sji :sample)]
+          (regular cd sji)
+          (is (= 3 (tu/sample-count)))
+          (is (thrown? UnsupportedOperationException
+                (throwex cd sji (UnsupportedOperationException.))))
+          (is (= 2 (tu/sample-count))))
+        ;; rollback-for
+        (is (.rollbackOn (.getTransactionAttribute (tx/get-txnt))
+              (NullPointerException.)))
+        (let [sji (jd/make-sji :sample)]
+          (regular cd sji)
+          (is (= 3 (tu/sample-count)))
+          (is (thrown? NullPointerException
+                (throwex cd sji (NullPointerException.))))
+          (is (= 1 (tu/sample-count))))))))
+
+
+(deftest test-readonly-txn
+  (testing "Read-only transaction"
+    (sp/with-dbspec (custom-dbspec (tx/make-txndef :read-only true))
+      (is (thrown? WriteNotAllowedException
+            (commit-test))))))
+
+
+;; test-txn-timeout - not well supported by underlying transaction manager
+
+
+(defn test-ns-hook []
+  (test-commit)
+  (test-rollback)
+  (test-custom-txndef)
+  (test-predicate-based-rollback)
+  (test-rule-based-rollback)
+  (test-readonly-txn))