Are SVNKit methods reenterable?

People often ask me which SVNKit objects can and which can’t be reused from different threads or while another operation running on those objects.

SVNRepository methods are not reenterable

This means that the same SVNRepository instance can’t be used from the several threads at the same time. But also this means that the same SVNRepository object can’t be used within the same thread but from some callback provided to another function.

For example, this code (rather useless) checking that paths returned by SVNRepository#log method really exist:

final SVNRepository svnRepository = SVNRepositoryFactory.create(url);
try {
    log(new String[]{""}, 1, 2, true, true, new ISVNLogEntryHandler() {
        @Override
        public void handleLogEntry(SVNLogEntry logEntry) throws SVNException {
            final long revision = logEntry.getRevision();
            final Map<String,SVNLogEntryPath> changedPaths = logEntry.getChangedPaths();
            for (Map.Entry<String, SVNLogEntryPath> entry : changedPaths.entrySet()) {
                final String path = entry.getKey();

                //WRONG!!! svnRepository object can't be reused!
                final SVNNodeKind kind = svnRepository.checkPath(path, revision);
                System.out.println(kind);
            }
        }
    });
} finally {
    svnRepository.closeSession();
}

fails with

java.lang.Error: SVNRepository methods are not reenterable
	at org.tmatesoft.svn.core.io.SVNRepository.lock(SVNRepository.java:2820)
	at org.tmatesoft.svn.core.io.SVNRepository.lock(SVNRepository.java:2811)
	at org.tmatesoft.svn.core.internal.io.fs.FSRepository.openRepositoryRoot(FSRepository.java:767)
	at org.tmatesoft.svn.core.internal.io.fs.FSRepository.openRepository(FSRepository.java:758)
	at org.tmatesoft.svn.core.internal.io.fs.FSRepository.checkPath(FSRepository.java:205)
	at org.tmatesoft.svn.test.InfoTest$1.handleLogEntry(InfoTest.java:150)
	at org.tmatesoft.svn.core.internal.io.fs.FSLog.sendLog(FSLog.java:332)
	at org.tmatesoft.svn.core.internal.io.fs.FSLog.runLog(FSLog.java:162)
	at org.tmatesoft.svn.core.internal.io.fs.FSRepository.logImpl(FSRepository.java:381)
	at org.tmatesoft.svn.core.io.SVNRepository.log(SVNRepository.java:1035)
	at org.tmatesoft.svn.core.io.SVNRepository.log(SVNRepository.java:940)
	at org.tmatesoft.svn.core.io.SVNRepository.log(SVNRepository.java:864)

The same is true about reusing SVNRepository object while commiting to repository.

SVNRepository#getCommitEditor starts a transaction. This transaction can be terminated in three ways:

  • By ISVNEditor#closeEdit call on the editor. In this case the transaction is committed (or rejected).
  • By ISVNEditor#abortEdit that terminates the transaction.
  • By any exception thrown by ISVNEditor methods.

In all other cases the transaction remains unfinished. While the transaction is not finished, a corresponding SVNRepository object can’t be reused. An example:

final SVNRepository svnRepository = SVNRepositoryFactory.create(url);
try {
    final ISVNEditor commitEditor = svnRepository.getCommitEditor("Commit message", null);
    commitEditor.openRoot(-1);

    //WRONG!!! svnRepository can't be reused until commitEditor.closeEdit(); is called
    svnRepository.checkPath("", -1);

    commitEditor.closeDir();
    commitEditor.closeEdit();
} finally {
    svnRepository.closeSession();
}

This code also fails with a similar stacktrace. One of the most common mistakes is not to cancel commit transaction if any custom code throws an exception:

final SVNRepository svnRepository = SVNRepositoryFactory.create(url);
try {
    try {
        final ISVNEditor commitEditor = svnRepository.getCommitEditor("Commit message", null);
        commitEditor.openRoot(-1);

        //some code that can throw an exception
        if (2 + 2 == 4) {
            throw new SomeException();
        }

        commitEditor.closeDir();
        commitEditor.closeEdit();
    } catch (SomeException e) {
        e.printStackTrace();

        //the commit transaction should be closed here by commitEditor.abortEdit() call
    }
    //this call will fail because of unclosed transaction
    svnRepository.checkPath("", -1);
} finally {
    svnRepository.closeSession();
}

