Repository: neo4j-contrib/neo4j-apoc-procedures
Pull request #68 (2016-07-15 07:40:52)

implementing apoc.util.sleep


Patch:
diff --git a/readme.adoc b/readme.adoc
index 78f95dd4a..3183691ec 100644
--- a/readme.adoc
+++ b/readme.adoc
@@ -2,7 +2,7 @@
 :readme:
 :img: https://raw.githubusercontent.com/neo4j-contrib/neo4j-apoc-procedures/master/docs/img
 
-image:https://travis-ci.org/neo4j-contrib/neo4j-apoc-procedures.svg[]
+image:https://travis-ci.org/neo4j-contrib/neo4j-apoc-procedures.svg[alt="Travis CI Status", link="https://travis-ci.org/neo4j-contrib/neo4j-apoc-procedures"]
 
 https://neo4j-contrib.github.io/neo4j-apoc-procedures[Documentation (WIP)].
 
@@ -563,6 +563,7 @@ Sometimes type information gets lost, these functions help you to coerce an "Any
 |===
 | apoc.util.sha1([values]) | computes the sha1 of the concatenation of all string values of the list
 | apoc.util.md5([values]) | computes the md5 of the concatenation of all string values of the list
+| apoc.util.sleep({duration}) | sleeps for <duration> millis, transaction termination is honored
 |===
 
 === Date/time Support
diff --git a/src/main/java/apoc/util/Utils.java b/src/main/java/apoc/util/Utils.java
index a803b3658..33f6e89b7 100644
--- a/src/main/java/apoc/util/Utils.java
+++ b/src/main/java/apoc/util/Utils.java
@@ -4,6 +4,7 @@ import apoc.Description;
 import apoc.result.StringResult;
 import org.apache.commons.codec.digest.DigestUtils;
 import org.neo4j.graphdb.GraphDatabaseService;
+import org.neo4j.kernel.api.KernelTransaction;
 import org.neo4j.procedure.Context;
 import org.neo4j.procedure.Name;
 import org.neo4j.procedure.Procedure;
