diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c25cafc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +language: java +sudo: false +dist: trusty +jdk: oraclejdk8 +install: true +script: + - ./gradlew -version + - ./gradlew build +notifications: + email: true +before_cache: + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ +cache: + directories: + - "$HOME/.gradle/caches/" + - "$HOME/.gradle/wrapper/" +deploy: + provider: releases + api_key: + secure: W9XJLLhuWj7f3/xcV8TYWVTrtoYi8iMQTcatwNR3I9nKmK05ywab/G+iD4anZJWyZrOtvuS0ztBrPQMiGGdEju1iMRkuFTlC0XJOAJNL60gGgeZ7ngpBV/RWEEqGcUw+CNXvVIkbox9uXK5qokw8PMB7osnjmRpyfmLIL2hbiYVzXN3S3iKGn8nO7evWIVYjhEcHcZSCMlcLviBWJgCVzBpofubTy6zOVIggRG17tJjfstDsoyjmCWoZA18ZlGftcvM1a60H5retY9k/qYYVT5EPTrDmLLAHLP7Vy2ScgfzCWcO9V9Q8/BgfEwlpQ4j7ngHWW8zIk3QCqPZ9iDiXTkok8qhMQTRKebIOXJGnaI9ZoT/WehL6mGhHW23Cvpp8ubDLSyFuv9ylfUk9drdshEP/b02dV2o3S06PYhjXFinoOBRXMkIHwX0QFt8W85OyOCjARpBR5qDyxH5gCxSpfFaoUG0/CdWWoKdXDlwaG3KNzyU2jsjGsI3Y73q2AE+9qc9tgHHheK2TIjJPRMkjy0bxRHmEnSMM3JsP+/vNJNEDnZFgMxpHcixMikVbxpBzcSFSnjQNZs1bETMaHmIsPX34w8ODwwd5XQCrZb0zJiqe8A7tlcjcY9izTJ7+2O2hl9Lq8DJCQNb0awWXbX+KYjHH9YG2eF5sXxPrLLWgX+k= + file_glob: true + file: + - build/distributions/* + skip_cleanup: true + overwrite: true + on: + tags: true +env: + global: + - GRADLE_OPTS="-Xms256m" + - JDK_JAVA_OPTIONS='--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/sun.net.dns=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED' \ No newline at end of file diff --git a/README.md b/README.md index 2947544..014f12c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,39 @@ # ethereum-ingest Ingests events from the Ethereum blockchain into ElasticSearch, MongoDB, Hazelcast, CQEngine and SQLite. + +In development; tested with ElasticSeach 5.6.2 and geth 1.7.1. + +Build with +``` +gradle jar +``` +Requires chili-core through jitpack or local repo. + +Start geth with rpc enabled +``` +geth --rpcapi personal,db,eth,net,web3 --rpc --testnet +``` + +Run with +``` +java -jar .jar +``` + +Set configuration in application.json. + +Default configuration +- storage: elasticsearch +- os: windows +- ipc: \\.\pipe\geth.ipc + + +Storage can be any of the following +- MONGODB +- ELASTICSEARCH +- HAZELCAST +- SQLITE +- MEMORY + +os can be any of the following +- UNIX +- WINDOWS \ No newline at end of file diff --git a/application.json b/application.json new file mode 100644 index 0000000..361fcbf --- /dev/null +++ b/application.json @@ -0,0 +1,6 @@ +{ + "storage": "ELASTICSEARCH", + "ipc": "\\\\.\\pipe\\geth.ipc", + "os": "WINDOWS", + "index": "ethereum-ingest-1" +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..cabadf6 --- /dev/null +++ b/build.gradle @@ -0,0 +1,72 @@ +apply plugin: 'java' +apply plugin: 'idea' +apply plugin: 'maven' + +project.version = "1.0.0-SNAPSHOT" +project.group = 'com.codingchili.ethereumingest' + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +repositories { + mavenLocal() + maven { url 'https://jitpack.io' } + mavenCentral() +} + +dependencies { + compile 'com.github.codingchili.chili-core:core:1.0.6-SNAPSHOT' + compile 'org.web3j:core:2.3.1' +} + +test { + testLogging { + exceptionFormat "full" + } + reports.html.enabled = false +} + +task sourcesJar(type: Jar) { + classifier 'sources' + from sourceSets.main.allSource +} + +jar { + zip64 true + from { + (configurations.runtime).collect { + it.isDirectory() ? it : zipTree(it) + } + } + manifest { + attributes 'Implementation-Title': 'ethereum-ingest', + 'Implementation-Version': version, + 'Main-Class': 'com.codingchili.ethereumingest.Service' + } + exclude 'META-INF/*.RSA', 'META-INF/*.DSA', 'META-INF/*.SF' +} + +task alljavadoc(type: Javadoc) { + source subprojects.collect { it.sourceSets.main.allJava } + classpath = files(subprojects.collect { it.sourceSets.main.compileClasspath }) + destinationDir = file("${buildDir}/docs/javadoc") +} + +task archiveJavadoc(type: Zip, dependsOn: alljavadoc) { + baseName = 'javadocs' + from fileTree(file("${buildDir}/docs/javadoc")) +} + +task testReport(type: TestReport, dependsOn: 'build') { + destinationDir = file("$buildDir/reports/allTests") + reportOn subprojects*.test +} + +task archiveTestReport(type: Zip, dependsOn: testReport) { + baseName = 'testreport' + from fileTree(file("$buildDir/reports/allTests")) +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..0e73cd6 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Mar 14 08:11:26 CET 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-bin.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4453cce --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/com/codingchili/ethereumingest/ApplicationConfig.java b/src/main/java/com/codingchili/ethereumingest/ApplicationConfig.java new file mode 100644 index 0000000..a5a532a --- /dev/null +++ b/src/main/java/com/codingchili/ethereumingest/ApplicationConfig.java @@ -0,0 +1,96 @@ +package com.codingchili.ethereumingest; + +import com.codingchili.core.configuration.Configurable; +import com.codingchili.core.storage.ElasticMap; +import com.codingchili.core.storage.HazelMap; +import com.codingchili.core.storage.IndexedMapPersisted; +import com.codingchili.core.storage.IndexedMapVolatile; +import com.codingchili.core.storage.MongoDBMap; + +import static com.codingchili.ethereumingest.ApplicationConfig.OSType.WINDOWS; +import static com.codingchili.ethereumingest.ApplicationConfig.StorageType.ELASTICSEARCH; + +/** + * Representation of configuration file.s + */ +public class ApplicationConfig implements Configurable { + private String path = "application.config"; + private StorageType storage = ELASTICSEARCH; + private String ipc = "\\\\.\\pipe\\geth.ipc"; + private OSType os = WINDOWS; + private String index = "ethereum-ingest"; + + static { + System.setProperty("es.set.netty.runtime.available.processors", "false"); + } + + public String getIpc() { + return ipc; + } + + public void setIpc(String ipc) { + this.ipc = ipc; + } + + public OSType getOs() { + return os; + } + + public void setOs(OSType os) { + this.os = os; + } + + public StorageType getStorage() { + return storage; + } + + public void setStorage(StorageType storage) { + this.storage = storage; + } + + @Override + public String getPath() { + return path; + } + + @Override + public void setPath(String path) { + this.path = path; + } + + public String getIndex() { + return index; + } + + public void setIndex(String index) { + this.index = index; + } + + public Class getStoragePlugin() { + switch (storage) { + case MONGODB: + return MongoDBMap.class; + case ELASTICSEARCH: + return ElasticMap.class; + case HAZELCAST: + return HazelMap.class; + case SQLITE: + return IndexedMapPersisted.class; + case MEMORY: + return IndexedMapVolatile.class; + } + throw new IllegalArgumentException("Missing 'storage' in 'application.config'"); + } + + public enum OSType { + UNIX, WINDOWS + } + + public enum StorageType { + MONGODB, + ELASTICSEARCH, + HAZELCAST, + SQLITE, + MEMORY + } +} diff --git a/src/main/java/com/codingchili/ethereumingest/BlockHandler.java b/src/main/java/com/codingchili/ethereumingest/BlockHandler.java new file mode 100644 index 0000000..53b68eb --- /dev/null +++ b/src/main/java/com/codingchili/ethereumingest/BlockHandler.java @@ -0,0 +1,82 @@ +package com.codingchili.ethereumingest; + +import com.codingchili.core.context.CoreContext; +import com.codingchili.core.files.Configurations; +import com.codingchili.core.listener.CoreHandler; +import com.codingchili.core.listener.Request; +import com.codingchili.core.logging.Level; +import com.codingchili.core.logging.Logger; +import com.codingchili.core.protocol.Address; +import com.codingchili.core.protocol.Roles; +import com.codingchili.core.storage.AsyncStorage; +import com.codingchili.core.storage.StorageLoader; +import io.vertx.core.Future; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.ipc.UnixIpcService; +import org.web3j.protocol.ipc.WindowsIpcService; + +import static com.codingchili.core.protocol.RoleMap.PUBLIC; + +@Roles(PUBLIC) +@Address("api") +public class BlockHandler implements CoreHandler { + private ApplicationConfig config = Configurations.get("application.json", ApplicationConfig.class); + private CoreContext core; + private Logger logger; + + @Override + public void init(CoreContext core) { + this.core = core; + this.logger = core.logger(getClass()); + } + + @Override + public void start(Future start) { + logger.log("Loading block storage " + config.getStorage()); + blockStorage().setHandler(storage -> { + logger.log("Block storage loaded, using index " + config.getIndex()); + logger.log("Subscribing to ipc.. " + config.getIpc() + " on " + config.getOs()); + Web3j web = getIpcClient(); + logger.log("Successfully connected to ipc, waiting for blocks.."); + start.complete(); + + web.blockObservable(false).subscribe(block -> { + String blockNumber = block.getBlock().getNumberRaw(); + logger.log("Received block number " + blockNumber); + + storage.result().put(new EthereumBlock(block.getBlock()), done -> { + if (done.succeeded()) { + logger.log("Persisted block " + blockNumber); + } else { + logger.log("Failed to persist block " + blockNumber, Level.SEVERE); + } + }); + }); + }); + } + + private Web3j getIpcClient() { + if (config.getOs().equals(ApplicationConfig.OSType.WINDOWS)) { + return Web3j.build(new WindowsIpcService(config.getIpc())); + } else { + return Web3j.build(new UnixIpcService(config.getIpc())); + } + } + + private Future> blockStorage() { + Future> future = Future.future(); + + new StorageLoader(core) + .withDB(config.getIndex(), config.getIndex()) + .withClass(EthereumBlock.class) + .withPlugin(config.getStoragePlugin()) + .build(future); + + return future; + } + + @Override + public void handle(Request request) { + request.accept(); + } +} \ No newline at end of file diff --git a/src/main/java/com/codingchili/ethereumingest/EthereumBlock.java b/src/main/java/com/codingchili/ethereumingest/EthereumBlock.java new file mode 100644 index 0000000..a87e2ee --- /dev/null +++ b/src/main/java/com/codingchili/ethereumingest/EthereumBlock.java @@ -0,0 +1,93 @@ +package com.codingchili.ethereumingest; + +import com.codingchili.core.storage.Storable; +import org.web3j.protocol.core.methods.response.EthBlock; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +/** + * Contains block data. + */ +public class EthereumBlock implements Storable { + private String hash; + private Long number; + private Long size; + private String difficulty; + private String author; + private String miner; + private String timestamp; + + public EthereumBlock(EthBlock.Block block) { + this.number = block.getNumber().longValue(); + this.hash = block.getHash(); + this.difficulty = block.getDifficultyRaw(); + this.author = block.getAuthor(); + this.miner = block.getMiner(); + this.timestamp = ZonedDateTime.ofInstant(Instant.ofEpochSecond(block.getTimestamp().intValue()), + ZoneId.systemDefault()).toOffsetDateTime().toString(); + this.size = block.getSize().longValue(); + } + + @Override + public String id() { + return hash; + } + + public String getHash() { + return hash; + } + + public void setHash(String hash) { + this.hash = hash; + } + + public String getDifficulty() { + return difficulty; + } + + public void setDifficulty(String difficulty) { + this.difficulty = difficulty; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getMiner() { + return miner; + } + + public void setMiner(String miner) { + this.miner = miner; + } + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + public Long getSize() { + return size; + } + + public void setSize(Long size) { + this.size = size; + } + + public Long getNumber() { + return number; + } + + public void setNumber(Long number) { + this.number = number; + } +} diff --git a/src/main/java/com/codingchili/ethereumingest/Service.java b/src/main/java/com/codingchili/ethereumingest/Service.java new file mode 100644 index 0000000..3739ac1 --- /dev/null +++ b/src/main/java/com/codingchili/ethereumingest/Service.java @@ -0,0 +1,37 @@ +package com.codingchili.ethereumingest; + +import com.codingchili.core.Launcher; +import com.codingchili.core.context.CoreContext; +import com.codingchili.core.listener.CoreService; +import io.vertx.core.Future; + +import static com.codingchili.core.files.Configurations.launcher; +import static com.codingchili.core.files.Configurations.system; + +public class Service implements CoreService { + private CoreContext core; + + public static void main(String[] args) { + system().setHandlers(1).setListeners(1); + launcher().setApplication("EthereumIngest") + .setVersion("1.0.0") + .deployable(Service.class); + Launcher.main(args); + } + + @Override + public void init(CoreContext core) { + this.core = core; + } + + @Override + public void start(Future start) { + core.handler(BlockHandler::new).setHandler(done -> { + if (done.succeeded()) { + start.complete(); + } else { + start.fail(done.cause()); + } + }); + } +}