/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.tinkerpop.gremlin.driver; import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.Traverser; import org.apache.tinkerpop.gremlin.process.traversal.step.util.BulkSet; import org.javatuples.Pair; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicReference; /** * A queue of incoming {@link Result} objects. The queue is updated by the {@link Handler.GremlinResponseHandler} * until a response terminator is identified. * * @author Stephen Mallette (http://stephen.genoprime.com) */ @SuppressWarnings("ThrowableResultOfMethodCallIgnored") final class ResultQueue { private final LinkedBlockingQueue<Result> resultLinkedBlockingQueue; private Object aggregatedResult = null; private final AtomicReference<Throwable> error = new AtomicReference<>(); private final CompletableFuture<Void> readComplete; private final Queue<Pair<CompletableFuture<List<Result>>,Integer>> waiting = new ConcurrentLinkedQueue<>(); public ResultQueue(final LinkedBlockingQueue<Result> resultLinkedBlockingQueue, final CompletableFuture<Void> readComplete) { this.resultLinkedBlockingQueue = resultLinkedBlockingQueue; this.readComplete = readComplete; } /** * Adds a {@link Result} to the queue which will be later read by the {@link ResultSet}. * * @param result a return value from the {@link Traversal} or script submitted for execution */ public void add(final Result result) { this.resultLinkedBlockingQueue.offer(result); tryDrainNextWaiting(false); } /** * Adds a side-effect to the queue which may later be read by the {@link ResultSet}. Note that the side-effect * is only returned when a {@link Traversal} is submitted and refers to the side-effects defined in that traversal. * A "script" will not return side-effects. * * @param aggregateTo the value of the {@link ResponseMessage} metadata for {@link Tokens#ARGS_AGGREGATE_TO}. * @param sideEffectValue the value of the side-effect itself */ public void addSideEffect(final String aggregateTo, final Object sideEffectValue) { switch (aggregateTo) { case Tokens.VAL_AGGREGATE_TO_BULKSET: if (!(sideEffectValue instanceof Traverser.Admin)) throw new IllegalStateException(String.format("Side-effect value %s is a %s which does not aggregate to %s", sideEffectValue, sideEffectValue.getClass().getSimpleName(), aggregateTo)); if (null == aggregatedResult) aggregatedResult = new BulkSet(); final BulkSet<Object> bs = validate(aggregateTo, BulkSet.class); final Traverser.Admin traverser = (Traverser.Admin) sideEffectValue; bs.add(traverser.get(), traverser.bulk()); break; case Tokens.VAL_AGGREGATE_TO_LIST: if (null == aggregatedResult) aggregatedResult = new ArrayList(); final List<Object> list = validate(aggregateTo, List.class); list.add(sideEffectValue); break; case Tokens.VAL_AGGREGATE_TO_SET: if (null == aggregatedResult) aggregatedResult = new HashSet(); final Set<Object> set = validate(aggregateTo, Set.class); set.add(sideEffectValue); break; case Tokens.VAL_AGGREGATE_TO_MAP: if (!(sideEffectValue instanceof Map.Entry) && !(sideEffectValue instanceof Map)) throw new IllegalStateException(String.format("Side-effect value %s is a %s which does not aggregate to %s", sideEffectValue, sideEffectValue.getClass().getSimpleName(), aggregateTo)); // some serialization formats (e.g. graphson) may deserialize a Map.Entry to a Map with a single entry if (sideEffectValue instanceof Map && ((Map) sideEffectValue).size() != 1) throw new IllegalStateException(String.format("Side-effect value %s is a %s which does not aggregate to %s as it is a Map that does not have one entry", sideEffectValue, sideEffectValue.getClass().getSimpleName(), aggregateTo)); if (null == aggregatedResult) aggregatedResult = new HashMap(); final Map<Object,Object > m = validate(aggregateTo, Map.class); final Map.Entry entry = sideEffectValue instanceof Map.Entry ? (Map.Entry) sideEffectValue : (Map.Entry) ((Map) sideEffectValue).entrySet().iterator().next(); m.put(entry.getKey(), entry.getValue()); break; case Tokens.VAL_AGGREGATE_TO_NONE: if (null == aggregatedResult) aggregatedResult = sideEffectValue; break; default: throw new IllegalStateException(String.format("%s is an invalid value for %s", aggregateTo, Tokens.ARGS_AGGREGATE_TO)); } } private <V> V validate(final String aggregateTo, final Class<?> expected) { if (!(expected.isAssignableFrom(aggregatedResult.getClass()))) throw new IllegalStateException(String.format("Side-effect \"%s\" contains the type %s that is not acceptable for %s", aggregatedResult.getClass().getSimpleName(), aggregateTo)); return (V) aggregatedResult; } public CompletableFuture<List<Result>> await(final int items) { final CompletableFuture<List<Result>> result = new CompletableFuture<>(); waiting.add(Pair.with(result, items)); tryDrainNextWaiting(false); return result; } public int size() { if (error.get() != null) throw new RuntimeException(error.get()); return this.resultLinkedBlockingQueue.size(); } public boolean isEmpty() { if (error.get() != null) throw new RuntimeException(error.get()); return this.size() == 0; } public boolean isComplete() { return readComplete.isDone(); } void drainTo(final Collection<Result> collection) { if (error.get() != null) throw new RuntimeException(error.get()); resultLinkedBlockingQueue.drainTo(collection); } void markComplete() { // if there was some aggregation performed in the queue then the full object is hanging out waiting to be // added to the ResultSet if (aggregatedResult != null) add(new Result(aggregatedResult)); this.readComplete.complete(null); this.drainAllWaiting(); } void markError(final Throwable throwable) { error.set(throwable); this.readComplete.completeExceptionally(throwable); this.drainAllWaiting(); } /** * Completes the next waiting future if there is one. */ private synchronized void tryDrainNextWaiting(final boolean force) { // need to peek because the number of available items needs to be >= the expected size for that future. if not // it needs to keep waiting final Pair<CompletableFuture<List<Result>>, Integer> nextWaiting = waiting.peek(); if (nextWaiting != null && (force || (resultLinkedBlockingQueue.size() >= nextWaiting.getValue1() || readComplete.isDone()))) { final int items = nextWaiting.getValue1(); final CompletableFuture<List<Result>> future = nextWaiting.getValue0(); final List<Result> results = new ArrayList<>(items); resultLinkedBlockingQueue.drainTo(results, items); // it's important to check for error here because a future may have already been queued in "waiting" prior // to the first response back from the server. if that happens, any "waiting" futures should be completed // exceptionally otherwise it will look like success. if (null == error.get()) future.complete(results); else future.completeExceptionally(error.get()); waiting.remove(nextWaiting); } } /** * Completes all remaining futures. */ private void drainAllWaiting() { while (!waiting.isEmpty()) { tryDrainNextWaiting(true); } } }