Index: tracvote/__init__.py
===================================================================
--- tracvote/__init__.py	(revision 14133)
+++ tracvote/__init__.py	(working copy)
@@ -36,7 +36,9 @@
 from trac.web.chrome import add_notice, add_script, add_stylesheet
 from trac.wiki.api import IWikiChangeListener, IWikiMacroProvider, parse_args
 
+from tracvote.compat import get_read_db, with_transaction
 
+
 # Provide `resource_exists`, that has been backported to Trac 0.11.8 only.
 try:
     from trac.resource import resource_exists
@@ -98,10 +100,10 @@
     Because versioned resources with no version value default to 'latest',
     the current version has to be retrieved separately.
     """
+    db = get_read_db(env)
+    cursor = db.cursor()
     realm = resource.realm
     if realm == 'ticket':
-        db = env.get_db_cnx()
-        cursor = db.cursor()
         cursor.execute("""
             SELECT SUM(c.change) FROM (
                 SELECT 1 as change
@@ -112,8 +114,6 @@
         tkt_changes = cursor.fetchone()
         resource.version = tkt_changes and tkt_changes[0] or 0
     elif realm == 'wiki':
-        db = env.get_db_cnx()
-        cursor = db.cursor()
         cursor.execute("""
             SELECT version
               FROM wiki
@@ -124,6 +124,7 @@
         resource.version = page_version and int(page_version[0]) or 0
     return resource
 
+
 def resource_from_path(env, path):
     """Find realm and resource ID from resource URL.
 
@@ -200,7 +201,7 @@
             args.append(realm)
         if top:
             args.append(top)
-        db = self.env.get_db_cnx()
+        db = get_read_db(self.env)
         cursor = db.cursor()
         cursor.execute("""
             SELECT realm,resource_id,SUM(vote) AS count
@@ -209,15 +210,14 @@
              ORDER by count DESC,resource_id%s
         """ % (realm and ' WHERE realm=%s' or '', top and ' LIMIT %s' or ''),
             args)
-        rows = cursor.fetchall()
-        for row in rows:
+        for row in cursor:
             yield row
 
     def get_vote_counts(self, resource):
         """Get negative, total and positive vote counts and return them in a
         tuple.
         """
-        db = self.env.get_db_cnx()
+        db = get_read_db(self.env)
         cursor = db.cursor()
         cursor.execute("""
             SELECT sum(vote)
@@ -246,7 +246,7 @@
 
     def get_vote(self, req, resource):
         """Return the current users vote for a resource."""
-        db = self.env.get_db_cnx()
+        db = get_read_db(self.env)
         cursor = db.cursor()
         cursor.execute("""
             SELECT vote
@@ -265,57 +265,59 @@
                      resource.realm, to_unicode(resource.id)))
         if not resource.version:
             args.pop(1)
-        db = self.env.get_db_cnx()
-        cursor = db.cursor()
-        cursor.execute("""
-            UPDATE votes
-               SET changetime=%%s%s,vote=%%s
-             WHERE username=%%s
-               AND realm=%%s
-               AND resource_id=%%s
-        """ % (resource.version and ',version=%s' or ''), args)
-        if self.get_vote(req, resource) is None:
+        @with_transaction(self.env)
+        def fn(db):
+            cursor = db.cursor()
             cursor.execute("""
-                INSERT INTO votes
-                    (realm,resource_id,version,username,vote,time,changetime)
-                VALUES (%s,%s,%s,%s,%s,%s,%s)
-            """, (resource.realm, to_unicode(resource.id), resource.version,
-                  get_reporter_id(req), vote, now_ts, now_ts))
-        db.commit()
+                UPDATE votes
+                   SET changetime=%%s%s,vote=%%s
+                 WHERE username=%%s
+                   AND realm=%%s
+                   AND resource_id=%%s
+            """ % (resource.version and ',version=%s' or ''), args)
+            if self.get_vote(req, resource) is None:
+                cursor.execute("""
+                    INSERT INTO votes
+                        (realm,resource_id,version,username,vote,time,
+                         changetime)
+                    VALUES (%s,%s,%s,%s,%s,%s,%s)
+                """, (resource.realm, to_unicode(resource.id),
+                      resource.version, get_reporter_id(req), vote, now_ts,
+                      now_ts))
 
     def reparent_votes(self, resource, old_id):
         """Update resource reference of votes on a renamed resource."""
-        db = self.env.get_db_cnx()
-        cursor = db.cursor()
-        cursor.execute("""
-            UPDATE votes
-               SET resource_id=%s
-             WHERE realm=%s
-               AND resource_id=%s
-        """, (to_unicode(resource.id), resource.realm, to_unicode(old_id)))
-        db.commit()
+        @with_transaction(self.env)
+        def fn(db):
+            cursor = db.cursor()
+            cursor.execute("""
+                UPDATE votes
+                   SET resource_id=%s
+                 WHERE realm=%s
+                   AND resource_id=%s
+            """, (to_unicode(resource.id), resource.realm, to_unicode(old_id)))
 
     def delete_votes(self, resource):
         """Delete votes for a resource."""
         args = list((resource.realm, to_unicode(resource.id)))
         if resource.version:
             args.append(resource.version)
-        db = self.env.get_db_cnx()
-        cursor = db.cursor()
-        cursor.execute("""
-            DELETE
-              FROM votes
-             WHERE realm=%%s
-               AND resource_id=%%s%s
-        """ % (resource.version and ' AND version=%s' or ''), args)
-        db.commit()
+        @with_transaction(self.env)
+        def fn(db):
+            cursor = db.cursor()
+            cursor.execute("""
+                DELETE
+                  FROM votes
+                 WHERE realm=%%s
+                   AND resource_id=%%s%s
+            """ % (resource.version and ' AND version=%s' or ''), args)
 
     def get_votes(self, req, resource=None, top=0):
         """Return most recent votes, optionally only for one resource."""
         args = resource and [resource.realm, to_unicode(resource.id)] or []
         if top:
             args.append(top)
-        db = self.env.get_db_cnx()
+        db = get_read_db(self.env)
         cursor = db.cursor()
         cursor.execute("""
             SELECT realm,resource_id,vote,username,changetime
@@ -324,13 +326,12 @@
              ORDER BY changetime DESC%s
         """ % (resource and ' AND realm=%s AND resource_id=%s' or '',
                top and ' LIMIT %s' or ''), args)
-        rows = cursor.fetchall()
-        for row in rows:
+        for row in cursor:
             yield row
 
     def get_total_vote_count(self, realm):
         """Return the total vote count for a realm, like 'ticket'."""
-        db = self.env.get_db_cnx()
+        db = get_read_db(self.env)
         cursor = db.cursor()
         cursor.execute('SELECT sum(vote) FROM votes WHERE resource LIKE %s',
                        (realm + '%',))
@@ -353,7 +354,7 @@
 
     def get_realm_votes(self, realm):
         """Return a dictionary of vote count for a realm."""
-        db = self.env.get_db_cnx()
+        db = get_read_db(self.env)
         cursor = db.cursor()
         cursor.execute('SELECT resource FROM votes WHERE resource LIKE %s',
                        (realm + '%',))
@@ -596,7 +597,7 @@
         elif schema_ver < self.schema_version:
             # Perform incremental upgrades.
             for i in range(schema_ver + 1, self.schema_version + 1):
-                name  = 'db%i' % i
+                name = 'db%i' % i
                 try:
                     upgrades = __import__('tracvote.upgrades', globals(),
                                           locals(), [name])
@@ -622,7 +623,8 @@
 
     def get_schema_version(self, db=None):
         """Return the current schema version for this plugin."""
-        db = db and db or self.env.get_db_cnx()
+        if not db:
+            db = get_read_db(self.env)
         cursor = db.cursor()
         cursor.execute("""
             SELECT value
@@ -677,7 +679,7 @@
         resource.
         """
         negative, total, positive = resource and \
-                                    self.get_vote_counts(resource) or (0,0,0)
+                                    self.get_vote_counts(resource) or (0, 0, 0)
         count_detail = ['%+i' % i for i in (positive, negative) if i]
         if count_detail:
             count_detail = ' (%s)' % ', '.join(count_detail)
Index: tracvote/compat.py
===================================================================
--- tracvote/compat.py	(revision 0)
+++ tracvote/compat.py	(working copy)
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 Jun Omae <jun66j5@gmail.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution.
+
+try:
+    from trac.db.api import with_transaction
+    def get_read_db(env):
+        return env.get_read_db()
+except ImportError:
+    try:
+        import threading
+    except ImportError:
+        import dummy_threading as threading
+        threading._get_ident = lambda: 0
+
+    from trac.core import Component
+    from trac.db.api import DatabaseManager
+
+    class ThreadLocal(threading.local):
+        def __init__(self, **kwargs):
+            threading.local.__init__(self)
+            self.__dict__.update(kwargs)
+
+    class TransactionManager(Component):
+        def __init__(self):
+            self._transaction_local = ThreadLocal(db=None)
+
+    def with_transaction(env, db=None):
+        _transaction_local = TransactionManager(env)._transaction_local
+        def transaction_wrapper(fn):
+            ldb = _transaction_local.db
+            if db is not None:
+                if ldb is None:
+                    _transaction_local.db = db
+                    try:
+                        fn(db)
+                    finally:
+                        _transaction_local.db = None
+                else:
+                    assert ldb is db, "Invalid transaction nesting"
+                    fn(db)
+            elif ldb:
+                fn(ldb)
+            else:
+                ldb = _transaction_local.db = env.get_db_cnx()
+                try:
+                    fn(ldb)
+                    ldb.commit()
+                    _transaction_local.db = None
+                except:
+                    _transaction_local.db = None
+                    ldb.rollback()
+                    ldb = None
+                    raise
+        return transaction_wrapper
+
+    def get_read_db(env):
+        return TransactionManager(env)._transaction_local.db or \
+               DatabaseManager(env).get_connection()
Index: tracvote/tests/main.py
===================================================================
--- tracvote/tests/main.py	(revision 14133)
+++ tracvote/tests/main.py	(working copy)
@@ -8,6 +8,7 @@
 #
 # Author: Steffen Hoffmann
 
+import inspect
 import shutil
 import tempfile
 import unittest
@@ -21,40 +22,51 @@
 from trac.wiki.model import WikiPage
 
 from tracvote import VoteSystem, resource_check
+from tracvote.compat import get_read_db, with_transaction
 
 
 _ACTIONS = dict(view='VOTE_VIEW', modify='VOTE_MODIFY')
 
 
+if 'db' in inspect.getargspec(Ticket.__init__)[0]:
+    def new_ticket(*args, **kwargs):
+        return Ticket(*args, **kwargs)
+else:
+    def new_ticket(*args, **kwargs):
+        if 'db' in kwargs:
+            del kwargs['db']
+        return Ticket(*args, **kwargs)
+
+
 class EnvironmentSetupTestCase(unittest.TestCase):
 
     def setUp(self):
         self.env = EnvironmentStub(enable=['trac.*'])
         self.env.path = tempfile.mkdtemp()
         self.db_mgr = DatabaseManager(self.env)
-        self.db = self.env.get_db_cnx()
         self.votes = VoteSystem(self.env)
 
     def tearDown(self):
-        self.db.close()
-        # Really close db connections.
         self.env.shutdown()
         shutil.rmtree(self.env.path)
 
     # Helpers
 
     def _schema_init(self, schema=None):
-        cursor = self.db.cursor()
-        cursor.execute("DROP TABLE IF EXISTS votes")
-        if schema:
-            connector = self.db_mgr._get_connector()[0]
-            for table in schema:
-                for stmt in connector.to_sql(table):
-                    cursor.execute(stmt)
+        @with_transaction(self.env)
+        def fn(db):
+            cursor = db.cursor()
+            cursor.execute("DROP TABLE IF EXISTS votes")
+            if schema:
+                connector = self.db_mgr._get_connector()[0]
+                for table in schema:
+                    for stmt in connector.to_sql(table):
+                        cursor.execute(stmt)
 
     def _verify_curr_schema(self):
-        self.assertFalse(self.votes.environment_needs_upgrade(self.db))
-        cursor = self.db.cursor()
+        db = get_read_db(self.env)
+        self.assertFalse(self.votes.environment_needs_upgrade(db))
+        cursor = db.cursor()
         cursor.execute('SELECT * FROM votes')
         cols = get_column_names(cursor)
         self.assertTrue('resource' not in cols)
@@ -69,7 +81,8 @@
         self.assertEquals(self.votes.schema_version, schema_ver)
 
     def _verify_schema_unregistered(self):
-        cursor = self.db.cursor()
+        db = get_read_db(self.env)
+        cursor = db.cursor()
         cursor.execute("""
             SELECT value
               FROM system
@@ -84,12 +97,13 @@
         #   Revert these changes for clean install testing.
         self._schema_init()
 
-        self.assertEquals(0, self.votes.get_schema_version(self.db))
-        self.assertTrue(self.votes.environment_needs_upgrade(self.db))
+        @with_transaction(self.env)
+        def fn(db):
+            self.assertEquals(0, self.votes.get_schema_version(db))
+            self.assertTrue(self.votes.environment_needs_upgrade(db))
+            self.votes.upgrade_environment(db)
+            self._verify_curr_schema()
 
-        self.votes.upgrade_environment(self.db)
-        self._verify_curr_schema()
-
     def test_upgrade_v1_to_current(self):
         # The initial db schema from r2963 - 02-Jan-2008 by Alec Thomas.
         schema = [
@@ -101,38 +115,43 @@
             ]
         self._schema_init(schema)
 
-        # Populate tables with test data.
-        cursor = self.db.cursor()
-        cursor.executemany("""
-            INSERT INTO votes
-                   (resource,username,vote)
-            VALUES (%s,%s,%s)
-        """, (('ticket/1','user',-1), ('ticket/2','user',1),
-              ('wiki/DeletedPage','user',-1), ('wiki/ExistingPage','user',1)))
-        # Resources must exist for successful data migration.
-        t = Ticket(self.env, db=self.db)
-        t['summary'] = 'test ticket'
-        t.insert()
-        w = WikiPage(self.env, 'ExistingPage')
-        w.text = 'content'
-        w.save('author', 'comment', '::1')
-        self._verify_schema_unregistered()
-        self.assertEquals(1, self.votes.get_schema_version(self.db))
-        self.assertTrue(self.votes.environment_needs_upgrade(self.db))
+        @with_transaction(self.env)
+        def fn(db):
+            # Populate tables with test data.
+            cursor = db.cursor()
+            cursor.executemany("""
+                INSERT INTO votes
+                       (resource,username,vote)
+                VALUES (%s,%s,%s)
+            """, (('ticket/1', 'user', -1), ('ticket/2', 'user', 1),
+                  ('wiki/DeletedPage', 'user', -1),
+                  ('wiki/ExistingPage', 'user', 1)))
+            # Resources must exist for successful data migration.
+            t = new_ticket(self.env, db=db)
+            t['summary'] = 'test ticket'
+            t.insert()
+            w = WikiPage(self.env, 'ExistingPage')
+            w.text = 'content'
+            w.save('author', 'comment', '::1')
+            self._verify_schema_unregistered()
+            self.assertEquals(1, self.votes.get_schema_version(db))
+            self.assertTrue(self.votes.environment_needs_upgrade(db))
 
-        # Data migration and registration of unversioned schema.
-        self.votes.upgrade_environment(self.db)
-        self._verify_curr_schema()
+            # Data migration and registration of unversioned schema.
+            self.votes.upgrade_environment(db)
+            self._verify_curr_schema()
 
-        cursor.execute('SELECT * FROM votes')
-        votes = cursor.fetchall()
-        t_votes = [id for realm,id,ver,u,v,t,c in votes if realm == 'ticket']
-        w_votes = [id for realm,id,ver,u,v,t,c in votes if realm == 'wiki']
-        self.assertTrue('1' in t_votes)
-        if resource_check:
-            self.assertFalse('2' in t_votes)
-            self.assertFalse('DeletedPage' in w_votes)
-        self.assertTrue('ExistingPage' in w_votes)
+            cursor.execute('SELECT * FROM votes')
+            votes = cursor.fetchall()
+            t_votes = [id for realm, id, ver, u, v, t, c in votes
+                          if realm == 'ticket']
+            w_votes = [id for realm, id, ver, u, v, t, c in votes
+                          if realm == 'wiki']
+            self.assertTrue('1' in t_votes)
+            if resource_check:
+                self.assertFalse('2' in t_votes)
+                self.assertFalse('DeletedPage' in w_votes)
+            self.assertTrue('ExistingPage' in w_votes)
 
 
 class VoteSystemTestCase(unittest.TestCase):
@@ -140,19 +159,22 @@
     def setUp(self):
         self.env = EnvironmentStub(default_data=True,
                                    enable=['trac.*', 'tracvote.*'])
+        self.env.config.set('trac', 'permission_policies',
+                            'DefaultPermissionPolicy, LegacyAttachmentPolicy')
         self.env.path = tempfile.mkdtemp()
         self.perm = PermissionSystem(self.env)
         self.req = Mock()
 
-        self.db = self.env.get_db_cnx()
         self.votes = VoteSystem(self.env)
         # Current tracvotes schema is setup with enabled component anyway.
         #   Revert these changes for getting default permissions inserted.
         self._revert_schema_init()
-        self.votes.upgrade_environment(self.db)
 
+        @with_transaction(self.env)
+        def fn(db):
+            self.votes.upgrade_environment(db)
+
     def tearDown(self):
-        self.db.close()
         # Really close db connections.
         self.env.shutdown()
         shutil.rmtree(self.env.path)
@@ -160,11 +182,13 @@
     # Helpers
 
     def _revert_schema_init(self):
-        cursor = self.db.cursor()
-        cursor.execute("DROP TABLE IF EXISTS votes")
-        cursor.execute("DELETE FROM system WHERE name='vote_version'")
-        cursor.execute("DELETE FROM permission WHERE action %s"
-                       % self.db.like(), ('VOTE_%',))
+        @with_transaction(self.env)
+        def fn(db):
+            cursor = db.cursor()
+            cursor.execute("DROP TABLE IF EXISTS votes")
+            cursor.execute("DELETE FROM system WHERE name='vote_version'")
+            cursor.execute("DELETE FROM permission WHERE action %s"
+                           % db.like(), ('VOTE_%',))
 
     # Tests
 
