[CORDA-442] let Driver run without network map (#1890)

* [CORDA-442] let Driver run without network map

- Nodes started by driver run without a networkMapNode.

- Driver does not take a networkMapStartStrategy anymore

- a new parameter in the configuration "noNetworkMapServiceMode" allows for a node not to be a networkMapNode nor to connect to one.

- Driver now waits for each node to write its own NodeInfo file to disk and then copies it into each other node.

- When driver starts a node N, it waits for every node to be have N nodes in their network map.

Note: the code to copy around the NodeInfo files was already in DemoBench, the NodeInfoFilesCopier class was just moved from DemoBench into core (I'm very open to core not being the best place, please advise)
This commit is contained in:
Alberto Arri
2017-10-18 13:49:32 +01:00
committed by GitHub
parent b4c53647cd
commit b33b013284
18 changed files with 316 additions and 279 deletions

View File

@ -0,0 +1,165 @@
package net.corda.nodeapi
import net.corda.cordform.CordformNode
import net.corda.core.internal.ThreadBox
import net.corda.core.internal.createDirectories
import net.corda.core.internal.isRegularFile
import net.corda.core.internal.list
import net.corda.core.utilities.loggerFor
import rx.Observable
import rx.Scheduler
import rx.Subscription
import rx.schedulers.Schedulers
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption.COPY_ATTRIBUTES
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.attribute.FileTime
import java.util.concurrent.TimeUnit
/**
* Utility class which copies nodeInfo files across a set of running nodes.
*
* This class will create paths that it needs to poll and to where it needs to copy files in case those
* don't exist yet.
*/
class NodeInfoFilesCopier(scheduler: Scheduler = Schedulers.io()) : AutoCloseable {
companion object {
private val log = loggerFor<NodeInfoFilesCopier>()
const val NODE_INFO_FILE_NAME_PREFIX = "nodeInfo-"
}
private val nodeDataMapBox = ThreadBox(mutableMapOf<Path, NodeData>())
/**
* Whether the NodeInfoFilesCopier is closed. When the NodeInfoFilesCopier is closed it will stop polling the
* filesystem and all the public methods except [#close] will throw.
*/
private var closed = false
private val subscription: Subscription
init {
this.subscription = Observable.interval(5, TimeUnit.SECONDS, scheduler)
.subscribe { poll() }
}
/**
* @param nodeDir a path to be watched for NodeInfos
* Add a path of a node which is about to be started.
* Its nodeInfo file will be copied to other nodes' additional-node-infos directory, and conversely,
* other nodes' nodeInfo files will be copied to this node additional-node-infos directory.
*/
fun addConfig(nodeDir: Path) {
require(!closed) { "NodeInfoFilesCopier is already closed" }
nodeDataMapBox.locked {
val newNodeFile = NodeData(nodeDir)
put(nodeDir, newNodeFile)
for (previouslySeenFile in allPreviouslySeenFiles()) {
atomicCopy(previouslySeenFile, newNodeFile.additionalNodeInfoDirectory.resolve(previouslySeenFile.fileName))
}
log.info("Now watching: $nodeDir")
}
}
/**
* @param nodeConfig the configuration to be removed.
* Remove the configuration of a node which is about to be stopped or already stopped.
* No files written by that node will be copied to other nodes, nor files from other nodes will be copied to this
* one.
*/
fun removeConfig(nodeDir: Path) {
require(!closed) { "NodeInfoFilesCopier is already closed" }
nodeDataMapBox.locked {
remove(nodeDir) ?: return
log.info("Stopped watching: $nodeDir")
}
}
fun reset() {
require(!closed) { "NodeInfoFilesCopier is already closed" }
nodeDataMapBox.locked {
clear()
}
}
/**
* Stops polling the filesystem.
* This function can be called as many times as one wants.
*/
override fun close() {
if (!closed) {
closed = true
subscription.unsubscribe()
}
}
private fun allPreviouslySeenFiles() = nodeDataMapBox.alreadyLocked { values.flatMap { it.previouslySeenFiles.keys } }
private fun poll() {
nodeDataMapBox.locked {
for (nodeData in values) {
nodeData.nodeDir.list { paths ->
paths.filter { it.isRegularFile() }
.filter { it.fileName.toString().startsWith(NODE_INFO_FILE_NAME_PREFIX) }
.forEach { path -> processPath(nodeData, path) }
}
}
}
}
// Takes a path under nodeData config dir and decides whether the file represented by that path needs to
// be copied.
private fun processPath(nodeData: NodeData, path: Path) {
nodeDataMapBox.alreadyLocked {
val newTimestamp = Files.readAttributes(path, BasicFileAttributes::class.java).lastModifiedTime()
val previousTimestamp = nodeData.previouslySeenFiles.put(path, newTimestamp) ?: FileTime.fromMillis(-1)
if (newTimestamp > previousTimestamp) {
for (destination in this.values.filter { it.nodeDir != nodeData.nodeDir }.map { it.additionalNodeInfoDirectory }) {
val fullDestinationPath = destination.resolve(path.fileName)
atomicCopy(path, fullDestinationPath)
}
}
}
}
private fun atomicCopy(source: Path, destination: Path) {
val tempDestination = try {
Files.createTempFile(destination.parent, "", null)
} catch (exception: IOException) {
log.warn("Couldn't create a temporary file to copy $source", exception)
throw exception
}
try {
// First copy the file to a temporary file within the appropriate directory.
Files.copy(source, tempDestination, COPY_ATTRIBUTES, REPLACE_EXISTING)
} catch (exception: IOException) {
log.warn("Couldn't copy $source to $tempDestination.", exception)
Files.delete(tempDestination)
throw exception
}
try {
// Then rename it to the desired name. This way the file 'appears' on the filesystem as an atomic operation.
Files.move(tempDestination, destination, REPLACE_EXISTING)
} catch (exception: IOException) {
log.warn("Couldn't move $tempDestination to $destination.", exception)
Files.delete(tempDestination)
throw exception
}
}
/**
* Convenience holder for all the paths and files relative to a single node.
*/
private class NodeData(val nodeDir: Path) {
val additionalNodeInfoDirectory: Path = nodeDir.resolve(CordformNode.NODE_INFO_DIRECTORY)
// Map from Path to its lastModifiedTime.
val previouslySeenFiles = mutableMapOf<Path, FileTime>()
init {
additionalNodeInfoDirectory.createDirectories()
}
}
}

View File

@ -0,0 +1,139 @@
package net.corda.nodeapi
import net.corda.cordform.CordformNode
import net.corda.testing.eventually
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import rx.schedulers.TestScheduler
import java.nio.file.Files
import java.nio.file.Path
import java.time.Duration
import java.util.concurrent.TimeUnit
import kotlin.streams.toList
import kotlin.test.assertEquals
/**
* tests for [NodeInfoFilesCopier]
*/
class NodeInfoFilesCopierTest {
@Rule @JvmField var folder = TemporaryFolder()
private val rootPath get() = folder.root.toPath()
private val scheduler = TestScheduler()
companion object {
private const val ORGANIZATION = "Organization"
private const val NODE_1_PATH = "node1"
private const val NODE_2_PATH = "node2"
private val content = "blah".toByteArray(Charsets.UTF_8)
private val GOOD_NODE_INFO_NAME = "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}test"
private val GOOD_NODE_INFO_NAME_2 = "${NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX}anotherNode"
private val BAD_NODE_INFO_NAME = "something"
}
private fun nodeDir(nodeBaseDir : String) = rootPath.resolve(nodeBaseDir).resolve(ORGANIZATION.toLowerCase())
private val node1RootPath by lazy { nodeDir(NODE_1_PATH) }
private val node2RootPath by lazy { nodeDir(NODE_2_PATH) }
private val node1AdditionalNodeInfoPath by lazy { node1RootPath.resolve(CordformNode.NODE_INFO_DIRECTORY) }
private val node2AdditionalNodeInfoPath by lazy { node2RootPath.resolve(CordformNode.NODE_INFO_DIRECTORY) }
lateinit var nodeInfoFilesCopier: NodeInfoFilesCopier
@Before
fun setUp() {
nodeInfoFilesCopier = NodeInfoFilesCopier(scheduler)
}
@Test
fun `files created before a node is started are copied to that node`() {
// Configure the first node.
nodeInfoFilesCopier.addConfig(node1RootPath)
// Ensure directories are created.
advanceTime()
// Create 2 files, a nodeInfo and another file in node1 folder.
Files.write(node1RootPath.resolve(GOOD_NODE_INFO_NAME), content)
Files.write(node1RootPath.resolve(BAD_NODE_INFO_NAME), content)
// Configure the second node.
nodeInfoFilesCopier.addConfig(node2RootPath)
advanceTime()
eventually<AssertionError, Unit>(Duration.ofMinutes(1)) {
// Check only one file is copied.
checkDirectoryContainsSingleFile(node2AdditionalNodeInfoPath, GOOD_NODE_INFO_NAME)
}
}
@Test
fun `polling of running nodes`() {
// Configure 2 nodes.
nodeInfoFilesCopier.addConfig(node1RootPath)
nodeInfoFilesCopier.addConfig(node2RootPath)
advanceTime()
// Create 2 files, one of which to be copied, in a node root path.
Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME), content)
Files.write(node2RootPath.resolve(BAD_NODE_INFO_NAME), content)
advanceTime()
eventually<AssertionError, Unit>(Duration.ofMinutes(1)) {
// Check only one file is copied to the other node.
checkDirectoryContainsSingleFile(node1AdditionalNodeInfoPath, GOOD_NODE_INFO_NAME)
}
}
@Test
fun `remove nodes`() {
// Configure 2 nodes.
nodeInfoFilesCopier.addConfig(node1RootPath)
nodeInfoFilesCopier.addConfig(node2RootPath)
advanceTime()
// Create a file, in node 2 root path.
Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME), content)
advanceTime()
// Remove node 2
nodeInfoFilesCopier.removeConfig(node2RootPath)
// Create another file in node 2 directory.
Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME_2), content)
advanceTime()
eventually<AssertionError, Unit>(Duration.ofMinutes(1)) {
// Check only one file is copied to the other node.
checkDirectoryContainsSingleFile(node1AdditionalNodeInfoPath, GOOD_NODE_INFO_NAME)
}
}
@Test
fun `clear`() {
// Configure 2 nodes.
nodeInfoFilesCopier.addConfig(node1RootPath)
nodeInfoFilesCopier.addConfig(node2RootPath)
advanceTime()
nodeInfoFilesCopier.reset()
advanceTime()
Files.write(node2RootPath.resolve(GOOD_NODE_INFO_NAME_2), content)
// Give some time to the filesystem to report the change.
Thread.sleep(100)
assertEquals(0, Files.list(node1AdditionalNodeInfoPath).toList().size)
}
private fun advanceTime() {
scheduler.advanceTimeBy(1, TimeUnit.HOURS)
}
private fun checkDirectoryContainsSingleFile(path: Path, filename: String) {
assertEquals(1, Files.list(path).toList().size)
val onlyFileName = Files.list(path).toList().first().fileName.toString()
assertEquals(filename, onlyFileName)
}
}