-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add failOnError and return status options to apoc.cypher.timeboxed #711
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ | |
*/ | ||
package apoc.cypher; | ||
|
||
import static apoc.util.Util.toBoolean; | ||
import static java.util.concurrent.TimeUnit.MILLISECONDS; | ||
|
||
import apoc.Pools; | ||
|
@@ -30,6 +31,7 @@ | |
import java.util.stream.Stream; | ||
import java.util.stream.StreamSupport; | ||
import org.neo4j.graphdb.GraphDatabaseService; | ||
import org.neo4j.graphdb.QueryExecutionException; | ||
import org.neo4j.graphdb.Result; | ||
import org.neo4j.graphdb.Transaction; | ||
import org.neo4j.graphdb.TransactionTerminatedException; | ||
|
@@ -68,11 +70,20 @@ public Stream<CypherStatementMapResult> runTimeboxed( | |
@Name(value = "statement", description = "The Cypher statement to run.") String cypher, | ||
@Name(value = "params", description = "The parameters for the given Cypher statement.") | ||
Map<String, Object> params, | ||
@Name(value = "timeout", description = "The maximum time the statement can run for.") long timeout) { | ||
@Name(value = "timeout", description = "The maximum time, in milliseconds, the statement can run for.") | ||
long timeout, | ||
@Name( | ||
value = "config", | ||
defaultValue = "{}", | ||
description = "{ failOnError = false :: BOOLEAN, appendStatusRow = false :: BOOLEAN }") | ||
Map<String, Object> config) { | ||
|
||
final BlockingQueue<Map<String, Object>> queue = new ArrayBlockingQueue<>(100); | ||
final AtomicReference<Transaction> txAtomic = new AtomicReference<>(); | ||
|
||
boolean failOnError = toBoolean(config.get("failOnError")); | ||
boolean appendStatusRow = toBoolean(config.get("appendStatusRow")); | ||
|
||
// run query to be timeboxed in a separate thread to enable proper tx termination | ||
// if we'd run this in current thread, a tx.terminate would kill the transaction the procedure call uses itself. | ||
pools.getDefaultExecutorService().submit(() -> { | ||
|
@@ -89,9 +100,22 @@ public Stream<CypherStatementMapResult> runTimeboxed( | |
final Map<String, Object> map = result.next(); | ||
offerToQueue(queue, map, timeout); | ||
} | ||
if (appendStatusRow) { | ||
Map<String, Object> map = statusMap(true, false, null); | ||
offerToQueue(queue, map, timeout); | ||
} | ||
innerTx.commit(); | ||
} catch (TransactionTerminatedException e) { | ||
log.warn("query " + cypher + " has been terminated"); | ||
if (appendStatusRow || failOnError) { | ||
Map<String, Object> map = statusMap(false, true, null); | ||
offerToQueue(queue, map, timeout); | ||
} | ||
} catch (QueryExecutionException e) { | ||
if (appendStatusRow || failOnError) { | ||
Map<String, Object> map = statusMap(false, false, e.getMessage()); | ||
offerToQueue(queue, map, timeout); | ||
} | ||
} finally { | ||
offerToQueue(queue, POISON, timeout); | ||
txAtomic.set(null); | ||
|
@@ -107,6 +131,10 @@ public Stream<CypherStatementMapResult> runTimeboxed( | |
log.debug( | ||
"tx is null, either the other transaction finished gracefully or has not yet been start."); | ||
} else { | ||
if (appendStatusRow || failOnError) { | ||
Map<String, Object> map = statusMap(false, true, null); | ||
offerToQueue(queue, map, timeout); | ||
} | ||
tx.terminate(); | ||
offerToQueue(queue, POISON, timeout); | ||
log.warn("terminating transaction, putting POISON onto queue"); | ||
|
@@ -128,8 +156,26 @@ public boolean hasNext() { | |
try { | ||
nextElement = queue.poll(timeout, MILLISECONDS); | ||
if (nextElement == null) { | ||
log.warn("couldn't grab queue element, aborting - this should never happen"); | ||
hasFinished = true; | ||
// Wait a little bit longer and try again, waiting exactly the timeout means that | ||
// there might be a slight timing issue with termination vs. setting the queue as terminated | ||
nextElement = queue.poll(100, MILLISECONDS); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't we simply add 100 to the first poll instead? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my testing it only happened on failed queries, so I figured it was nicer to not add 100ms to all polling for those that work |
||
// If it is still null, then accept and move on | ||
if (nextElement == null) { | ||
log.warn("Empty queue, aborting."); | ||
if (failOnError) { | ||
throw new RuntimeException("The query has been terminated."); | ||
} | ||
hasFinished = true; | ||
} | ||
} | ||
|
||
if (failOnError && nextElement.get("wasSuccessful").equals(Boolean.FALSE)) { | ||
if (nextElement.get("failedWithError").equals(Boolean.TRUE)) { | ||
throw new RuntimeException("The inner query errored with: " + nextElement.get("error")); | ||
} | ||
if (nextElement.get("wasTerminated").equals(Boolean.TRUE)) { | ||
throw new RuntimeException("The query has been terminated."); | ||
} | ||
} else { | ||
hasFinished = POISON.equals(nextElement); | ||
} | ||
|
@@ -145,10 +191,20 @@ public Map<String, Object> next() { | |
return nextElement; | ||
} | ||
}; | ||
|
||
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(queueConsumer, Spliterator.ORDERED), false) | ||
.map(CypherStatementMapResult::new); | ||
} | ||
|
||
private Map<String, Object> statusMap(boolean successful, boolean terminated, String errorMessage) { | ||
Map<String, Object> map = new HashMap<>(); | ||
map.put("wasSuccessful", successful ? Boolean.TRUE : Boolean.FALSE); | ||
map.put("wasTerminated", terminated ? Boolean.TRUE : Boolean.FALSE); | ||
map.put("failedWithError", errorMessage == null ? Boolean.FALSE : Boolean.TRUE); | ||
map.put("error", errorMessage); | ||
return map; | ||
} | ||
|
||
private void offerToQueue(BlockingQueue<Map<String, Object>> queue, Map<String, Object> map, long timeout) { | ||
try { | ||
boolean hasBeenAdded = queue.offer(map, timeout, MILLISECONDS); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not of concern for this PR, but it's not allowed to access transactions from multiple threads as we do with this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...perhaps innocent enough since we only terminate it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, APOC is such a can of worms, sometimes tunnel vision (aka ignoring these other issues) is the only way to complete a task :P