<project> <!-- ... --> <dependencies> <!-- ... --> <dependency> <groupId>org.jboss.narayana.jta</groupId> <artifactId>narayana-jta</artifactId> </dependency> <!-- ... --> </dependencies> <!-- ... --> </project>
Starting with 5.0.0.Final, ModeShape has its own persistence mechanism offering a number of built-in providers. All these providers have some common characteristics which we'll outline below:
ModeShape 5, just like ModeShape 3 and ModesShape 4 uses and requires transactions.
There are two different flavor of transactions that ModeShape can work with:
built-in transactions - these are completely transparent to the user and exert their effect each time a user performs a session.save() operation: all the changes within that session are persisted atomically as part of an internal transaction.
user transactions - these are managed by someone other than the ModeShape internal code. In this case, some other code starts and commits or rolls-back a transaction, making one or multiple session.save calls in between. See this section for detailed information about JTA transactions and how to use them with ModeShape
In either case, ModeShape has to integrate with a JTA transaction manager in order to be able to start (1) or be aware (2) of existing transactions. To do this, you have to tell ModeShape what TransactionManager to use
Ideally in a production system, you should add and use an established JTA implementation (see below) but you also have the option of not using any explicit transaction management, in which case ModeShape will default to an in-memory implementation which is only suitable for testing or prototyping.
The typical way of telling ModeShape to use one transaction manager or another is by simply adding the appropriate implementation to your runtime classpath. ModeShape uses a transaction manager lookup class, which attempts at runtime to find one of the following JTA implementations in order:
JNDI lookup one of:
java:jboss/TransactionManager
java:comp/TransactionManager
java:comp/UserTransaction
java:/TransactionManager
java:appserver/TransactionManager
java:pm/TransactionManager
javax.transaction.TransactionManager
osgi:service/javax.transaction.TransactionManager
standalone implementation of JBoss JTA
standalone implementation of Atomikos JTA
If any of the above are found, ModeShape will integrate with and use that JTA implementation. If none of the above are found, ModeShape will log a warning and default to a local, in-memory JTA implementation.
We're somewhat partial to the JBoss Transaction Manager. It's solid and used in the popular JBoss Application Server and Red Hat Middleware platforms. And it's what we use in our own testing and examples.
Using it is easy, especially if you're using our embedded BOM (as we describe in our Getting Started Guide), because all you have to do is add a dependency in your POM on the JBoss Transaction Manager:
<project> <!-- ... --> <dependencies> <!-- ... --> <dependency> <groupId>org.jboss.narayana.jta</groupId> <artifactId>narayana-jta</artifactId> </dependency> <!-- ... --> </dependencies> <!-- ... --> </project>
Note that you don't need to (but can) specify the version, since our BOM already defines the default version.
This is always used when running ModeShape inside JBoss AS via our JBoss AS kit
If you're using the ModeShape BOM (as we describe in our Getting Started Guide), simply add the following dependency to your project:
<project> <!-- ... --> <dependencies> <!-- ... --> <dependency> <groupId>com.atomikos</groupId> <artifactId>transactions-jta</artifactId> </dependency> <!-- ... --> </dependencies> <!-- ... --> </project>
If the default transaction manager lookup behavior is not suitable for your use case, you have the option of implementing your own org.modeshape.jcr.api.txn.TransactionManagerLookup and configuring ModeShape to use it like so:
{ "name" : "In Memory Repository", "storage" : { "transactionManagerLookup" : { "name" : "org.modeshape.jcr.txn.AtomikosTransactionManagerLookup", "classloader" : "(optional) any valid Java URL towards a resource which contains the lookup implementation" } } }
where the org.modeshape.jcr.api.txn.TransactionManagerLookup API is straightforward:
public interface TransactionManagerLookup { /** * Searches for a transaction manager instance. * * @return a {@link TransactionManager} instance; never {@code null} */ TransactionManager getTransactionManager(); }
All of ModeShape's persistence providers support multiple concurrent readers and writers, with effective READ_COMMITTED isolation. However, to ensure strict serializability, ModeShape uses global locking on each of the node keys which are modified as part of a transaction. This means that when multiple concurrent writers modify the same node, there will be lock contention proportionate to number of writer threads, which can significantly decrease performance.
For highly contented use cases, ModeShape offers the option of controlling the lock timeout interval before any writer thread will abort a transaction if it cannot obtain a lock on any of the modified nodes:
{ "name" : "Test Repository", "lockTimeoutMillis" : 100, }
or
<repository name="sample" lock-timeout-millis="10000"/>
The default value is 10000, meaning 10 seconds.
Some persistence providers may have additional specific settings for controlling the number of concurrent threads making changes to the persistent store.
Since ModeShape holds all global locks per transaction and thread, using JTA to change the owner thread of a transaction is not supported and will break the persistence code. This means that when using user-transactions you should never attempt to suspend and then resume a transaction off a different thread. See this issue for an example.
ModeShape 3 and 4 used, in additional to the main Infinispan cache which stored the repository data, a second, local, in-memory cache, for each repository workspace in order to provider fast read access to frequently used nodes. This cache exists solely for performance reasons and ModeShape 5 preserves the concept, using a LRU ConcurrentMap implementation.
The number of entries held in this cache can be configured like so:
{ "name" : "Test Repository", "workspaces" : { "predefined" : ["otherWorkspace"], "default" : "default", "allowCreation" : true, "cacheSize" : 100 } }
or
<repository name="sample"> <workspaces default-workspace="default" allow-workspace-creation="true" cache-size="100"/> </repository>
The default value is 10000 but when clustering you can test using a higher number, especially if your system is read-intensive as this may provide better locality.
Clustering in ModeShape 5 is much more conservative in order to ensure data integrity. As such, only persistence providers which are "naturally shareable" by multiple processes can be used when clustering. Currently the only provider supporting this is the database persistence provider.
See this section of our documentation for more information about clustering in ModeShape 5.
ModeShape 5 maintains the overall design of representing nodes as BSON documents, but instead of storing them in Infinispan it stores them in key-value transactional stores.
The SPI for this store is part of the modeshape-schematic module and has the following form:
public interface SchematicDb extends TransactionListener, Lifecycle { /** * Returns a unique identifier for this schematic DB. * <p> * Implementations should make sure that the provided id is unique per storage instance. * In other words, if two {@link SchematicDb} instances of the same type store data in two different places, they should * return a different id. * </p> * * @return a {@link String}, never {@code null} */ String id(); /** * Returns a set over all the keys present in the DB. * <p> * If this method is called within an existing transaction, it should take into account the transient transactional context * (i.e. any local but not yet committed changes) * </p> * * @return a {@link List} instance, never {@code null} */ List<String> keys(); /** * Get the document with the supplied key. This will represent the full {@link SchematicEntry} document if one exists. * <p> * If this method is called within an existing transaction, it should take into account the transient transactional context * (i.e. any local but not yet committed changes) * </p> * * @param key the key or identifier for the document * @return the document, or null if there was no document with the supplied key */ Document get( String key ); /** * Loads a set of documents from the DB returning the corresponding schematic entries. * * <p> * If this method is called within an existing transaction, it should <b>not take into account</b> the transient transactional * context (i.e. any local but not yet committed changes) and should always return the latest persisted information. * </p> * * @param keys an {@link Collection} of keys; never {@code null} * @return a {@link List} of {@link SchematicEntry entries}; never {@code null} */ List<SchematicEntry> load(Collection<String> keys); /** * Stores the supplied schematic entry under the given key. If an entry already exists with the same key, it should be * overwritten. * @param key a schematic entry id, never {@code null} * @param entry a {@link SchematicEntry} instance, never {@code null} */ @RequiresTransaction void put(String key, SchematicEntry entry); /** * Get an editor for the content of the given entry with the supplied key. * * @param key the key or identifier for the document * @param createIfMissing true if a new entry should be created and added to the database if an existing entry does not exist * @return the content document, or null if there was no document with the supplied key and a new one could not be created */ @RequiresTransaction EditableDocument editContent(String key, boolean createIfMissing); /** * Store the supplied content at the given key. * * <p> * Depending on the actual implementation, this may or may not be thread-safe. ModeShape never assumes this is thread-safe * when calling it. * </p> * * @param key the key or identifier for the content * @param content the content that is to be stored * @return the existing entry for the supplied key, or null if there was no entry and the put was successful */ @RequiresTransaction SchematicEntry putIfAbsent(String key, Document content); /** * Remove the existing document at the given key. * * @param key the key or identifier for the document * @return {@code true} if the removal was successful, {@code false} otherwise */ @RequiresTransaction boolean remove(String key); /** * Removes all the entries from this DB. */ @RequiresTransaction void removeAll(); /** * Store the supplied content document at the given key. If a document already exists with the given key, this should * overwrite the existing document. * * @param key the key or identifier for the document * @param content the document that is to be stored * @see #putIfAbsent(String, Document) */ @RequiresTransaction default void put( String key, Document content ) { put(key, SchematicEntry.create(key, content)); } /** * Get the entry with the supplied key. * <p> * If this method is called within an existing transaction, it should take into account the transient transactional context * (i.e. any local but not yet committed changes) * </p> * * @param key the key or identifier for the document * @return the entry, or null if there was no document with the supplied key */ default SchematicEntry getEntry( String key) { Document doc = get(key); return doc != null ? () -> doc : null; } /** * Determine whether the database contains an entry with the supplied key. * <p> * If this method is called within an existing transaction, it should take into account the transient transactional context * (i.e. any local but not yet committed changes) * </p> * * @param key the key or identifier for the document * @return true if the database contains an entry with this key, or false otherwise */ default boolean containsKey( String key ) { return get(key) != null; } /** * Store the supplied document. This document is expected to be a full entry document, which contains both a "metadata" * and "content" section. * * @param entryDocument the document that contains the metadata document and content document. * @see #putIfAbsent(String, Document) */ @RequiresTransaction default void putEntry(Document entryDocument) SchematicEntry entry = () -> entryDocument; put(entry.id(), entry); }
By default, ModeShape 5 has the following list of persistence providers: