Skip to content
This repository has been archived by the owner on Mar 12, 2020. It is now read-only.

Commit

Permalink
Split Bolt and Embedded implementation for Neo4J #19
Browse files Browse the repository at this point in the history
Neo4JConfig allows different implementations.
GuiStateMachineApi must be implemented for embedded databases/Bolt.
We cache the session factories which is required for embedded databases.
  • Loading branch information
tdauth committed Apr 17, 2019
1 parent 154079f commit 3d1068a
Show file tree
Hide file tree
Showing 12 changed files with 210 additions and 125 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package de.retest.guistatemachine.api
import java.nio.file.Paths

import de.retest.guistatemachine.api.impl.GuiStateMachineApiImpl
import de.retest.guistatemachine.api.neo4j.GuiStateMachineApiNeo4J
import de.retest.guistatemachine.api.neo4j.GuiStateMachineApiNeo4JEmbedded

/**
* This API allows the creation, modification and deletion of state machines ([[GuiStateMachine]]) which are created
Expand Down Expand Up @@ -44,7 +44,7 @@ object GuiStateMachineApi {
/**
* The default directory where all state machines are stored.
*/
val StorageDirectory = Paths.get(System.getProperty("user.home"), ".retest", "guistatemachines").toAbsolutePath.toString
val Neo4JEmbeddedStorageDirectory: Path = Paths.get(System.getProperty("user.home"), ".retest", "guistatemachines").toAbsolutePath.toString

val default = new GuiStateMachineApiImpl

Expand All @@ -53,5 +53,5 @@ object GuiStateMachineApi {
*/
def apply(): GuiStateMachineApi = default

val neo4j = new GuiStateMachineApiNeo4J(StorageDirectory)
val neo4jEmbedded = new GuiStateMachineApiNeo4JEmbedded(Paths.get(Neo4JEmbeddedStorageDirectory))
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ object Example extends App {
private val action0 = new NavigateToAction("http://google.com")
private val action1 = new NavigateToAction("http://wikipedia.org")

val stateMachine = GuiStateMachineApi.neo4j.getStateMachine("tmp") match {
val api = GuiStateMachineApi.neo4jEmbedded
val stateMachine = api.getStateMachine("tmp") match {
case Some(s) => s

case None => GuiStateMachineApi.neo4j.createStateMachine("tmp")
case None => api.createStateMachine("tmp")
}

println(s"All states before clearing: ${stateMachine.getAllStates.size}")
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package de.retest.guistatemachine.api.neo4j

import com.typesafe.scalalogging.Logger
import de.retest.guistatemachine.api.{GuiStateMachine, GuiStateMachineApi}

class GuiStateMachineApiNeo4JBolt(url: String, port: Int, user: String, password: String) extends GuiStateMachineApi {
private val logger = Logger[GuiStateMachineApiNeo4JBolt]

override def createStateMachine(name: String): GuiStateMachine = {
val conf = BoltConfig(url, port, user, password) // TODO #19 How to distinguish between databases.
new GuiStateMachineNeo4J(conf, Neo4JSessionFactory.getSessionFactory(conf))
}

override def removeStateMachine(name: String): Boolean = false // TODO #19 How to remove a Bolt database?

override def getStateMachine(name: String): Option[GuiStateMachine] = None // TODO #19 How to get a Bolt database? Some list?

override def clear(): Unit = {} // TODO #19 How to remove all Bolt databases?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package de.retest.guistatemachine.api.neo4j

import java.io.File
import java.nio.file.{Path, Paths}

import com.typesafe.scalalogging.Logger
import de.retest.guistatemachine.api.{GuiStateMachine, GuiStateMachineApi}
import org.apache.commons.io.FileUtils

/**
* This implementation is only thread-safe but cannot be used by multiple processes on the same storage directory.
* There should be only one single instance of this implementation per storage directory shared in the whole application.
* @param storageDirectory The directory where all subdirectories for embedded graph databases are created.
*/
class GuiStateMachineApiNeo4JEmbedded(storageDirectory: Path) extends GuiStateMachineApi {
private val logger = Logger[GuiStateMachineApiNeo4JEmbedded]

override def createStateMachine(name: String): GuiStateMachine = synchronized {
if (isDirectory(name)) {
throw new RuntimeException(s"State machine $name does already exist.")
} else {
val path = getPath(name)
logger.info("Created new graph DB in {}.", path)
val conf = EmbeddedConfig(path)
val sessionFactory = Neo4JSessionFactory.getSessionFactory(conf)
new GuiStateMachineNeo4J(conf, sessionFactory)
}
}

override def removeStateMachine(name: String): Boolean = synchronized {
if (isDirectory(name)) {
val file = getFile(name)
logger.info("Deleting state machine in {}.", file)
FileUtils.deleteDirectory(file)
true
} else {
false
}
}

override def getStateMachine(name: String): Option[GuiStateMachine] = synchronized {
if (isDirectory(name)) {
val path = getPath(name)
logger.info("Getting graph DB in {}.", path)
val conf = EmbeddedConfig(path)
val sessionFactory = Neo4JSessionFactory.getSessionFactory(conf)
Some(new GuiStateMachineNeo4J(conf, sessionFactory))
} else {
None
}
}

override def clear(): Unit = synchronized {
val storageDir = storageDirectory.toFile
if (storageDir.isDirectory) {
storageDir.listFiles().toSeq foreach { file =>
Neo4JSessionFactory.removeSessionFactory(EmbeddedConfig(file.toPath))
}
logger.info("Deleting all state machines in {}.", storageDirectory)
FileUtils.deleteDirectory(storageDir)
} else {
logger.info("Directory {} does not exist.", storageDirectory)
}
}

/**
* Gets the path of the embedded database with a certain name relative to the storage directory path.
* It needs to add an "embedded" directory since the parent directory will contain the lock and a logs directory.
* @param name The name of the embedded database.
* @return The path of the directory of the embedded database.
*/
private def getPath(name: String): Path = storageDirectory.resolve(Paths.get(name, "embedded"))
private def getFile(name: String): File = getPath(name).toFile
private def isDirectory(name: String): Boolean = getFile(name).isDirectory
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ package de.retest.guistatemachine.api.neo4j
import com.typesafe.scalalogging.Logger
import de.retest.guistatemachine.api.{GuiStateMachine, State, SutStateIdentifier}
import org.neo4j.ogm.cypher.{ComparisonOperator, Filter}
import org.neo4j.ogm.session.Session
import org.neo4j.ogm.session.{Session, SessionFactory}

import scala.collection.immutable.HashMap

class GuiStateMachineNeo4J(var uri: String) extends GuiStateMachine {
class GuiStateMachineNeo4J(var config: Neo4JConfig, var sessionFactory: SessionFactory) extends GuiStateMachine {
private val logger = Logger[GuiStateMachineNeo4J]

override def getState(sutStateIdentifier: SutStateIdentifier): State = {
val result = Neo4jSessionFactory.transaction { session =>
val result = Neo4JUtil.transaction { session =>
getNodeBySutStateIdentifier(session, sutStateIdentifier) match {
case None =>
// Create a new node for the SUT state in the graph database.
Expand All @@ -22,7 +22,7 @@ class GuiStateMachineNeo4J(var uri: String) extends GuiStateMachine {
// Do nothing if the node for the SUT state does already exist.
case Some(_) => false
}
}(uri)
}(sessionFactory)

if (result) {
logger.info(s"Created new state from SUT state identifier $sutStateIdentifier.")
Expand All @@ -32,7 +32,7 @@ class GuiStateMachineNeo4J(var uri: String) extends GuiStateMachine {
}

override def getAllStates: Map[SutStateIdentifier, State] =
Neo4jSessionFactory.transaction { session =>
Neo4JUtil.transaction { session =>
val allNodes = session.loadAll(classOf[SutStateEntity])
var result = HashMap[SutStateIdentifier, State]()
val iterator = allNodes.iterator()
Expand All @@ -43,17 +43,18 @@ class GuiStateMachineNeo4J(var uri: String) extends GuiStateMachine {
result = result + (sutState -> StateNeo4J(sutState, this))
}
result
}(uri)
}(sessionFactory)

override def clear(): Unit =
Neo4jSessionFactory.transaction { session =>
Neo4JUtil.transaction { session =>
session.purgeDatabase()
}(uri)
}(sessionFactory)

override def assignFrom(other: GuiStateMachine): Unit = {
// TODO #19 Should we delete the old graph database?
val otherStateMachine = other.asInstanceOf[GuiStateMachineNeo4J]
uri = otherStateMachine.uri
config = otherStateMachine.config
sessionFactory = otherStateMachine.sessionFactory
}

private[neo4j] def getNodeBySutStateIdentifier(session: Session, sutStateIdentifier: SutStateIdentifier): Option[SutStateEntity] = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package de.retest.guistatemachine.api.neo4j

import java.nio.file.Path

import org.neo4j.ogm.config.Configuration

sealed trait Neo4JConfig {
def buildConfig(): Configuration
}

case class EmbeddedConfig(path: Path) extends Neo4JConfig {
override def buildConfig(): Configuration = new Configuration.Builder().uri(path.toUri.toString).build
}

case class BoltConfig(host: String, port: Int, user: String, password: String) extends Neo4JConfig {
override def buildConfig(): Configuration =
new Configuration.Builder()
.uri(s"bolt://$host:$port")
.credentials(user, password)
.build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package de.retest.guistatemachine.api.neo4j

import com.typesafe.scalalogging.Logger
import org.neo4j.ogm.session.SessionFactory

import scala.collection.concurrent.TrieMap

sealed trait Neo4JSessionFactory

object Neo4JSessionFactory {
private val logger = Logger[Neo4JSessionFactory]

/**
* Session factories should always be shared in the application. Besides, we have to avoid exceptions like:
* ```
* org.neo4j.kernel.StoreLockException: Unable to obtain lock on store lock file: /tmp/GuiStateMachineApiNeo4jSpec8181209634775261316/store_lock.
* Please ensure no other process is using this database, and that the directory is writable (required even for read-only access)
* ```
* Apparently, Neo4J does not support multiple processes to access the same embedded database.
* TODO #19 Can we allow access by multiple processes on the same database.
* TODO #19 When do we close this?
*/
private val sessionFactories = TrieMap[Neo4JConfig, SessionFactory]()

def getSessionFactory(neo4JConfig: Neo4JConfig): SessionFactory = sessionFactories.get(neo4JConfig) match {
case Some(sessionFactory) =>
logger.info("Reusing session factory for {}", neo4JConfig)
sessionFactory
case None =>
logger.info("Creating new session factory for {}", neo4JConfig)
val conf = neo4JConfig.buildConfig()
val packageName = this.getClass.getPackage.getName
val sessionFactory = new SessionFactory(conf, packageName)
sessionFactories += (neo4JConfig -> sessionFactory)
sessionFactory

}

def removeSessionFactory(neo4JConfig: Neo4JConfig): Boolean = sessionFactories.remove(neo4JConfig) match {
case Some(sessionFactory) =>
logger.info("Closing session factory for {}", neo4JConfig)
sessionFactory.close()
true
case None => false
}
}
25 changes: 25 additions & 0 deletions src/main/scala/de/retest/guistatemachine/api/neo4j/Neo4JUtil.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package de.retest.guistatemachine.api.neo4j

import org.neo4j.ogm.session.{Session, SessionFactory}
import org.neo4j.ogm.transaction.Transaction

object Neo4JUtil {

def transaction[A](f: Session => A)(implicit sessionFactory: SessionFactory): A = {
// We have to create a session for every transaction since sessions are not thread-safe.
val session = sessionFactory.openSession()
var txn: Option[Transaction] = None
try {
val transaction = session.beginTransaction()
txn = Some(transaction)
val r = f(session)
transaction.commit()
r
} finally {
txn match {
case Some(transaction) => transaction.close()
case None =>
}
}
}
}

This file was deleted.

Loading

0 comments on commit 3d1068a

Please sign in to comment.