@@ -20,6 +21,9 @@ public class Utils {
     @Context
     public GraphDatabaseService db;
 
+    @Context
+    public KernelTransaction transaction;
+
     @Procedure
     @Description("apoc.util.sha1([values]) | computes the sha1 of the concatenation of all string values of the list")
     public Stream<StringResult> sha1(@Name("values") List<Object> values) {
@@ -33,4 +37,16 @@ public class Utils {
         String value = values.stream().map(v -> v == null ? "" : v.toString()).collect(Collectors.joining());
         return Stream.of(new StringResult(DigestUtils.md5Hex(value)));
     }
+
+    @Procedure
+    @Description("apoc.util.sleep(<duration>) | sleeps for <duration> millis, transaction termination is honored")
+    public void sleep(@Name("duration") long duration) throws InterruptedException {
+        long started = System.currentTimeMillis();
+        while (System.currentTimeMillis()-started < duration) {
+            Thread.sleep(5);
+            if (transaction.shouldBeTerminated()) {
+                return;
+            }
+        }
+    }
 }


Test patch:
diff --git a/src/test/java/apoc/util/TestUtil.java b/src/test/java/apoc/util/TestUtil.java
index f5c007d55..0a045472f 100644
--- a/src/test/java/apoc/util/TestUtil.java
+++ b/src/test/java/apoc/util/TestUtil.java
@@ -1,5 +1,6 @@
 package apoc.util;
 
+import org.hamcrest.Matcher;
 import org.neo4j.graphdb.GraphDatabaseService;
 import org.neo4j.graphdb.Result;
 import org.neo4j.graphdb.Transaction;
@@ -11,12 +12,12 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
 import java.util.function.Consumer;
+import java.util.function.Supplier;
 
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 
-//import org.neo4j.kernel.GraphDatabaseAPI;
-
 /**
  * @author mh
  * @since 26.02.16
@@ -119,4 +120,15 @@ public class TestUtil {
             }
         }
     }
+
+    public static <T> T assertDuration(Matcher<? super Long> matcher, Supplier<T> function) {
+        long start = System.currentTimeMillis();
+        T result = null;
+        try {
+            result = function.get();
+        } finally {
+            assertThat("duration " + matcher, System.currentTimeMillis()-start, matcher);
+            return result;
+        }
+    }
 }
diff --git a/src/test/java/apoc/util/UtilsTest.java b/src/test/java/apoc/util/UtilsTest.java
index c0b8dfba1..e9e8b4894 100644
--- a/src/test/java/apoc/util/UtilsTest.java
+++ b/src/test/java/apoc/util/UtilsTest.java
@@ -1,11 +1,19 @@
 package apoc.util;
 
+import org.hamcrest.Matchers;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.neo4j.graphdb.GraphDatabaseService;
+import org.neo4j.graphdb.Result;
+import org.neo4j.graphdb.Transaction;
 import org.neo4j.test.TestGraphDatabaseFactory;
 
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.function.Predicate;
+
 import static org.junit.Assert.assertEquals;
 
 /**
@@ -29,12 +37,67 @@ public class UtilsTest {
 
     @Test
     public void testSha1() throws Exception {
-        TestUtil.testCall(db, "call apoc.util.sha1(['ABC'])", r -> assertEquals("3c01bdbb26f358bab27f267924aa2c9a03fcfdb8",r.get("value")));
+        TestUtil.testCall(db, "call apoc.util.sha1(['ABC'])", r -> assertEquals("3c01bdbb26f358bab27f267924aa2c9a03fcfdb8", r.get("value")));
     }
 
     @Test
     public void testMd5() throws Exception {
-        TestUtil.testCall(db, "call apoc.util.md5(['ABC'])", r -> assertEquals("902fbdd2b1df0c4f70b4a5d23525e932",r.get("value")));
+        TestUtil.testCall(db, "call apoc.util.md5(['ABC'])", r -> assertEquals("902fbdd2b1df0c4f70b4a5d23525e932", r.get("value")));
+    }
+
+    @Test
+    public void testSleep() {
+        String cypherSleep = "call apoc.util.sleep({duration})";
+        TestUtil.testCall(db, cypherSleep, MapUtil.map("duration", 0l), r -> {
+        });  // force building query plan
+
+        long duration = 300;
+        TestUtil.assertDuration(Matchers.greaterThanOrEqualTo(duration), () -> {
+            TestUtil.testCall(db, cypherSleep, MapUtil.map("duration", duration), r -> { });
+            return null;
+        });
+    }
+
+    @Test
+    public void testSleepWithTerminate() {
+        String cypherSleep = "call apoc.util.sleep({duration})";
+        TestUtil.testCall(db, cypherSleep, MapUtil.map("duration", 0l), r -> {
+        });  // force building query plan
+
+        long duration = 300;
+        TestUtil.assertDuration(Matchers.lessThan(duration), () -> {
+            final Transaction[] tx = new Transaction[1];
+
+            Future future = Executors.newSingleThreadScheduledExecutor().submit( () -> {
+                tx[0] = db.beginTx();
+                try {
+                    Result result = db.execute(cypherSleep, MapUtil.map("duration", 10000));
+                    tx[0].success();
+                    return result;
+                } finally {
+                    tx[0].close();
+                }
+            });
+
+            sleepUntil( dummy -> tx[0]!=null );
+            tx[0].terminate();
+            try {
+                future.get();
+            } catch (InterruptedException|ExecutionException e) {
+                throw new RuntimeException(e);
+            }
+            return null;
+        });
+    }
+
+    private void sleepUntil(Predicate<Void> predicate) {
+        while (!predicate.test(null)) {
+            try {
+                Thread.sleep(5);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
     }
 
 }