Still incorrect because the catch block should contain commitEditor.abortEdit() call that would stop the commit transaction.

DefaultSVNRepositoryPool connections can’t be reused simultaneously

SVNKit uses ISVNRepositoryPool interface to keep and reuse connections between Subversion requests. This approach significantly improves SVNKit performance but the connections pool should be used carefully.

DefaultSVNRepositoryPool is an implementation of ISVNRepository pool provided by SVNKit. It keeps “repository root” -> SVNRepository instance map and returns an existing or creates a new connection on ISVNRepositoryPool#createRepository invocation.

Note that ISVNRepositoryPool does not know if any of the connection it keeps has any operation in progress and returns the connection if URL requested matches corresponding repository root of the saved connection. And from the previous section you know that SVNRepository instances can’t be reused.

For example:

final ISVNRepositoryPool repositoryPool = new DefaultSVNRepositoryPool(null, null);
try {
    final SVNRepository svnRepository1 = repositoryPool.createRepository(url, true);
    final SVNRepository svnRepository2 = repositoryPool.createRepository(url, true);
    final ISVNEditor commitEditor = svnRepository1.getCommitEditor("Commit message", null);
    commitEditor.openRoot(-1);

    //WRONG!!! svnRepository2 is the same object as svnRepository1!
    svnRepository2.checkPath("", -1);

    commitEditor.closeDir();
    commitEditor.closeEdit();
} finally {
    repositoryPool.dispose();
}

This code fails because repositoryPool.createRepository(url, true); returns the same instance for the 2nd and all subsequent calls. Instead one should create the second connection with mayReuse=false and of course close it by hand afterwards because it won’t be closed on ISVNRepositoryPool#dispose:

final ISVNRepositoryPool repositoryPool = new DefaultSVNRepositoryPool(null, null);
SVNRepository svnRepository2 = null;
try {
    final SVNRepository svnRepository1 = repositoryPool.createRepository(url, true);
    svnRepository2 = repositoryPool.createRepository(url, false);
    final ISVNEditor commitEditor = svnRepository1.getCommitEditor("Commit message", null);
    commitEditor.openRoot(-1);

    //Correct, svnRepository2 is another connection
    svnRepository2.checkPath("", -1);

    commitEditor.closeDir();
    commitEditor.closeEdit();
} finally {
    repositoryPool.dispose();
    if (svnRepository2 != null) {
        //it should be closed by hand because it was created with mayReuse=false
        svnRepository2.closeSession();
    }
}

This code is correct though is not symmetric. You can often meet it inside SVNKit itself for operations where 2 connections are used at the same time.

SVNClientManager and SVNXXXClient can’t be reused

This is also true because of several reasons. First, SVNClientManager implements and aggregates ISVNRepositoryPool which, as you know now, can’t be reused. But also because of the way SVNKit works it can’t be reused for working copy of 1.7 format operations (otherwise there can be an error “svn: E200030: There are unfinished transactions detected in …”).

The reason is that SVNBasicClient encapsulates SvnOperationFactory, that encapsulates SVNWCContext, that encapsulates SVNWCDb, that contains

private Map<String, SVNWCDbDir> dirData;

This is a cache path->working_copy_root_data where “working_copy_root_data” is a structure that contains a working copy root path and a database object (SVNSqlJetDb), and this database object contains “openCount” — transaction in progress counter that is increased when a transaction starts and is decreased when it ends (in thread-unsafe manner). If the operation is finished, but openCount > 0 (for example, because the database is used from another thread, you see

svn: E200030: There are unfinished transactions detected in ...

exception). So SVNSqlJetDb objects can’t be reused among threads. And the same is true about callbacks.

Instead of reusing SVNClientManager or SVNXXXClient instance one should create a separate instance per thread. For callbacks case — at least 2: one for the main operation and another one for operations inside a callback. But note: these operations cannot modify the same working copy because

Several working copy modification operations cannot run simultaneously

It is more Subversion’s restriction that SVNKit’s. Until WC 1.7 format any Subversion working copy directory could be processed independently allowing parallel executing of modification operaions if they run on different directories.

Now every working copy modification operations locks the whole working copy until completion and no other write operation can be run at the same time.

But read-only operations do not lock anything and can be run anytime. Subversion working copy 1.7 is based on transactions moving the working copy from valid state to another valid state. So read-only operations while another write operation will find find the working copy in some intermediate but valid state.

Comments are closed.